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.
| Dimension | Bicep | Terraform / OpenTofu |
|---|---|---|
| Cloud scope | Azure only | Multi-cloud and SaaS via 4,000+ providers |
| Language | Bicep DSL (transpiles to ARM JSON) | HCL (for_each, expressions, functions) |
| State | Managed by Azure, no state file | Self-managed state file + locking backend |
| Plan preview | what-if operation | terraform plan |
| New Azure service support | Same-day, first-party | Days behind via the azurerm provider |
| Modularity | Modules + ACR registry, Template Specs | Modules + public registry |
| Rollback | ARM rolls back failed deployments | Manual / re-apply |
| Drift handling | Re-deploy reconciles | terraform 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 manualdependsOnarrays. - Run
az deployment group what-ifbefore every production deployment so you never apply blind. - Decorate parameters with
@description,@allowed, and@secureto 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.