Introduction
Azure’s vastness can be overwhelming—hundreds of services, multiple ways to accomplish the same thing, and pricing models that require a spreadsheet to understand. After managing Azure infrastructure for several years, I’ve developed patterns that balance capability with cost.
This guide covers practical Azure infrastructure: networking fundamentals, compute choices, security patterns, and cost optimization strategies.
Azure Networking Fundamentals
Virtual Network Architecture
Start with a hub-and-spoke topology:
┌─────────────────────┐
│ Hub VNet │
│ (10.0.0.0/16) │
│ │
│ ┌───────────────┐ │
│ │ Gateway Subnet│ │───── VPN/ExpressRoute
│ │ (10.0.1.0/24) │ │
│ └───────────────┘ │
│ ┌───────────────┐ │
│ │ Firewall │ │
│ │ (10.0.2.0/24) │ │
│ └───────────────┘ │
└──────────┬──────────┘
│ Peering
┌─────────────────────┼─────────────────────┐
│ │ │
┌────────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐
│ Spoke: Prod │ │ Spoke: Dev │ │ Spoke: Mgmt │
│ (10.1.0.0/16) │ │ (10.2.0.0/16) │ │ (10.3.0.0/16) │
│ │ │ │ │ │
│ Web: 10.1.1.0/24│ │ Web: 10.2.1.0/24│ │ Jump: 10.3.1.0/24│
│ App: 10.1.2.0/24│ │ App: 10.2.2.0/24│ │ Mon: 10.3.2.0/24│
│ DB: 10.1.3.0/24│ │ DB: 10.2.3.0/24│ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Infrastructure as Code with Bicep
// main.bicep
targetScope = 'subscription'
param location string = 'eastus'
param environment string = 'prod'
// Resource Group
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: 'rg-infra-${environment}'
location: location
}
// Hub Virtual Network
module hubVnet 'modules/vnet.bicep' = {
scope: rg
name: 'hubVnet'
params: {
name: 'vnet-hub-${environment}'
location: location
addressPrefix: '10.0.0.0/16'
subnets: [
{
name: 'GatewaySubnet'
addressPrefix: '10.0.1.0/24'
}
{
name: 'AzureFirewallSubnet'
addressPrefix: '10.0.2.0/24'
}
{
name: 'snet-management'
addressPrefix: '10.0.3.0/24'
}
]
}
}
// Production Spoke
module prodVnet 'modules/vnet.bicep' = {
scope: rg
name: 'prodVnet'
params: {
name: 'vnet-prod-${environment}'
location: location
addressPrefix: '10.1.0.0/16'
subnets: [
{
name: 'snet-web'
addressPrefix: '10.1.1.0/24'
nsgId: webNsg.outputs.id
}
{
name: 'snet-app'
addressPrefix: '10.1.2.0/24'
nsgId: appNsg.outputs.id
}
{
name: 'snet-db'
addressPrefix: '10.1.3.0/24'
nsgId: dbNsg.outputs.id
}
]
}
}
// VNet Peering
module peering 'modules/peering.bicep' = {
scope: rg
name: 'hubToProdPeering'
params: {
hubVnetId: hubVnet.outputs.id
spokeVnetId: prodVnet.outputs.id
}
}
// modules/vnet.bicep
param name string
param location string
param addressPrefix string
param subnets array
resource vnet 'Microsoft.Network/virtualNetworks@2021-08-01' = {
name: name
location: location
properties: {
addressSpace: {
addressPrefixes: [addressPrefix]
}
subnets: [for subnet in subnets: {
name: subnet.name
properties: {
addressPrefix: subnet.addressPrefix
networkSecurityGroup: contains(subnet, 'nsgId') ? {
id: subnet.nsgId
} : null
privateEndpointNetworkPolicies: 'Disabled'
}
}]
}
}
output id string = vnet.id
output name string = vnet.name
Network Security Groups
// modules/nsg.bicep
param name string
param location string
param rules array
resource nsg 'Microsoft.Network/networkSecurityGroups@2021-08-01' = {
name: name
location: location
properties: {
securityRules: rules
}
}
output id string = nsg.id
// Web tier NSG example
module webNsg 'modules/nsg.bicep' = {
scope: rg
name: 'webNsg'
params: {
name: 'nsg-web-${environment}'
location: location
rules: [
{
name: 'AllowHTTPS'
properties: {
priority: 100
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: 'Internet'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '443'
}
}
{
name: 'AllowLoadBalancer'
properties: {
priority: 110
direction: 'Inbound'
access: 'Allow'
protocol: '*'
sourceAddressPrefix: 'AzureLoadBalancer'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
}
}
{
name: 'DenyAllInbound'
properties: {
priority: 4096
direction: 'Inbound'
access: 'Deny'
protocol: '*'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
}
}
]
}
}
Compute Patterns
VM Sizing Guide
| Workload | Series | Example | Use Case |
|---|---|---|---|
| General | D | D4s_v5 | Web servers, apps |
| Compute | F | F8s_v2 | Batch processing |
| Memory | E | E8s_v5 | Databases, caching |
| Storage | L | L8s_v3 | Data warehouses |
| GPU | NC | NC6s_v3 | ML training |
Cost-Effective VM Strategy
// Spot VMs for non-critical workloads
resource spotVm 'Microsoft.Compute/virtualMachines@2021-11-01' = {
name: 'vm-worker-${uniqueString(resourceGroup().id)}'
location: location
properties: {
hardwareProfile: {
vmSize: 'Standard_D4s_v5'
}
priority: 'Spot'
evictionPolicy: 'Deallocate'
billingProfile: {
maxPrice: 0.1 // Max price per hour
}
// ... other properties
}
}
VM Scale Sets
resource vmss 'Microsoft.Compute/virtualMachineScaleSets@2021-11-01' = {
name: 'vmss-web-${environment}'
location: location
sku: {
name: 'Standard_D2s_v5'
tier: 'Standard'
capacity: 2
}
properties: {
overprovision: true
upgradePolicy: {
mode: 'Rolling'
rollingUpgradePolicy: {
maxBatchInstancePercent: 25
maxUnhealthyInstancePercent: 25
pauseTimeBetweenBatches: 'PT5S'
}
}
virtualMachineProfile: {
osProfile: {
computerNamePrefix: 'web'
adminUsername: 'azureuser'
linuxConfiguration: {
disablePasswordAuthentication: true
ssh: {
publicKeys: [
{
path: '/home/azureuser/.ssh/authorized_keys'
keyData: sshPublicKey
}
]
}
}
}
storageProfile: {
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Premium_LRS'
}
}
imageReference: {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
}
networkProfile: {
networkInterfaceConfigurations: [
{
name: 'nic-config'
properties: {
primary: true
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: {
id: webSubnetId
}
loadBalancerBackendAddressPools: [
{
id: loadBalancer.properties.backendAddressPools[0].id
}
]
}
}
]
}
}
]
}
}
}
}
// Autoscale settings
resource autoscale 'Microsoft.Insights/autoscalesettings@2021-05-01-preview' = {
name: 'autoscale-vmss'
location: location
properties: {
targetResourceUri: vmss.id
profiles: [
{
name: 'default'
capacity: {
minimum: '2'
maximum: '10'
default: '2'
}
rules: [
{
metricTrigger: {
metricName: 'Percentage CPU'
metricResourceUri: vmss.id
timeGrain: 'PT1M'
statistic: 'Average'
timeWindow: 'PT5M'
timeAggregation: 'Average'
operator: 'GreaterThan'
threshold: 75
}
scaleAction: {
direction: 'Increase'
type: 'ChangeCount'
value: '1'
cooldown: 'PT5M'
}
}
{
metricTrigger: {
metricName: 'Percentage CPU'
metricResourceUri: vmss.id
timeGrain: 'PT1M'
statistic: 'Average'
timeWindow: 'PT5M'
timeAggregation: 'Average'
operator: 'LessThan'
threshold: 25
}
scaleAction: {
direction: 'Decrease'
type: 'ChangeCount'
value: '1'
cooldown: 'PT5M'
}
}
]
}
]
}
}
Security Patterns
Private Endpoints
Keep data plane traffic off the internet:
// Private endpoint for Azure SQL
resource sqlPrivateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = {
name: 'pe-sql-${environment}'
location: location
properties: {
subnet: {
id: dbSubnetId
}
privateLinkServiceConnections: [
{
name: 'sql-connection'
properties: {
privateLinkServiceId: sqlServer.id
groupIds: ['sqlServer']
}
}
]
}
}
// DNS integration
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
name: 'privatelink.database.windows.net'
location: 'global'
}
resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
parent: privateDnsZone
name: 'link-hub'
location: 'global'
properties: {
virtualNetwork: {
id: hubVnetId
}
registrationEnabled: false
}
}
Key Vault Integration
// Key Vault with RBAC
resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {
name: 'kv-${environment}-${uniqueString(resourceGroup().id)}'
location: location
properties: {
tenantId: subscription().tenantId
sku: {
family: 'A'
name: 'standard'
}
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
enablePurgeProtection: true
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
virtualNetworkRules: [
{
id: managementSubnetId
}
]
}
}
}
// Managed identity for VMs
resource vmIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
name: 'id-vm-${environment}'
location: location
}
// Grant secret access
resource secretsUserRole 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = {
scope: keyVault
name: guid(keyVault.id, vmIdentity.id, 'secrets-user')
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'4633458b-17de-408a-b874-0445c86b69e6' // Key Vault Secrets User
)
principalId: vmIdentity.properties.principalId
principalType: 'ServicePrincipal'
}
}
Cost Optimization
Reserved Instances Strategy
# Analyze VM usage for RI recommendations
from azure.mgmt.consumption import ConsumptionManagementClient
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
client = ConsumptionManagementClient(credential, subscription_id)
# Get reservation recommendations
recommendations = client.reservation_recommendations.list(
scope=f'/subscriptions/{subscription_id}',
filter="properties/lookBackPeriod eq 'Last30Days'"
)
for rec in recommendations:
print(f"VM Size: {rec.recommended_quantity}")
print(f"Estimated savings: ${rec.net_savings}")
print(f"Term: {rec.term}")
Auto-Shutdown for Dev/Test
// Auto-shutdown schedule
resource autoShutdown 'Microsoft.DevTestLab/schedules@2018-09-15' = {
name: 'shutdown-computevm-${vm.name}'
location: location
properties: {
status: 'Enabled'
taskType: 'ComputeVmShutdownTask'
dailyRecurrence: {
time: '1900'
}
timeZoneId: 'Pacific Standard Time'
targetResourceId: vm.id
notificationSettings: {
status: 'Enabled'
timeInMinutes: 30
emailRecipient: 'team@yourorg.com'
}
}
}
Cost Alerts
resource budgetAlert 'Microsoft.Consumption/budgets@2021-10-01' = {
name: 'budget-${environment}'
properties: {
category: 'Cost'
amount: 10000
timeGrain: 'Monthly'
timePeriod: {
startDate: '2024-01-01'
}
filter: {
dimensions: {
name: 'ResourceGroup'
values: [resourceGroup().name]
}
}
notifications: {
actual_80_percent: {
enabled: true
operator: 'GreaterThanOrEqualTo'
threshold: 80
contactEmails: ['finance@yourorg.com', 'infra@yourorg.com']
}
forecasted_100_percent: {
enabled: true
operator: 'GreaterThanOrEqualTo'
threshold: 100
thresholdType: 'Forecasted'
contactEmails: ['finance@yourorg.com', 'infra@yourorg.com']
}
}
}
}
Monitoring and Diagnostics
Azure Monitor Configuration
// Log Analytics Workspace
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
name: 'log-${environment}'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 90
features: {
enableLogAccessUsingOnlyResourcePermissions: true
}
}
}
// Diagnostic settings for VMs
resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
name: 'diag-vm'
scope: vm
properties: {
workspaceId: logAnalytics.id
logs: []
metrics: [
{
category: 'AllMetrics'
enabled: true
retentionPolicy: {
days: 30
enabled: true
}
}
]
}
}
Lessons Learned
- Start with networking right. Changing VNet architecture later is painful.
- Use managed identities everywhere. Stop storing service principal secrets.
- Private endpoints are worth the complexity. Keep data off the internet.
- Reserved instances need planning. Buy RIs after you understand your baseline.
- Tagging is essential. You can’t manage costs without proper tagging.
Conclusion
Azure infrastructure requires both breadth and depth—understanding which services to use and how to configure them correctly. Start with proven patterns, automate everything with Bicep, and iterate based on real-world monitoring data.
The investment in proper architecture pays dividends: fewer security incidents, predictable costs, and infrastructure that scales with your needs.