Azure Bicep: What it is and when to use it over ARM/Terraform

Back view of modern programmer sitting and writing code in dark room-4

Teams don't struggle with Bicep because the syntax is hard, they struggle because they adopt it without understanding what it actually replaces: a transpilation layer over ARM, not a new deployment engine. That distinction determines whether Bicep saves you from Terraform's state-file overhead or just adds another abstraction to debug.

For a CTO deciding whether to migrate an existing Azure estate, the real questions are about deployment scopes, module reuse, and what breaks during ARM-to-Bicep conversion. Here's what actually happens when you make that switch.

Azure Bicep in brief

Azure Bicep is Microsoft's concise, declarative authoring language for Azure resources, and every Bicep file compiles down to a standard Azure Resource Manager template before deployment reaches the control plane. The Bicep CLI handles that transpilation step, so deployment scopes, resource providers, and idempotent behavior stay identical to raw ARM JSON - only the authoring syntax changes.

Our team has migrated more than a dozen Azure estates from ARM JSON to Bicep, cutting pipeline run times and surfacing real transpilation errors along the way. Azure/bicep has progressed from v0.3.1 (2020) to v0.43.x (2025), reflecting frequent, near-weekly stable releases (GitHub - Azure/bicep Releases page, 2025).

This piece covers what that transpilation model means in practice, and when the trade pays off against ARM templates or Terraform.

What is Azure Bicep and how does it map to ARM?

Bicep is a domain-specific language that compiles, or transpiles, into standard Azure Resource Manager templates before anything reaches the control plane. Nothing about deployment behavior changes: the same idempotency guarantees, the same resource providers, the same deployment scopes apply whether Azure Resource Manager receives hand-written JSON or Bicep-generated JSON. Infrastructure as Code on Azure has always meant ARM underneath; Bicep just replaces the authoring layer.

The mapping is mechanical. Every Bicep resource block maps to a resources entry in the compiled template, carrying the same apiVersion, name, location, and property schema you'd write by hand in ARM JSON. Parameters and outputs follow the same one-to-one translation.

Run az bicep build on any.bicep file and you can inspect the exact ARM JSON that Azure Resource Manager will execute, which is the fastest way to see the difference between concise Bicep authoring and the JSON it produces.

Deployment scopes work identically too: resource group, subscription, management group, or tenant, set with the same targetScope semantics ARM has always supported.

The Azure/bicep GitHub repository shows Bicep shipping new CLI releases roughly every two to four weeks, a cadence that outpaces ARM template schema updates. Use the compiled JSON when you need to ask Resource Manager's REST API directly what changed.

Why choose Bicep for Azure infrastructure?

As an Infrastructure as Code language, Bicep favors concise authoring over verbose ARM templates written in JSON, cutting boilerplate without losing native control-plane integration. A resource block that names each property, location, and parameters often collapses to three or four lines in a Bicep file, and the compiler catches a missing location or an undeclared parameter before you deploy.

The difference shows up first in state handling, and it's the real split from Terraform. Terraform tracks infrastructure state in a separate state file and reconciles drift against it; Bicep needs none, since it re-derives state directly from Azure Resource Manager on every run.

RBAC follows the same logic. These deployments use native Azure Resource Manager deployment scopes (resource group, subscription, management group, and tenant), so the service principal running a deployment needs only RBAC roles, not separate state-store permissions. That matters once you're managing hundreds of declarative resources across shared scopes.

Migration is incremental: az bicep decompile converts an existing template into a starting point, and shows what that shift does to deployment time and code review effort.

Ask what your current ARM review cycle costs before ruling out the switch, and see the ARM REST API docs and Microsoft Learn's what-if documentation for the exact output format.

Installing Bicep: Azure CLI and Azure PowerShell

The Bicep CLI installs in under two minutes through either Azure CLI or Azure PowerShell, and both paths ship the same compiler binary that transpiles Bicep files into ARM JSON templates. Azure CLI has bundled Bicep since version 2.20.0, according to Microsoft Learn's Bicep install guide, so az bicep install followed by az bicep upgrade covers most setups without a separate download.

Azure PowerShell users run Install-Module -Name Az.Resources and let PowerShell resolve the Bicep dependency automatically on first New-AzResourceGroupDeployment call. Verify either path with az bicep version or bicep --version before deploying anything to a resource group, since a stale compiler silently misreports parameter validation errors.

Add the VS Code Bicep extension next. It gives you resource autocompletion against the live Azure Resource Manager schema, inline hover for API versions, and real-time linting that catches an undeclared parameter before you save the file.

For a quick syntax check without a local install, the Bicep Playground compiles snippets to ARM JSON in the browser, which is where we point junior engineers before they touch a real subscription.

Bicep vs ARM templates: Key differences

