ARM Templates & Bicep
Deploy Azure infrastructure with ARM templates and Bicep. Bicep is the recommended domain-specific language that compiles to ARM JSON, offering cleaner syntax, modules, and first-class tooling support.
When to Use
- You need Azure-native Infrastructure as Code without third-party tooling.
- Your organization standardizes on Azure and wants tight portal integration.
- You need What-If analysis before deploying changes.
- You are migrating existing ARM JSON templates to Bicep for maintainability.
- You need deployment scopes at resource group, subscription, management group, or tenant level.
Prerequisites
bash# Install Azure CLI curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash # Install Bicep CLI (bundled with Azure CLI 2.20+) az bicep install az bicep upgrade # Verify installation az bicep version # Login and set subscription az login az account set --subscription "my-subscription-id"
Bicep Fundamentals
Resource Group Deployment with Virtual Network
bicep// main.bicep @description('Azure region for all resources') param location string = resourceGroup().location @description('Environment name used for resource naming') @allowed(['dev', 'staging', 'prod']) param environment string = 'dev' @description('Base name for all resources') param baseName string var vnetName = '${baseName}-${environment}-vnet' var nsgName = '${baseName}-${environment}-nsg' resource nsg 'Microsoft.Network/networkSecurityGroups@2023-05-01' = { name: nsgName location: location properties: { securityRules: [ { name: 'AllowHTTPS' properties: { priority: 100 direction: 'Inbound' access: 'Allow' protocol: 'Tcp' sourcePortRange: '*' destinationPortRange: '443' sourceAddressPrefix: '*' destinationAddressPrefix: '*' } } { name: 'DenyAllInbound' properties: { priority: 4096 direction: 'Inbound' access: 'Deny' protocol: '*' sourcePortRange: '*' destinationPortRange: '*' sourceAddressPrefix: '*' destinationAddressPrefix: '*' } } ] } } resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = { name: vnetName location: location properties: { addressSpace: { addressPrefixes: [ '10.0.0.0/16' ] } subnets: [ { name: 'web-subnet' properties: { addressPrefix: '10.0.1.0/24' networkSecurityGroup: { id: nsg.id } } } { name: 'app-subnet' properties: { addressPrefix: '10.0.2.0/24' } } { name: 'data-subnet' properties: { addressPrefix: '10.0.3.0/24' privateEndpointNetworkPolicies: 'Enabled' } } ] } } output vnetId string = vnet.id output webSubnetId string = vnet.properties.subnets[0].id output appSubnetId string = vnet.properties.subnets[1].id
VM Deployment with Managed Identity
bicep// vm.bicep param location string = resourceGroup().location param vmName string param subnetId string param adminUsername string = 'azureuser' @secure() param adminPublicKey string resource nic 'Microsoft.Network/networkInterfaces@2023-05-01' = { name: '${vmName}-nic' location: location properties: { ipConfigurations: [ { name: 'ipconfig1' properties: { privateIPAllocationMethod: 'Dynamic' subnet: { id: subnetId } } } ] } } resource vm 'Microsoft.Compute/virtualMachines@2023-07-01' = { name: vmName location: location identity: { type: 'SystemAssigned' } properties: { hardwareProfile: { vmSize: 'Standard_B2s' } osProfile: { computerName: vmName adminUsername: adminUsername linuxConfiguration: { disablePasswordAuthentication: true ssh: { publicKeys: [ { path: '/home/${adminUsername}/.ssh/authorized_keys' keyData: adminPublicKey } ] } } } storageProfile: { imageReference: { publisher: 'Canonical' offer: '0001-com-ubuntu-server-jammy' sku: '22_04-lts-gen2' version: 'latest' } osDisk: { createOption: 'FromImage' managedDisk: { storageAccountType: 'Premium_LRS' } } } networkProfile: { networkInterfaces: [ { id: nic.id } ] } diagnosticsProfile: { bootDiagnostics: { enabled: true } } } } output vmPrincipalId string = vm.identity.principalId output vmId string = vm.id
Bicep Modules
Module Definition
bicep// modules/storage.bicep @description('Storage account name (3-24 chars, lowercase alphanumeric)') param storageAccountName string param location string = resourceGroup().location param sku string = 'Standard_LRS' @allowed(['Hot', 'Cool', 'Archive']) param accessTier string = 'Hot' resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: storageAccountName location: location sku: { name: sku } kind: 'StorageV2' properties: { accessTier: accessTier supportsHttpsTrafficOnly: true minimumTlsVersion: 'TLS1_2' allowBlobPublicAccess: false networkAcls: { defaultAction: 'Deny' bypass: 'AzureServices' } } } output storageAccountId string = storageAccount.id output primaryBlobEndpoint string = storageAccount.properties.primaryEndpoints.blob
Consuming Modules
bicep// main.bicep param location string = resourceGroup().location param environment string = 'prod' module storage 'modules/storage.bicep' = { name: 'storage-deployment' params: { storageAccountName: 'myapp${environment}sa' location: location sku: environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS' } } module vnet 'modules/network.bicep' = { name: 'vnet-deployment' params: { location: location environment: environment } } // Reference module outputs output storageBlobEndpoint string = storage.outputs.primaryBlobEndpoint
ARM JSON Template Structure
json{ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "storageAccountName": { "type": "string", "metadata": { "description": "Name of the storage account" } }, "location": { "type": "string", "defaultValue": "[resourceGroup().location]" } }, "variables": { "storageSku": "Standard_LRS" }, "resources": [ { "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2023-01-01", "name": "[parameters('storageAccountName')]", "location": "[parameters('location')]", "sku": { "name": "[variables('storageSku')]" }, "kind": "StorageV2", "properties": { "supportsHttpsTrafficOnly": true, "minimumTlsVersion": "TLS1_2" } } ], "outputs": { "storageId": { "type": "string", "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" } } }
Deployment Commands
bash# Validate a Bicep template before deployment az deployment group validate \ --resource-group mygroup \ --template-file main.bicep \ --parameters environment='prod' baseName='myapp' # Preview changes with What-If az deployment group what-if \ --resource-group mygroup \ --template-file main.bicep \ --parameters environment='prod' baseName='myapp' # Deploy Bicep to resource group az deployment group create \ --resource-group mygroup \ --template-file main.bicep \ --parameters environment='prod' baseName='myapp' \ --name "deploy-$(date +%Y%m%d-%H%M%S)" # Deploy ARM JSON with parameter file az deployment group create \ --resource-group mygroup \ --template-file template.json \ --parameters @parameters.prod.json # Subscription-level deployment (e.g., resource groups, policies) az deployment sub create \ --location eastus \ --template-file subscription-level.bicep \ --parameters @params.json # Management group deployment az deployment mg create \ --management-group-id my-mg \ --location eastus \ --template-file mg-policy.bicep # Export resource group to ARM JSON az group export --name mygroup --output json > exported-template.json # Decompile ARM JSON to Bicep az bicep decompile --file exported-template.json # Build Bicep to ARM JSON (for inspection) az bicep build --file main.bicep --outfile main.json # List deployments and their status az deployment group list \ --resource-group mygroup \ --output table # Delete a failed deployment az deployment group delete \ --resource-group mygroup \ --name my-failed-deployment
Parameter Files
json// parameters.prod.json { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "environment": { "value": "prod" }, "baseName": { "value": "myapp" }, "adminPublicKey": { "reference": { "keyVault": { "id": "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vault}" }, "secretName": "ssh-public-key" } } } }
Linked and Nested Templates
bicep// Deploy to a different resource group module networkInSharedRg 'modules/network.bicep' = { name: 'shared-network' scope: resourceGroup('shared-networking-rg') params: { location: location } } // Conditional deployment param deployMonitoring bool = true module monitoring 'modules/monitoring.bicep' = if (deployMonitoring) { name: 'monitoring-deployment' params: { location: location } } // Loop deployment param storageAccounts array = [ { name: 'logs', sku: 'Standard_LRS' } { name: 'data', sku: 'Standard_GRS' } ] module storageLoop 'modules/storage.bicep' = [for account in storageAccounts: { name: 'storage-${account.name}' params: { storageAccountName: '${baseName}${account.name}sa' sku: account.sku location: location } }]
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
InvalidTemplate error | Syntax error in ARM JSON or Bicep | Run az bicep build to check for compile errors |
ResourceNotFound during deployment | Resource dependency not declared | Add dependsOn or use implicit references in Bicep |
DeploymentFailed with quota error | Subscription quota exceeded | Request quota increase or use a different region |
AuthorizationFailed | Insufficient RBAC permissions | Assign Contributor role on the target resource group |
| Parameter file secrets in source control | Secrets stored as plain text | Use Key Vault references in parameter files |
| Deployment takes very long | Large number of resources deployed serially | Use dependsOn carefully to allow parallel deployment |
What-If shows unexpected deletions | Complete mode instead of Incremental | Use --mode Incremental (the default) to avoid deleting unmanaged resources |
| Bicep module not found | Incorrect relative path | Verify path is relative to the consuming file |
Related Skills
terraform-azure-- Multi-cloud IaC alternative with broader provider support.azure-networking-- VNet, NSG, and firewall configurations referenced in templates.azure-vms-- Virtual machine sizing and configuration details.azure-aks-- Kubernetes cluster definitions for Bicep/ARM.


