Azure Bicep Explained: Why It Replaces ARM Templates
Azure·October 6, 2026·6 min read

Azure Bicep Explained: Why It Replaces ARM Templates

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

  1. Introduction to Bicep, to write and deploy one template.
  2. Expressions and parameters, to make it configurable.
  3. VNet and VM, to deal with real dependencies.
  4. Conditions, loops and what-if, for templates that adapt to their environment.
  5. 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.