Compare Bicep vs ARM Templates for Azure IaC, including syntax, deployment behavior, Deployment Stacks, migration steps, CI/CD integration, and best practices.
Bicep vs ARM Templates for Azure Infrastructure as Code: Syntax, Deployment, and Migration Guide
The comparison gets framed as a choice between two deployment technologies, which sets up the wrong mental model from the start. Every Bicep file becomes the exact same ARM JSON that a hand-written template would produce, processed by the exact same engine — the real decision is about authoring experience and a few genuinely newer capabilities, not about picking between two different ways Azure deploys resources.
"Bicep vs ARM Templates" gets discussed like a choice between two competing infrastructure-as-code technologies — pick one, commit to it, migrate away from the other. The more accurate framing is narrower and less dramatic: Bicep is an authoring syntax for the same underlying ARM template language and the same Azure Resource Manager deployment engine, not a different way of getting resources deployed. That reframing matters because it changes what the "migration" actually is — not a rebuild of how deployments work, but a syntax-level rewrite plus adoption of a few genuinely newer capabilities that happen to be easier to reach from Bicep. This guide works through the syntax differences that matter, an operational distinction around resource cleanup that trips up more environments than it should, an honest account of what the decompilation tooling actually guarantees, and a concrete migration path.
Worth stating precisely, since it resolves several common questions before they need separate answers.
| Common question | Resolved by the framing correction |
|---|---|
| Does What-If validation work differently for Bicep vs ARM JSON? | No — What-If is an Azure Resource Manager capability operating on the compiled JSON, identical regardless of source syntax |
| Will ARM JSON support be dropped now that Bicep exists? | No — Microsoft's own guidance confirms continued support for the underlying ARM JSON language, since Bicep depends on it entirely |
| Do Bicep and ARM JSON deployments behave differently at runtime? | No — same runtime, same core functionality, only the authoring syntax differs |
Because the underlying deployment behavior doesn't change, adopting Bicep for new templates — or migrating existing ARM JSON templates — doesn't introduce a new deployment engine with its own separate failure modes to learn. The risk profile of a Bicep migration is much closer to "rewrite these files in a more maintainable syntax" than "adopt a fundamentally different infrastructure tool," which is worth communicating clearly to any team hesitant about the effort involved.
The same storage account resource, in both syntaxes — the practical difference in verbosity and readability is real, even though both compile to an identical deployed result.
| Bicep feature | What it replaces in ARM JSON |
|---|---|
| String interpolation ('st${...}') | Verbose concat() function calls |
| module keyword | Nested or linked template references |
| existing keyword | Manually constructing a reference to an already-deployed resource without redeploying it |
| for loop expressions | ARM's copy loop property |
| Native if conditionals | ARM's condition property, with more verbose expression syntax |
Beyond readability, the Bicep VS Code extension provides real-time type checking and IntelliSense against the actual Azure resource schema — catching a misspelled property name or an invalid API version at authoring time rather than at deployment time. This is a genuine productivity and error-prevention advantage that ARM JSON's plain-text authoring experience doesn't match, independent of the syntax verbosity difference itself.
This is a real, common, and often-undiscovered source of cost and configuration drift — worth checking for directly in any environment relying on plain deployment commands.
| Deployment method | Behavior when a resource is removed from the template |
|---|---|
| az deployment group create | Resource stays deployed indefinitely — no cleanup mechanism at all |
| az stack group create | Resource can be genuinely deleted, detached, or left in place, per the explicit --action-on-unmanage setting |
Any environment that has relied exclusively on plain deployment commands over time likely has some accumulated drift between what the templates currently describe and what's actually deployed — resources that were removed from source but never cleaned up manually. This is worth an explicit audit pass, comparing current template contents against actual deployed resources in the resource group, independent of whether or when a move to Deployment Stacks happens.
Microsoft's own tooling documentation is direct about this, worth taking at face value rather than assuming a one-command migration.
| What decompilation reliably does | What it doesn't guarantee |
|---|---|
| Converts the bulk of standard resource declarations correctly | A clean, warning-free result on every template — Microsoft's own documentation states there's "no guaranteed mapping from ARM template JSON to Bicep" |
| Flags conversion issues with warnings pointing toward manual fixes | Automatic resolution of those warnings — manual review and correction is expected |
| Gives a genuinely useful head start on a large existing template | Idiomatic Bicep — decompiled output often doesn't use modules, loops, or other Bicep-native patterns even where they'd improve the result |
A decompiled Bicep file that compiles back to equivalent ARM JSON without errors has cleared the minimum bar, not the actual goal — the resulting file is worth a genuine readability and idiom pass, since decompilation optimizes for correctness of the conversion, not for producing the kind of clean, maintainable Bicep a team would choose to write by hand. Variable names, module opportunities, and loop simplifications are all worth a manual look even on a decompilation that reports no errors.
A genuinely newer, Bicep-native parameter file format exists alongside the older JSON parameters approach — worth knowing both, since a lot of still-circulating Bicep content only shows the older format.
| Format | Detail |
|---|---|
| JSON parameters file | The original approach — a separate .json file with a fixed schema, works with both ARM JSON and Bicep templates |
| .bicepparam file | Bicep-native parameter file syntax, requiring Bicep CLI 0.18 or later — supports type checking and a cleaner syntax consistent with the main Bicep file |
Bicep's tooling includes a conversion path from JSON parameter files to .bicepparam format, similar in spirit to the ARM-to-Bicep decompilation covered in Section 4 — apply the same expectation of reviewing the output rather than treating the conversion as guaranteed-correct on the first pass, particularly for parameter files with complex nested objects or array structures.
Microsoft recommends Bicep for new projects, and that's the right default — but there are specific, legitimate reasons an existing environment might reasonably stay on ARM JSON, worth naming directly rather than treating migration as universally urgent.
| Reason to stay on ARM JSON | Why it's legitimate |
|---|---|
| A large, working ARM template library with no active pain points | Migration effort should be justified by a real problem being solved, not adopted purely because a newer syntax exists |
| Heavy reliance on tools that generate ARM JSON directly | Portal's Export Template feature and Azure Policy remediation both produce ARM JSON natively — workflows built around these stay simpler without an added decompile step |
| Azure Blueprints or legacy governance frameworks in use | These have historically required ARM JSON format specifically |
| Multi-cloud or third-party resource management needed | Bicep's scope is Azure resources and, via the Microsoft Graph extension, Entra ID — it has no path to GitHub, GitLab, Databricks, Cloudflare, or other providers the way Terraform's provider ecosystem does |
If infrastructure-as-code needs genuinely extend beyond Azure and Entra ID — managing GitHub repository settings, third-party SaaS configuration, or resources in another cloud provider alongside Azure — Bicep structurally can't be the single tool for that job, regardless of how much cleaner its syntax is for the Azure-specific portion. Terraform's broader provider ecosystem is the more appropriate choice for that scope, and Bicep remains the better fit specifically for teams whose infrastructure-as-code needs are genuinely Azure-and-Entra-scoped.
-
Confirm the Bicep CLI is available
It ships inside the Azure CLI and installs automatically on first use of a command that needs it — no separate install required for normal use.
-
Run the decompile command against the existing ARM JSON template
az bicep decompile --file template.json — produces a starting-point .bicep file.
-
Review every warning the decompiler produces and resolve them manually
Per Section 4 — don't treat a warning-free compile as confirmation the output is production-ready; review even clean conversions.
-
Refactor toward idiomatic Bicep where it genuinely improves the result
Look for opportunities to introduce modules for repeated resource patterns, replace verbose conditional/loop constructs with native if/for syntax, and simplify variable naming.
-
Convert any JSON parameter files to .bicepparam format
Confirm Bicep CLI 0.18+ is available, then use the equivalent conversion tooling, reviewing the output per Section 5.
-
Validate the compiled Bicep produces equivalent ARM JSON to the original template
Compile the Bicep file back to JSON and diff it against the original, or deploy both to a test environment and compare the resulting resources directly.
-
Run a What-If deployment before any real cutover
az deployment group what-if — confirms the Bicep-sourced deployment produces the expected change set with no surprises.
-
Decide whether to adopt Deployment Stacks as part of the migration
If plain deployment commands have been in use, this migration is a reasonable point to also move to az stack group create for genuine resource lifecycle reconciliation, per Section 3.
-
Deploy to a non-production environment first, then production once validated
Treat this like any other infrastructure change — staged rollout, not a direct production cutover on the first deployment.
Both Azure DevOps and GitHub Actions have first-class support for deploying Bicep directly — worth knowing the actual pattern rather than assuming a separate compile step is required in the pipeline.
| Consideration | Detail |
|---|---|
| Compilation step | Not required as a separate pipeline stage — both Azure DevOps' Bicep task and GitHub Actions' azure/arm-deploy accept .bicep files directly |
| What-If as a pipeline gate | Run as a distinct step before the actual deployment, with the pipeline configured to require review or approval on a non-empty change set |
| Deployment Stacks in CI/CD | Worth checking current tooling support directly — some pipeline tasks have historically lagged behind CLI-level Deployment Stack support, so confirm the specific task/action version supports it before relying on it in an automated pipeline |
Running What-If as an explicit, visible pipeline step — with its output surfaced in the pipeline run rather than only available if someone runs it manually — gives reviewers a concrete, reviewable change set before any production deployment proceeds, catching an unexpected resource deletion or property change before it happens rather than after. This is a small addition to pipeline run time that meaningfully reduces the risk of a surprising production change.
| Anti-pattern | Why it feels right | Why it isn't |
|---|---|---|
| Treating Bicep and ARM JSON as having different deployment behavior or reliability | "They're different technologies, should behave differently" | Bicep compiles to the identical ARM JSON that's actually deployed — same engine, same runtime behavior |
| Running bicep decompile once and treating the output as finished, production-ready code | "It compiled without errors, must be correct" | Microsoft's own documentation states there's no guaranteed mapping — review and refactor is the expected next step, not optional polish |
| Relying exclusively on plain deployment commands and assuming removed resources get cleaned up automatically | "If it's not in the template, it shouldn't exist" | Plain deployments never delete resources you stop declaring — orphans accumulate silently unless Deployment Stacks are used |
| Migrating to Bicep purely because it's newer, without a specific problem it solves | "Newer is generally better" | A large, working ARM template library with no active pain points is a legitimate reason to defer migration — justify the effort with a real need |
| Choosing Bicep for infrastructure that spans beyond Azure and Entra ID | "It's the Microsoft-recommended IaC tool" | Bicep has no path to third-party providers like GitHub, GitLab, or other clouds — Terraform is the correct tool for that broader scope |
| Skipping What-If validation before a Bicep-sourced deployment because "the syntax is cleaner, less likely to have errors" | "Bicep catches more mistakes at authoring time" | Type checking catches syntax and schema errors, not logical deployment consequences — What-If remains essential regardless of source syntax |