Azure Bicep
Azure Bicep is a domain-specific language (DSL) for declaratively deploying Azure resources, designed as a concise, readable authoring layer over Azure Resource Manager (ARM) JSON templates (per the Microsoft “What is Bicep?” overview). You write a
.bicepfile describing the infrastructure you want; the Bicep CLI transpiles it into an ARM JSON template, and ARM — the deployment and management service for Azure — is the engine that actually creates, updates, and deletes the resources (ARM overview). Bicep exists because hand-writing ARM JSON is verbose and painful: bracketed[...]expression strings, mandatorydependsOnarrays, parameters and variables forced into rigid sections. Bicep is “a transparent abstraction over a Resource Manager JSON template that doesn’t lose the capabilities of a JSON template” — anything ARM can express, Bicep can too, in roughly half the lines. This note teaches the model: why Bicep exists, the transpile-to-ARM pipeline, modules, thewhat-ifpreview (the plan equivalent), deployment scopes, and how it contrasts with Terraform’sazurermprovider.
Mental Model
Bicep is authoring sugar; ARM is the machine. The mental split that unlocks everything: Bicep never talks to Azure. It compiles to ARM JSON, that JSON is submitted to Azure Resource Manager, and ARM does the real work — authenticating the request, ordering interdependent resources, calling each resource provider (Microsoft.Storage, Microsoft.Compute), and recording the deployment. State is not your concern at all: “Azure stores all states. No state or state files to manage.”
flowchart LR B[".bicep file<br/>concise DSL"] -->|bicep build /<br/>transpile| J["ARM JSON template<br/>(deploymentTemplate.json)"] J -->|az deployment create| ARM["Azure Resource Manager<br/>(the deployment ENGINE)"] ARM -->|authenticate + authorize| ARM ARM -->|dispatch| RP["Resource providers<br/>Microsoft.Storage · Microsoft.Compute · ..."] RP -->|create / update / delete| REAL["Real Azure resources"] ARM -.->|stores| STATE["Deployment history<br/>+ state (Azure-managed)"] style ARM fill:#e8f0ff style STATE fill:#e0ffe8
What it shows and the insight to take: Bicep occupies only the leftmost hop. Everything to the right — the engine, provider dispatch, orchestration, state — is ARM, and would be identical if you had written the JSON by hand. Learning Bicep is learning a nicer way to produce ARM’s input; learning how deployment actually behaves is learning ARM.
Why Bicep Exists — ARM JSON Is Painful
The overview page shows the same storage account in both languages. The ARM JSON needs a $schema, a contentVersion, parameters wrapped in typed objects, and string-embedded expressions like "[format('toylaunch{0}', uniqueString(resourceGroup().id))]". The Bicep is:
param location string = resourceGroup().location
param storageAccountName string = 'toylaunch${uniqueString(resourceGroup().id)}'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageAccountName
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
properties: { accessTier: 'Hot' }
}The concrete improvements (overview):
- No bracketed expressions. You call functions and read parameters/variables directly (
resourceGroup().location), not inside"[...]"strings. - Symbolic names + automatic dependencies. Each resource gets a symbolic name (
storageAccount); referencing one resource’s symbolic name from another creates the dependency implicitly, so you rarely writedependsOn. In ARM JSON you must hand-maintaindependsOnarrays. - Flexible structure. Parameters, variables, and outputs can appear anywhere in the file; ARM JSON forces them into fixed top-level sections.
- Day-zero resource coverage. Because Bicep resolves resource types and API versions against ARM directly, “as soon as a resource provider introduces new resource types and API versions, you can use them” — no waiting for a tool update. (This is the standard argument for Bicep over Terraform’s
azurermprovider, which can lag new Azure services.) - First-class tooling. The VS Code Bicep extension provides type safety, IntelliSense, and validation against the real resource schemas.
Bicep is deliberately not a general-purpose programming language: “A Bicep file declares Azure resources… without writing a sequence of programming commands.” It is declarative-over-imperative by design — you state desired state, ARM computes the ordering. Deployments are idempotent: deploy the same file repeatedly and you converge to the same resource state; if the declared properties already exist, no changes are made.
The Transpile-to-ARM Pipeline
The compilation step is the heart of Bicep’s design. bicep build main.bicep (or the transparent build that az deployment ... create --template-file main.bicep runs for you) emits an ARM JSON template that is what Azure actually receives. The pipeline runs both ways:
flowchart TD subgraph AUTHOR["Author time"] SRC["main.bicep"] -->|bicep build| ARMJSON["ARM template JSON"] ARMJSON -->|bicep decompile| SRC2["main.bicep (from existing JSON)"] end subgraph DEPLOY["Deploy time"] ARMJSON -->|submitted to| ARMENG["Azure Resource Manager"] ARMENG --> PROV["provisions resources"] end ARMJSON -.->|bicep snapshot / what-if| PREVIEW["preview changes<br/>(no deploy)"]
What it shows and the insight to take: the ARM JSON is a genuine, inspectable intermediate artifact — you can bicep build to see exactly what will be submitted, and bicep decompile to migrate an existing JSON template into Bicep. There is no hidden runtime: Bicep’s job finishes the moment the JSON is produced. This is why Bicep needs no separate state or provider protocol — it piggybacks entirely on ARM’s existing deployment machinery.
Azure Resource Manager — the Actual Deployment Engine
Because ARM does the real work, understanding it is understanding Bicep’s behavior (ARM overview). ARM is “the deployment and management service for Azure” — a consistent management layer through which all requests (portal, CLI, PowerShell, REST, SDKs) pass. When a request arrives, ARM:
- Authenticates and authorizes it (Azure role-based access control, Azure RBAC, is natively integrated) before forwarding to the appropriate service.
- Orchestrates interdependent resources — deploying them in the correct order and in parallel where possible, so “your deployments finish faster than serial deployments.”
- Dispatches to the relevant resource provider — “a service that supplies Azure resources,” e.g.
Microsoft.Computesupplies the virtual machine resource. - Resolves concurrent operations: if two requests update the same resource simultaneously, ARM lets one succeed and returns a
409error to the other — its built-in equivalent of state locking, handled for you.
Key ARM vocabulary you inherit when you write Bicep: a resource is a manageable item (VM, storage account, web app); a resource group is a container holding related resources that share a lifecycle; declarative syntax is the “here’s what I intend to create” style both ARM templates and Bicep use; an extension resource (like a role assignment) adds capability to another resource.
what-if — the Plan Equivalent
Before deploying, the what-if operation previews how resources will change without making any changes — the direct analogue of terraform plan and CloudFormation change sets (what-if guide). It “predicts the changes if the specified Bicep file is deployed,” checking current state against the file. Run it with az deployment group what-if (or -Whatif in PowerShell), and it prints a color-coded diff:
Resource and property changes are indicated with these symbols:
- Delete
+ Create
~ Modify
~ Microsoft.Network/virtualNetworks/vnet-001 [2025-01-01]
- tags.Owner: "Team A"
~ properties.addressSpace.addressPrefixes: [
- 0: "10.0.0.0/16"
+ 0: "10.0.0.0/15"
]
Resource changes: 1 to modify.
The operation classifies each resource into one of seven change types:
| Change type | Meaning |
|---|---|
| Create | Defined in the file, doesn’t exist yet — will be created. |
| Delete | Exists but not in the file — deleted only in complete mode (see below). |
| Ignore | Exists, not in the file, won’t be touched (also assigned when nested-template expansion limits are hit). |
| NoChange | Exists and defined; will be redeployed but no properties change (FullResourcePayloads result format). |
| Modify | Exists and defined; will be redeployed and properties will change. |
| Deploy | Exists and defined; will be redeployed, properties may or may not change (ResourceIdOnly format — insufficient info to tell). |
| NoEffect | A read-only property the service ignores. |
Two honesty caveats the docs stress. First, what-if can produce “noise” — false positives — because it “can’t resolve the reference function” and reports non-deterministic expressions (utcNow(), newGuid(), listKeys(), secure parameters, references to resources outside the template) as changes that may not actually change. Second, it expands nested templates only up to limits (500 nested templates, 800 resource groups, 5 minutes) before marking the rest Ignore. You can gate a real deployment on the preview with --confirm-with-what-if (-c), which shows the diff and prompts before executing.
Uncertain
Verify: whether Delete change type / complete-mode deletion applies to Bicep at all. Reason: the what-if doc states Delete “only applies when using complete mode for JSON template deployment”; Bicep-native deployments default to incremental mode and complete mode has resource-support caveats. To resolve: confirm current deployment-mode behavior for
.bicepfiles in the deployment-modes doc at time of use.#uncertain
Deployment Scopes
Unlike a flat “everything in one account” model, ARM (and therefore Bicep) deploys at four levels of scope, and a Bicep file declares which one it targets with targetScope (deploy-to-resource-group guide; ARM scope overview):
flowchart TD T["targetScope = 'tenant'<br/>whole Entra tenant"] --> MG["targetScope = 'managementGroup'<br/>governance grouping of subscriptions"] MG --> SUB["targetScope = 'subscription'<br/>billing + resource-group boundary"] SUB --> RG["targetScope = 'resourceGroup'<br/>(DEFAULT) — container of resources"] RG --> RES["individual resources<br/>storage, VMs, networks"] note["Lower scopes inherit settings<br/>(policy, RBAC) from higher ones"] T -.-> note
What it shows and the insight to take: scope is a containment hierarchy — management groups contain subscriptions, subscriptions contain resource groups, resource groups contain resources — and settings applied high (a policy on a subscription) flow down to everything beneath. A Bicep file is scoped to the resource group by default; you set targetScope = 'subscription' (or 'managementGroup' / 'tenant') to deploy things that live above a resource group, like creating the resource groups themselves, assigning policies, or provisioning management groups. All resource declarations in a file must deploy at the file’s scope — to cross into a different scope you use a module with its scope property (below). what-if is supported at all four scopes.
Modules
A module is simply a Bicep file that another Bicep file deploys, used to encapsulate and reuse a set of related resources (modules guide). Consuming one triggers a nested deployment under the hood — “Bicep modules are converted into a single ARM template with nested templates.” The syntax:
module stgModule '../storageAccount.bicep' = { // symbolic name + path
name: 'storageDeploy' // name of the nested deployment
scope: resourceGroup('demoRG') // optional — deploy to a different scope
params: { // inputs matching the module's params
storagePrefix: 'examplestg1'
}
}
output endpoint object = stgModule.outputs.storageEndpoint // read a module's outputLine by line: the symbolic name (stgModule) references the module elsewhere; the path can be a local .bicep/.json file, a private registry (br:myregistry.azurecr.io/...:v1), the public registry of Azure Verified Modules (br/public:avm/res/storage/storage-account:0.18.0), or a template spec (ts/...); the optional scope is precisely how you deploy a module to a different resource group, subscription, or management group than the parent file (using the resourceGroup(), subscription(), managementGroup(), tenant() scope functions). Modules deploy in parallel unless dependencies force ordering; @batchSize(n) on a [for ...] loop switches to serial batches. This is Bicep’s answer to Infrastructure Modules — the same “package resources behind an input/output interface” idea as a Terraform module, but realized as ARM nested deployments.
Bicep versus Terraform’s azurerm Provider
Both deploy to Azure declaratively, but the architecture differs sharply. Terraform reaches Azure through its azurerm provider (hashicorp/terraform-provider-azurerm) — a Go plugin that translates HCL into Azure REST calls — and manages its own terraform.tfstate (Terraform state docs). Bicep has no provider and no state; it rides ARM.
| Dimension | Azure Bicep | Terraform + azurerm provider |
|---|---|---|
| Cloud coverage | Azure-only (native) | Multi-cloud (azurerm is one of hundreds of providers) |
| Engine | Transpiles to ARM JSON; ARM deploys | Terraform core + azurerm provider call Azure REST directly |
| State | None to manage — Azure stores it | Self-managed terraform.tfstate; you configure a backend + locking |
| Plan / preview | what-if (ARM-native) | terraform plan |
| New-service coverage | Day-zero (resolves against ARM directly) | Can lag by weeks (provider must add the resource) |
| Language | Bicep DSL (declarative-only) | HCL |
| Concurrency safety | ARM resolves concurrent writes (409) | State locking (blob lease, etc.) that you set up |
| Reuse | Modules; Azure Verified Modules registry | Modules; Terraform Registry |
| Scope model | targetScope (RG/sub/MG/tenant) | Provider config + resource_group_name per resource |
| Cost | Free, open-source, Microsoft-supported | CLI free; HCP Terraform paid tiers |
The trade-off mirrors the CloudFormation-vs-Terraform one on the AWS side (see AWS CloudFormation): Bicep gives the tightest native Azure integration, zero state plumbing, and day-zero service coverage, but locks you to Azure. Terraform gives one workflow across every cloud and a huge ecosystem, at the cost of owning state and accepting provider lag for the newest Azure features. Teams that are Azure-only tend to pick Bicep; teams managing multiple clouds or with existing Terraform investment pick Terraform.
Failure Modes and Gotchas
what-ifnoise. Properties set toreference(),listKeys(),utcNow(),newGuid(), or secure parameters are reported as changing even when they won’t, because what-if can’t evaluate them outside a real deployment. Do not treat every~ Modifyas a genuine change — read the specific property.- Incremental vs complete mode. ARM deployments default to incremental mode (resources not in the template are left alone). Complete mode deletes resources absent from the template — powerful but dangerous, and the source of the
Deletechange type. Know which mode you are in. - No rollback. Unlike CloudFormation’s automatic rollback, a failed ARM deployment does not revert successfully-created resources; because deployments are idempotent you typically fix and redeploy, but partial state can linger.
- Module name collisions. Two deployments using the same module with the same static
nametargeting the same scope can corrupt each other’s outputs — leavenameoff (a GUID is generated) or make it unique. - Registry host allowlist. Since Bicep CLI v0.43.1, custom registry domains are blocked (error
BCP446); only*.azurecr.io,mcr.microsoft.com,ghcr.io, and a few others are permitted.
Uncertain
Verify: the current stable Bicep CLI version and any headline features as of mid-2026. Reason: the modules doc cites features at CLI v0.36.1 / v0.43.1 and Bicep is on a fast (roughly monthly) minor-release cadence; the newest version was not pinned to a release-notes page during this research. To resolve: check the Bicep GitHub releases.
#uncertain
See Also
- AWS CloudFormation — the AWS-native analogue; the same vendor-native-vs-Terraform contrast (CloudFormation → change sets/rollback; Bicep → what-if/ARM)
- Terraform — the multi-cloud incumbent whose
azurermprovider is Bicep’s main competitor on Azure - Infrastructure Modules — the generic module concept Bicep modules implement as ARM nested deployments
- Plan Apply and Destroy — the generic preview-then-apply lifecycle
what-if+ deployment implement - Infrastructure State Files — what state stores; ARM manages it for Bicep, Terraform does not
- Declarative vs Imperative Configuration — the declarative-over-imperative design Bicep is built on
- Cloud Development Kit — the imperative-language-to-declarative-template pattern (CDK), a different point in the same design space
- Infrastructure as Code MOC — parent map (§5 Tools Landscape)
- Cloud Architecture MOC — the Azure primitives (VMs, storage, VNets, RBAC) Bicep provisions