Skip to content
Infrastructure as Code iac tools 4 min read

Azure Bicep

Azure Bicep is Microsoft’s first-party domain-specific language for declaring Azure resources. It is a thin, human-friendly layer over ARM (Azure Resource Manager) JSON templates: every Bicep file transpiles to ARM JSON, which is what Azure actually executes. The point of Bicep is to keep the AWS-CloudFormation-style benefit of native, state-free deployments while shedding the notorious verbosity and brittleness of hand-written ARM. If your footprint is exclusively Azure, Bicep gives you same-day support for every service with no external tooling; if you span multiple clouds or want a large module registry, Terraform usually wins.

Why Bicep instead of raw ARM

ARM templates are JSON documents that work, but they are painful to author: no comments, string-concatenation functions for everything, deeply nested dependsOn arrays, and parameter blocks that dwarf the actual resources. Bicep compiles down to that same JSON but exposes a clean declarative syntax with type checking, automatic dependency inference, and editor IntelliSense.

The transpilation is fully transparent — you can convert in either direction:

# Compile Bicep to the ARM JSON Azure runs
az bicep build --file main.bicep

# Reverse-engineer an existing ARM template into Bicep
az bicep decompile --file template.json

Bicep is not a separate runtime. There is no Bicep “state” — the deployment, idempotency, and rollback all happen inside Azure Resource Manager exactly as they do for hand-written ARM. Bicep is purely an authoring DSL.

A small example

This declares a storage account and outputs its primary blob endpoint. Note that referencing storage.id later would automatically create the dependency — no explicit dependsOn needed.

@description('Globally unique storage account name')
param storageName string = 'acmelogs${uniqueString(resourceGroup().id)}'

@allowed(['Standard_LRS', 'Standard_GRS'])
param sku string = 'Standard_LRS'

resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageName
  location: resourceGroup().location
  sku: { name: sku }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
  }
  tags: { environment: 'prod', managedBy: 'bicep' }
}

output blobEndpoint string = storage.properties.primaryEndpoints.blob

You deploy it directly with the Azure CLI — no separate compile step is required, the CLI builds it for you:

az deployment group create \
  --resource-group acme-prod \
  --template-file main.bicep \
  --parameters sku=Standard_GRS

Output:

{
  "properties": {
    "provisioningState": "Succeeded",
    "outputs": {
      "blobEndpoint": {
        "type": "String",
        "value": "https://acmelogsq7x2k.blob.core.windows.net/"
      }
    }
  }
}

Modules

Bicep modules are the unit of reuse, equivalent to Terraform modules. A module is just another .bicep file consumed via the module keyword; you pass parameters in and read outputs back out. Modules can come from a local path, a registry, or a Template Spec.

module network './modules/vnet.bicep' = {
  name: 'core-network'
  params: {
    addressPrefix: '10.0.0.0/16'
    location: resourceGroup().location
  }
}

// Consume a module output — this creates the dependency implicitly
output subnetId string = network.outputs.subnetId

Modules can also be published to an Azure Container Registry and versioned, giving you a private registry workflow:

az bicep publish --file vnet.bicep \
  --target br:acme.azurecr.io/bicep/modules/vnet:1.2.0

Bicep vs Terraform on Azure

Both are mature and free (you pay only for resources). The decision turns on cloud breadth, state ownership, and ecosystem.

DimensionBicepTerraform / OpenTofu
Cloud scopeAzure onlyMulti-cloud and SaaS via 4,000+ providers
LanguageBicep DSL (transpiles to ARM JSON)HCL (for_each, expressions, functions)
StateManaged by Azure, no state fileSelf-managed state file + locking backend
Plan previewwhat-if operationterraform plan
New Azure service supportSame-day, first-partyDays behind via the azurerm provider
ModularityModules + ACR registry, Template SpecsModules + public registry
RollbackARM rolls back failed deploymentsManual / re-apply
Drift handlingRe-deploy reconcilesterraform plan detects drift

Bicep’s answer to terraform plan is the what-if flag, which previews adds, modifications, and deletes before you commit:

az deployment group what-if \
  --resource-group acme-prod \
  --template-file main.bicep

A practical pattern: teams standardizing on Azure-only platforms often pick Bicep for its zero-config state and instant service coverage, while teams running Azure alongside AWS or GCP standardize on Terraform (or its open-source fork OpenTofu) to keep one workflow across every provider.

Best Practices

  • Let dependencies be inferred — reference a resource’s symbolic name (storage.id) instead of writing manual dependsOn arrays.
  • Run az deployment group what-if before every production deployment so you never apply blind.
  • Decorate parameters with @description, @allowed, and @secure to get validation and self-documenting templates.
  • Factor environments out with parameter files (main.dev.bicepparam, main.prod.bicepparam) rather than copy-pasting templates.
  • Publish shared modules to an ACR registry with semantic version tags so consumers pin a known-good version.
  • Commit the source .bicep, not the generated ARM JSON — treat the JSON as a build artifact.
  • If you need multi-cloud, richer logic, or a large public module ecosystem, evaluate Terraform/OpenTofu before committing to Bicep.
Last updated June 14, 2026
Was this helpful?