09 · ARM Templates / Bicep Intro¶
Every command you've run in this course so far (az group create,
az vm create, ...) is imperative: you tell Azure the steps to take.
Infrastructure as Code (IaC) flips that around — you write a
declarative description of what you want to exist, and a tool
reconciles reality to match it, safely re-runnable any number of times.
Azure's native IaC formats are ARM templates (JSON) and Bicep (a
cleaner syntax that compiles down to ARM JSON) — this module focuses on
Bicep, since it's what Microsoft now recommends for all new work.
ARM templates vs. Bicep¶
An ARM template is a JSON document describing one or more resources,
their properties, and how they depend on each other. It works, but JSON is
verbose and unforgiving to hand-write — lots of brackets, no comments in
strict JSON, and expressions like [resourceId(...)] embedded as strings.
Bicep is a domain-specific language that compiles 1:1 to ARM JSON, but reads like a typed configuration file: no brackets-as-strings, real comments, module reuse, and editor tooling with autocomplete. Nobody should be hand-writing new ARM JSON in 2026 — write Bicep and let the tooling generate JSON only where something downstream still needs it (e.g. a CI system that only accepts raw ARM).
Install Bicep¶
The az CLI can manage the Bicep compiler for you:
Your first Bicep file¶
main.bicep:
@description('Name of the storage account (must be globally unique)')
param storageAccountName string = 'stbiceplearn${uniqueString(resourceGroup().id)}'
@description('Azure region for all resources')
param location string = resourceGroup().location
@allowed([
'Standard_LRS'
'Standard_ZRS'
'Standard_GRS'
])
param storageSku string = 'Standard_LRS'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: storageSku
}
kind: 'StorageV2'
}
output storageAccountId string = storageAccount.id
output primaryEndpoint string = storageAccount.properties.primaryEndpoints.blob
Key things to notice, compared to the raw az storage account create from
Module 04:
paramdeclares an input with an optional default and validation (@allowed) — no more remembering valid SKU strings from memory.resource <symbolicName> '<type>@<api-version>' = { ... }declares a resource. The symbolic name (storageAccount) is how you refer to it elsewhere in the same file — it's not the Azure resource name.uniqueString(resourceGroup().id)deterministically generates a short unique suffix so the same template is re-runnable without a name clash.outputexposes values (like the resulting endpoint URL) back to whoever deployed the template, or to a calling module.
Deploy a Bicep file¶
az group create --name rg-bicep-demo --location eastus
az deployment group create \
--resource-group rg-bicep-demo \
--template-file main.bicep \
--parameters storageSku=Standard_LRS
Behind the scenes this compiles main.bicep to ARM JSON and submits it to
Azure Resource Manager, which computes a deployment plan and creates/updates
resources to match. Run the exact same command again with no changes to the
file, and Azure reports "no changes" — this idempotency is the core
value of declarative IaC over imperative scripts.
Preview changes before applying (what-if)¶
az deployment group what-if \
--resource-group rg-bicep-demo \
--template-file main.bicep \
--parameters storageSku=Standard_GRS
what-if shows a diff (create/modify/delete) without actually changing
anything — always run this before applying a template to production-like
resources.
A multi-resource example¶
Bicep resources can reference each other by symbolic name, and Azure Resource Manager figures out the dependency order automatically:
param location string = resourceGroup().location
param vnetName string = 'vnet-bicep-demo'
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: vnetName
location: location
properties: {
addressSpace: {
addressPrefixes: ['10.10.0.0/16']
}
subnets: [
{
name: 'subnet-app'
properties: {
addressPrefix: '10.10.1.0/24'
}
}
]
}
}
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-09-01' = {
name: 'nsg-bicep-demo'
location: location
properties: {
securityRules: [
{
name: 'allow-https'
properties: {
priority: 200
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
// Referencing vnet.name / vnet.properties creates an implicit dependency --
// this subnet update is guaranteed to run after the VNet exists.
resource subnetNsg 'Microsoft.Network/virtualNetworks/subnets@2023-09-01' = {
parent: vnet
name: 'subnet-app'
properties: {
addressPrefix: '10.10.1.0/24'
networkSecurityGroup: {
id: nsg.id
}
}
}
parent: vnet (instead of manually building the nested resource name
string) is Bicep's way of expressing "this subnet belongs to that VNet" —
one of the ergonomic wins over hand-written ARM JSON.
Converting between ARM and Bicep¶
# Decompile an existing ARM JSON template to Bicep
az bicep decompile --file azuredeploy.json
# Compile Bicep down to ARM JSON (e.g. for a pipeline that only accepts JSON)
az bicep build --file main.bicep
az bicep decompile is a great way to learn Bicep syntax from templates
you export from the Portal (every resource's blade has an "Export template"
option that gives you its current ARM JSON).
How It Actually Works¶
Bicep isn't a separate deployment engine — it's a domain-specific language
that compiles down to ARM JSON (az deployment group create on a .bicep
file first transpiles it to the equivalent ARM template in memory, then
submits that JSON to the exact same ARM deployment API used by hand-written
templates or the Portal's "Export template" feature). ARM itself processes
a deployment as a declarative graph: it parses the template's
resources array, builds a dependency graph from explicit dependsOn
entries and implicit references (reference()/symbolic references pulled
from one resource's properties into another's), topologically sorts that
graph, and then provisions resources in parallel wherever the graph allows
it — this is why two unrelated resources in the same template deploy
concurrently while a NIC correctly waits for its VNet/subnet to exist first,
without you writing any explicit ordering logic.
Idempotency is enforced by ARM computing a diff against the resource's
last-known desired state (tracked per deployment in the resource group's
deployment history) rather than blindly re-running create calls — re-
deploying an unchanged template is a no-op at the resource-provider level
because ARM detects no property delta and skips the PUT. What-if
(az deployment group what-if) works by having ARM run this same diff
engine against your proposed template without actually calling any
resource provider's write API, which is how it can report additions/
modifications/deletions before you commit. Because Bicep compiles to plain
ARM JSON, there's no runtime dependency on Bicep at deployment time — the
CLI's compile step is purely a client-side convenience, and the resource
group's deployment history only ever stores the resulting ARM template, not
your original Bicep source.
Cheat sheet¶
| Command / syntax | Purpose |
|---|---|
az bicep install / upgrade |
Install or update the Bicep compiler. |
param <name> <type> = <default> |
Declare an input parameter. |
resource <symbolicName> '<type>@<api>' = {...} |
Declare a resource. |
output <name> <type> = <expr> |
Expose a value after deployment. |
az deployment group create --template-file |
Deploy a Bicep/ARM file to a resource group. |
az deployment group what-if --template-file |
Preview changes before applying. |
az bicep build / decompile |
Convert Bicep → ARM JSON, or JSON → Bicep. |
Exercise¶
- Write a Bicep file that declares a storage account (parameterized name
and SKU) and a blob container inside it
(
Microsoft.Storage/storageAccounts/blobServices/containers). - Run
az deployment group what-ifagainst a new resource group to preview it, thenaz deployment group createto actually deploy it. - Re-run the same
createcommand unchanged and confirm Azure reports no changes needed — that's idempotency in action. - Change the SKU parameter and re-deploy, using
what-iffirst to see exactly what will change. - Delete the resource group when finished.