Bicep and Azure Resource Manager templates deploy the same resources through the same Resource Manager API, but Bicep removes the JSON authoring overhead: no bracket matching, no nested dependsOn chains, and markedly less boilerplate for an equivalent template.

Both compile to the identical ARM JSON payload at deployment time, so the control-plane behavior, RBAC checks, and deployment scopes (resource group, subscription, management group, tenant) are unchanged.

Bicep ARM JSON templates
Syntax Declarative DSL Raw JSON
Modularity Native modules, no copy-paste Nested/linked templates
Tooling VS Code extension, type validation Manual schema lookup
State management None, reads live Azure state None, reads live Azure state
Migration path az bicep decompile from JSON N/A

Neither Bicep nor ARM templates track state in a file the way Terraform does; each deployment resolves against the live resource graph, which avoids drift-file corruption but means large deployments still take longer to plan than a cached Terraform state read.

Teams migrating legacy ARM estates typically underestimate the decompile cleanup effort.

Bicep vs Terraform: Which fits your Azure stack?

Infrastructure as Code choice on Azure usually reduces to one question: is your footprint Azure-only or multi-cloud? Bicep is the practical default for Azure-only stacks; Terraform earns its complexity once AWS or GCP resources join the same pipeline.

If you're building Azure-based applications and want to make the most of the platform's capabilities, choosing the right Infrastructure as Code tool is just one piece of the puzzle. Beyond that choice, the real technical divide is state management.

Bicep is stateless: every deployment file queries Azure Resource Manager for current resource state and computes changes at run time, no drift reconciliation, no backend to secure. Terraform keeps its own state file, which means a remote backend, locking, and periodic terraform plan runs to catch drift manually.

Syntax is where the difference gets concrete.

Bicep compiles down to the same JSON template that Resource Manager consumes, but a typical storage account declaration drops from around 30 lines of ARM JSON to roughly 10 lines of Bicep, with strong types on parameters and outputs built in.

Terraform's HCL reads almost as clean, but you select a provider block explicitly and pin version constraints yourself, whereas Bicep resolves API versions against the azure services you reference automatically.

Microsoft's Azure quickstart templates repository now ships Bicep-first, so teams starting from a working quickstart inherit the shorter syntax by default rather than retrofitting it later.

Deployment scopes matter too. Bicep maps natively to Resource Manager's four scopes: resource group, subscription, management group, and tenant, using one service principal and RBAC model across a single file. Terraform's azurerm provider reaches the same scopes but needs separate provider blocks and role assignments per scope, adding authoring overhead on large estates.

Without a state file to reconcile, Bicep deployments also tend to run a few seconds faster per incremental change, a margin that compounds across pipelines managing dozens of stacks a day.

Migrating from ARM JSON, az bicep decompile gives a usable first pass, though on our own ARM-to-Bicep engagements the output regularly left stale apiVersion values and broken copy loops needing manual cleanup before the template deployed cleanly.

According to HashiCorp's State of Cloud Strategy Survey, most enterprises run infrastructure across more than one cloud provider, the exact case where Terraform's provider model earns its keep over Bicep.

For Azure-only teams weighing a migration path, Netguru's Azure engineering practice has run both directions depending on the client's cloud footprint and existing pipeline maturity. If you're planning a broader cloud strategy beyond a single provider, it's worth mapping out migration priorities before committing to either tool.

What Bicep syntax looks like: Params, variables, decorators

Bicep drops the JSON brackets but keeps every semantic guarantee ARM templates give you: idempotent resource declarations, the same resource manager API versions, the same deployment scopes underneath.

Here's a resource block with parameters and variables, a decorator, and a file load:

@description('Storage account name, must be globally unique')
@minLength(3)
param storageAccountName string

@allowed(['eastus', 'westeurope'])
param location string = resourceGroup.location

var skuName = 'Standard_LRS'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
 name: storageAccountName
 location: location
 sku: { name: skuName }
 kind: 'StorageV2'
 properties: {
 customDomain: {
 name: loadTextContent('domain-config.txt')
 }
 }
}

Decorators (@description, @allowed, @secure, @minLength) replace ARM's verbose metadata and allowedValues blocks, and they surface directly in IntelliSense, which is where most of the authoring time savings actually come from. The loadTextContent function pulls static content, cloud-init scripts, JSON policy files, straight into a property at compile time, something ARM templates handle through ugly nested concat and base64 calls.

Running this same resource declaration twice produces no drift. Bicep resolves the current state from Azure Resource Manager on every deploy rather than tracking it in a separate file, which is the core operational difference from Terraform's state file model covered above. If you want the full decorator reference, Microsoft Learn's Bicep file documentation lists every one supported in the current Bicep CLI release.

What-if deployment: Previewing changes before you apply

What-if deployment asks Azure Resource Manager to simulate a deployment against live resources and return a diff before anything actually changes.

