2026-01-27 17:35:45 -05:00
---
name : arm-templates
description : Deploy Azure resources with ARM templates and Bicep. Create modular deployments and manage dependencies. Use when deploying Azure-native IaC.
license : MIT
metadata :
author : devops-skills
version : "1.0"
---
# ARM Templates & Bicep
2026-03-24 18:02:50 -04:00
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.
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
## 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
2026-01-27 17:35:45 -05:00
```bicep
2026-03-24 18:02:50 -04:00
// 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
2026-01-27 17:35:45 -05:00
param location string = resourceGroup (). location
param vmName string
2026-03-24 18:02:50 -04:00
param subnetId string
param adminUsername string = 'azureuser'
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
@ 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' = {
2026-01-27 17:35:45 -05:00
name : vmName
location : location
2026-03-24 18:02:50 -04:00
identity : {
type : 'SystemAssigned'
}
2026-01-27 17:35:45 -05:00
properties : {
hardwareProfile : {
vmSize : 'Standard_B2s'
}
osProfile : {
computerName : vmName
2026-03-24 18:02:50 -04:00
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
}
2026-01-27 17:35:45 -05:00
}
}
}
2026-03-24 18:02:50 -04:00
output vmPrincipalId string = vm . identity . principalId
2026-01-27 17:35:45 -05:00
output vmId string = vm . id
```
2026-03-24 18:02:50 -04:00
## 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
2026-01-27 17:35:45 -05:00
```bash
2026-03-24 18:02:50 -04:00
# 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
2026-01-27 17:35:45 -05:00
az deployment group create \
--resource-group mygroup \
--template-file main.bicep \
2026-03-24 18:02:50 -04:00
--parameters environment = 'prod' baseName = 'myapp' \
--name "deploy- $( date +%Y%m%d-%H%M%S) "
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
# Deploy ARM JSON with parameter file
2026-01-27 17:35:45 -05:00
az deployment group create \
--resource-group mygroup \
--template-file template.json \
2026-03-24 18:02:50 -04:00
--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
2026-01-27 17:35:45 -05:00
```
2026-03-24 18:02:50 -04:00
## Parameter Files
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
```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.