
ARM templates worked. Nobody enjoyed writing them.
Thousands of lines of JSON, deeply nested, no comments allowed, and error messages pointing at line 847 of a file you could not read without a map. Bicep is Microsoft's answer: a language that compiles down to ARM but reads like something a person would choose to write.
If you are still authoring raw ARM JSON, this is the upgrade worth making.
The same resource, both ways
A storage account in ARM JSON:
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[parameters('storageAccountName')]",
"location": "[parameters('location')]",
"sku": {
"name": "[parameters('skuName')]"
},
"kind": "StorageV2",
"properties": {}
}
The same thing in Bicep:
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: skuName
}
kind: 'StorageV2'
}
No square bracket expressions, no quoting every key, no nesting for its own sake. On one resource the saving is mild. At two hundred resources it is the difference between a file you can review and a file you skim and hope about.
Bicep compiles to ARM
Bicep does not replace ARM, it transpiles to it. Every Bicep construct has an ARM equivalent, and the deployment engine underneath is identical. You are changing the authoring experience, not the platform.
What actually improves
Parameters with types and validation
ARM declares parameters in a separate block and refers to them through [parameters('name')]. Bicep declares them inline, with types and decorators.
@description('The name of the storage account')
@minLength(3)
@maxLength(24)
param storageAccountName string
@description('The Azure region for deployment')
param location string = resourceGroup().location
var uniqueSuffix = uniqueString(resourceGroup().id)
Decorators replace the verbose JSON schema validation ARM required, and defaults are a plain = rather than a nested property. The practical effect is that bad input fails at validation time with a readable message rather than halfway through a deployment.
Parameters, variables, outputs and the built in functions are the foundation everything else sits on. Work through Bicep expressions and parameters before moving on.
Modules, and automatic dependency ordering
Modules break a large deployment into composable pieces with typed inputs and outputs. Networking in one file, compute in another.
module network 'modules/network.bicep' = {
name: 'networkDeployment'
params: {
vnetName: 'prod-vnet'
addressPrefix: '10.0.0.0/16'
subnetPrefix: '10.0.1.0/24'
}
}
module vm 'modules/compute.bicep' = {
name: 'vmDeployment'
params: {
subnetId: network.outputs.subnetId
vmName: 'prod-web-01'
}
}
The compute module consumes the network module's output, and that reference is all Bicep needs to work out the ordering. No hand maintained dependsOn arrays, which in ARM were a reliable source of deployments that succeeded in the wrong order.
Conditions and loops that read like code
Deploy something only in production:
param isProd bool
resource diagStorage 'Microsoft.Storage/storageAccounts@2023-01-01' = if (isProd) {
name: 'diagstorage${uniqueSuffix}'
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}
Loops are similarly direct. Note the index form, which is what you want whenever position matters:
param subnetNames array = ['web', 'app', 'data']
resource subnets 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = [for (name, i) in subnetNames: {
name: name
properties: {
addressPrefix: '10.0.${i}.0/24'
}
}]
Taking the index directly from the loop is both clearer and safer than searching the array for the current item, which quietly breaks the moment two entries are the same.
Run what-if before every real deployment
What-if executes the template in preview and reports exactly what would be created, changed or deleted. It costs seconds and it is the only thing standing between a refactor and an unintended deletion.
Conditions, loops and what-if are where templates stop being static. Practise all three together in a real subscription.
Starting from nothing
If you have never written Bicep, begin with the tooling and one small template. Introduction to Azure Bicep covers installation, authoring and deploying with the CLI.
Once that works, build something with more than one moving part. Deploying a virtual network and VM with Bicep adds NSG rules and a public IP, which is where resource dependencies start doing real work for you.
Deployment stacks, the part people miss
A deployment stack manages a set of resources as one unit with a tracked lifecycle. It knows which resources it created.
That matters because of a gap in ordinary template deployments. Remove a resource from your Bicep file and redeploy, and the resource stays exactly where it is. The template no longer describes it, and nothing deletes it. Over a year of refactoring, that is how subscriptions accumulate things nobody can account for.
A stack closes the gap by cleaning up what it owns. Deployment stacks for resource lifecycle management covers the model and its sharp edges.
Stacks delete things on purpose
Removing a resource from a stack's template and updating the stack deletes that resource from your subscription. That is the intended behaviour and it is why refactoring a stack template deserves a what-if run first, every time.
Where Bicep shows up in AZ-104
Templates fall under the compute deployment domain, which carries roughly a fifth to a quarter of the exam. You are expected to read an existing template, modify it, deploy from it, and export an existing deployment as one.
Reading is the skill that gets underpractised. Most people write a few templates and never work through someone else's, which is precisely what the exam asks for.
Our AZ-104 practice exam guide sets out a study plan that includes IaC, and the per domain scoring will show you where template work sits relative to your other domains.
A sensible order to learn this in
- Introduction to Bicep, to write and deploy one template.
- Expressions and parameters, to make it configurable.
- VNet and VM, to deal with real dependencies.
- Conditions, loops and what-if, for templates that adapt to their environment.
- Deployment stacks, for lifecycle ownership.
Each one assumes the previous. By the end you will be writing templates with modules, conditionals and managed lifecycles, which is the point at which IaC starts saving more time than it costs.
If you also work outside Azure, our Terraform on AWS guide covers the same ideas in the cross cloud tool, and the concepts map over almost directly.
Ready to Master Cloud Engineering?
Get access to hands-on labs, expert-led courses, and a supportive community.
Practice it hands-on
Labs where you can apply what this article covers, in a real environment.
Deploy a Full Azure Environment Using Bicep Infrastructure as Code
Write modular Bicep templates to deploy a VNet, App Service, SQL Database, and Key Vault, then deploy the full environment using Azure CLI.
cloudlearn.ioStart labAzure Deployment Stacks with Bicep for Resource Lifecycle Management
Use Azure Deployment Stacks to manage resource lifecycle as a single unit with detach and delete policies, state inspection, and stack cleanup.
cloudlearn.ioStart labAzure Bicep Conditions Loops and What-If Deployments
Use Bicep conditional expressions, for-loops, and what-if analysis to dynamically deploy and preview Azure infrastructure changes.
cloudlearn.ioStart lab