Run az deployment group what-if --template-file main.bicep --parameters params.json and Resource Manager evaluates the Bicep file, resolves resource dependencies, and prints a color-coded change set: Create, Modify, Delete, NoChange, or Ignore per resource.

The useful difference from terraform plan is where the comparison happens. Terraform diffs your template against a state file it owns. Bicep's what-if queries the actual control plane at Azure Resource Manager, so there's no state file to drift or corrupt.

What-if respects deployment scopes, resource group, subscription, or management group, and needs its own RBAC action, Microsoft.Resources/deployments/whatIf/action, separate from deploy permissions. We set that up as a read-only gate in CI so a service principal can preview changes without authority to apply them. See the Microsoft Learn what-if reference for the full operation grouping and result-format schema.

Bicep modules and the Bicep registry

Bicep modules package a set of resources into a reusable file, then get referenced from a parent Bicep file with a single module block. You pass parameters in and read outputs back out, the same way you'd reference any other resource.

A typical module wraps a resource group's networking layer, or an App Service plan plus its diagnostic settings. The resource definitions get written once and called by name across every environment.

Here's what that looks like in practice:

module network 'br:myregistry.azurecr.io/modules/network:v2' = {
  name: 'networkDeployment'
  params: {
    vnetName: 'prod-vnet'
    addressPrefix: '10.0.0.0/16'
  }
}

The br: prefix tells Bicep to pull from a registry rather than a local file, and the module's params block only accepts the types the module author defined, so a bad input fails at compile time instead of mid-deployment.

The Bicep Registry stores versioned modules that teams pull by reference instead of copying template files between repos. According to Microsoft Learn's Bicep Registry, Microsoft also publishes a public registry of verified modules covering common patterns like storage accounts, key vaults, and virtual networks, hosted on the Microsoft Container Registry at no cost to consumers.

For teams still evaluating whether Bicep fits, the Azure Quickstart Templates repo is a faster way to ask "how do I provision this?" than starting from a blank JSON template, since most common Azure services already have a working quickstart to adapt.

Standing up a private registry means provisioning an Azure Container Registry, wiring RBAC for who can select and publish modules, and holding a versioning discipline across teams. That overhead pays off once a team reuses more than a handful of modules across projects; below that, public modules plus a shared repo win.

Deploying Bicep files in Azure DevOps

Azure DevOps deploys Bicep files the same way it deploys ARM JSON: through an AzureResourceManagerTemplateDeployment@3 task, or an inline az deployment group create step that calls the Bicep CLI directly. The CLI handles transpilation to JSON at deploy time, so the pipeline never checks in a compiled template.

Pick the deployment scope before you write the pipeline stage. Resource group scope covers most application workloads. Subscription scope suits shared networking or policy assignments. Management group scope handles governance rolled out across multiple subscriptions. Each scope needs a service principal with matching RBAC, and mismatched scope-to-permission mapping is the most common pipeline failure we see during ARM-to-Bicep migrations.

A what-if stage ahead of the deploy stage catches drift before it ships. Run az deployment group what-if against the target resource group and gate the pipeline on manual approval if it flags an unexpected delete.

One detail worth checking early: Bicep CLI version pinning. Pipelines running an outdated CLI silently skip newer language features, which shows up as a template parse error with no clear file reference.

Hands-on: Multi-app deployment with Azure policy

A multi-app deployment with a shared Azure Policy assignment is the clearest case for Bicep modules: one main.bicep file orchestrates deployment scopes across subscription and resource group, while child modules stay scoped to a single app.

Set targetScope = 'subscription' in the entry file. From there, deploy a Microsoft.Authorization/policyAssignments resource once, and loop resource group creation with a module block per app:

targetScope = 'subscription'

resource policy 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
 name: 'require-tag-costcenter'
 properties: {
 policyDefinitionId: subscriptionResourceId('Microsoft.Authorization/policyDefinitions', 'require-tag')
 }
}

module apps 'app.bicep' = [for app in appNames: {
 name: 'deploy-${app}'
 scope: resourceGroup('rg-${app}')
 params: {
 location: location
 name: app
 }
}]

Each app.bicep module stays at resource group scope and only ever sees the parameters passed down. That separation matters for RBAC. The service principal running the pipeline needs Resource Policy Contributor at subscription scope but only Contributor on individual resource groups, so a compromised app-level credential can't touch the policy assignment.

On one ARM-to-Bicep migration, the loop-based module pattern replaced eleven near-duplicate ARM JSON templates that had drifted out of sync over two years. What-if against the consolidated Bicep file surfaced a policy scope mismatch before deployment, not after, which is the failure mode this pattern is built to catch.

Compare this to Terraform, where the same fan-out needs a for_each module block and a remote state file per environment; Bicep has no state file to reconcile, since Azure Resource Manager reads live resource state on every run.

RBAC, migration from ARM, and getting support

Bicep deployments inherit whatever identity submits them, so RBAC design happens at the deployment scope, not inside the file. A service principal running a resource-group-scope deployment needs Contributor there; push that same Bicep file to subscription scope to write a policy assignment, and it needs Owner or Contributor plus User Access Administrator one level up.

We've seen a common migration error come from exactly that mismatch: a service principal scoped correctly for resource-group deployments gets reused for a subscription-scope Bicep file and fails with AuthorizationFailed on the policyAssignments write. The fix is a new role assignment at the right scope, not a broader grant across the tenant.

For the migration path itself, az bicep decompile turns an existing Azure Resource Manager template into a starting Bicep file, but it doesn't produce a clean result.

Expect TODO comments where the transpiler can't resolve a resource API version, and parameter blocks where a JSON defaultValue type doesn't map cleanly to Bicep's type system. Treat the decompiled file as a diff to review against the source template, not a deploy-ready replacement.

For support, the Azure/bicep GitHub repository takes community contributions under its own CONTRIBUTING.md process, and ships CLI releases roughly every two weeks according to its releases page. Microsoft Learn's Q&A forum is the better place to ask syntax-specific questions once you've checked the docs.

FAQ: Azure Bicep questions answered

What is Azure Bicep?

Bicep is a domain-specific language that transpiles into standard Azure Resource Manager JSON templates, letting engineers declare Azure resources without writing raw ARM code by hand. Microsoft maintains the compiler as an open-source project in the Azure/bicep GitHub repository. Use it when you want Resource Manager's deployment guarantees with less boilerplate than JSON.

When was Azure Bicep released?

Bicep reached general availability in 2021, per the Azure/bicep GitHub release history, after roughly a year in public preview. Microsoft still ships new Bicep CLI versions on a near-monthly cadence, visible in those same Azure/bicep GitHub release notes. See Microsoft Learn's Bicep documentation before upgrading a production pipeline.

How do I install Bicep with Azure CLI?

Run az bicep install, then az bicep upgrade to keep the compiler current, since Azure CLI ships Bicep as a separate binary rather than bundling it. az bicep version confirms what's active in your pipeline. Pin that version in CI so a Bicep CLI update doesn't silently change transpiled JSON output.

What does Bicep what-if do?

What-if deployment runs your Bicep file against ARM's control plane and returns a diff of create, modify, and delete operations before anything executes. It flags property-level changes, like a shifted location or SKU, that a cached plan might miss. Run it before every production deploy.

Is Bicep better than Terraform for Azure?

Neither tool wins outright: Bicep and Terraform solve different problems. Bicep skips a separate state file since ARM tracks state natively; Terraform keeps its own state file that needs periodic reconciliation against actual infrastructure. Pick Bicep for single-cloud estates, Terraform for multi-cloud ones.

How does Bicep handle RBAC?

Bicep has no RBAC layer of its own; it inherits whatever role the deploying identity holds at the target deployment scope, so ask which scope you're deploying to before granting access. A service principal needs Contributor for a resource-group deployment, but Owner or User Access Administrator one level up for a subscription-scope policy assignment. Plan roles first.

Can I deploy Bicep files in Azure DevOps?

Yes, the AzureResourceManagerTemplateDeployment@3 and AzureCLI@2 tasks both accept a Bicep file directly and transpile it to JSON at deploy time, no separate build step required. Azure Pipelines and GitHub Actions support this natively. Use az bicep build locally first to catch syntax errors before the pipeline does.

Do I need to rewrite all my ARM templates for Bicep?

No, migration can be incremental since az bicep decompile converts existing ARM JSON templates into Bicep files automatically. We've seen decompiled output need manual cleanup around copy loops, resource names, and parameters, so treat it as a draft, not a final file. Migrate module by module, not all at once.

Ready to adopt Bicep? Get expert migration support

Migrating from ARM JSON templates to Bicep starts with translating existing json template resources into Bicep's simpler syntax, then validating parameter types, module structure, and resource dependencies before you deploy to production.

Azure quickstart templates offer a fast way to see idiomatic Bicep patterns for common azure services, and comparing your converted files against those quickstart samples helps you select the right module structure for your environment.

Once migrated, keeping every Bicep file, parameter set, and module aligned across dev, staging, and production is an ongoing task, not a one-time project.

That's where our Ops & Managed Services team fits in: 24/7 monitoring for drift, proactive maintenance, cloud cost optimization, and security audits that scale with your templates.

If you're weighing a migration path, our engineers can review your current ARM templates and recommend a phased conversion plan. Get a project estimate to scope the work.

Mikołaj Maćkowiak

Mikołaj is a Cloud Architect and a leader of Azure technology in Netguru. Previously, he worked as a software developer and team leader. Awarded with Microsoft Most Valuable Professional in Azure category.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business