From ARM templates to Bicep
-
Phase 0: Before you start
-
Phase 1: The foundation
-
Phase 2: Day-to-day operations
-
Phase 3: Enterprise architecture
-
Phase 4: Troubleshooting step by step
-
Phase 5: Final project and career
Moving from an ARM template to Bicep is the first step to managing Azure as code. ARM templates are JSON files Azure deploys; Bicep is a shorter, cleaner language that compiles to the same JSON, with type checking and error messages before anything is deployed.
Most admins meet ARM templates through the portal’s “Export template” button, then find the exported JSON is long, full of hard-coded values and hard to reuse. Converting it to Bicep is the usual next move, and the converter has traps.
In the lab you build an NSG by hand, export it, write the clean Bicep version, deploy it with a parameters file, and fix the errors the decompiler and the compiler throw at you.
| Time | You need | Cost | By the end you can |
|---|---|---|---|
| About 45 minutes | The CLI with Bicep (Lesson 3), VS Code with the Bicep extension | No cost: a network security group only | Read ARM JSON, write and deploy Bicep with parameters, and fix decompile and parameter errors |
What it is, in one line
Bicep is a simpler language for Azure Resource Manager templates that compiles into the same ARM JSON Azure deploys.
In Simple Words
The recipe card and the shopping list
An ARM template is like a recipe written as a long legal contract: every ingredient spelled out in full, with brackets and quotes everywhere. It works, but nobody enjoys reading it.
Bicep is the same recipe on a neat card: “2 eggs, 1 cup flour, bake 20 minutes”. A helper turns the card into the contract when needed, so the kitchen (Azure) gets exactly what it always got.
When you copy a recipe from someone else’s kitchen (export), you also copy their oven settings and their spelling mistakes. Today you clean those up.
Engineer's View
ARM JSON vs Bicep
| ARM template (JSON) | Bicep | |
|---|---|---|
| File | azuredeploy.json | main.bicep |
| References | [parameters(‘x’)], [resourceId(…)] | x, nsg.id |
| Dependencies | dependsOn written by hand | Worked out from references |
| Errors | Mostly at deployment | Most at build time, in VS Code |
| Parameters | parameters.json | .bicepparam with type checking |
| Deployed as | JSON | Compiled to JSON first |
- Same engine: Bicep has no runtime of its own.
az bicep buildturns a .bicep file into ARM JSON, and Resource Manager deploys that. - Decorators:
@description,@minValue,@maxValue,@allowedand@securedocument and validate parameters. - Decompile:
az bicep decompileconverts ARM JSON to Bicep on a best-effort basis; expect warnings and errors to fix by hand. - Exported templates repeat child resources (like NSG rules) both inside the parent and as separate resources, and hard-code names and regions.
- Deployment scopes: resource group, subscription, management group and tenant, with
targetScope.
How it works, step by step
What happens when you deploy a Bicep file:
- The CLI compiles
nsg.bicepto ARM JSON, checking types and decorators. Errors stop here. - It sends the JSON and your parameter values to Resource Manager.
- Resource Manager validates the template and works out the order of resources.
- It creates or updates each resource to match the template (incremental mode by default).
- Outputs, like the NSG’s ID, come back in the deployment result.
Because templates describe the end state, deploying the same file twice changes nothing the second time. That’s what makes them safe to run from a pipeline.
Never edit the compiled JSON. The .bicep file is the source; the JSON is a build output, like an .exe from source code.
Hands-on lab: ARM template to Bicep for an NSG
Make a folder for this lesson’s files:
New-Item -ItemType Directory -Path C:\azure-lab\bicep -Force | Out-Null
Set-Location C:\azure-lab\bicep1Create an NSG by hand, the “click-ops” way
Run this:
az group create --name rg-saat-l41 --location centralindia --tags course=saatvedha-azure lesson=41 -o none
az network nsg create --resource-group rg-saat-l41 --name nsg-saat-demo --tags course=saatvedha-azure lesson=41 -o none
az network nsg rule create --resource-group rg-saat-l41 --nsg-name nsg-saat-demo --name allow-https-in --priority 100 --direction Inbound --access Allow --protocol Tcp --source-address-prefixes Internet --destination-port-ranges 443 -o none2Export it as an ARM template
Run this, then open exported.json in VS Code:
az group export --name rg-saat-l41 > exported.json
(Get-Content exported.json).CountYou should see a number around 70: the line count of the exported JSON.
Look at what you got: a parameter called networkSecurityGroups_nsg_saat_demo_name, the region hard-coded, the rule written twice (inside the NSG and again as a separate resource), and dependsOn by hand. That’s typical of exported templates.
3Write the clean Bicep version
Save this as nsg.bicep (in VS Code, or with the here-string below), then build it:
$b = @'
@description('Name of the network security group')
param nsgName string = 'nsg-saat-demo'
@description('Azure region for the NSG')
param location string = resourceGroup().location
@description('Priority of the HTTPS rule (100 to 4096)')
@minValue(100)
@maxValue(4096)
param httpsPriority int = 100
resource nsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = {
name: nsgName
location: location
tags: {
course: 'saatvedha-azure'
lesson: '41'
}
properties: {
securityRules: [
{
name: 'allow-https-in'
properties: {
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'Internet'
destinationAddressPrefix: '*'
access: 'Allow'
priority: httpsPriority
direction: 'Inbound'
}
}
]
}
}
output nsgId string = nsg.id
'@
Set-Content -Path nsg.bicep -Value $b
az bicep build --file nsg.bicep
(Get-Content nsg.bicep).CountYou should see this, and a new nsg.json next to it:
38No output from the build means no errors and no warnings. 38 lines of Bicep compile to the same kind of JSON that took about 70 lines to export, and this version has a region parameter, a validated priority and an output.
4Deploy it with a parameters file
Create a .bicepparam file that changes the priority, and deploy:
Set-Content -Path nsg.bicepparam -Value "using 'nsg.bicep'`n`nparam httpsPriority = 200"
az deployment group create --resource-group rg-saat-l41 --name nsg-deploy --parameters nsg.bicepparam --query "{state:properties.provisioningState, nsgId:properties.outputs.nsgId.value}" -o yaml
az network nsg rule list --resource-group rg-saat-l41 --nsg-name nsg-saat-demo --query "[].{name:name, priority:priority, port:destinationPortRange}" -o tableYou should see something like this:
nsgId: /subscriptions/.../resourceGroups/rg-saat-l41/providers/Microsoft.Network/networkSecurityGroups/nsg-saat-demo
state: Succeeded
Name Priority Port
-------------- ---------- ------
allow-https-in 200 443The deployment updated the NSG you created by hand: same name, so Resource Manager changed the rule’s priority instead of creating a second NSG. From now on, this file is the source of truth.

5Deploy again and see nothing change
Run the same deployment a second time:
az deployment group create --resource-group rg-saat-l41 --name nsg-deploy-2 --parameters nsg.bicepparam --query "properties.provisioningState" -o tsvYou should see this:
SucceededSame file, same result, no changes. Lesson 42 adds what-if, which shows you that before you deploy.
One more habit for every ARM template to Bicep conversion: read the warnings, not just the errors. If you misspell a property, for example secrityRules instead of securityRules, Bicep still builds and prints Warning BCP089: The property "secrityRules" is not allowed on objects of type "NetworkSecurityGroupPropertiesFormat". Did you mean "securityRules"?
A warning doesn’t stop the build, so a pipeline carries on, and depending on how the resource provider treats unknown properties, you can end up with an NSG without the rules you meant to add. Make your pipeline fail on any Bicep warning, and fix them in VS Code, where they show as yellow squiggles while you type.

✅ Checkpoint: nsg.bicep builds with no warnings, deploys with nsg.bicepparam, and the rule now has priority 200.
Break It, Then Fix It
Both errors here are real Bicep compiler output; your line numbers may differ slightly.
1Break it: let the decompiler do the work
Convert the exported template automatically:
az bicep decompile --file exported.jsonYou’ll see something like this:
WARNING: Decompilation is a best-effort process, as there is no guaranteed mapping from ARM JSON to Bicep Template or Bicep Parameters.
...
exported.bicep(14,13) : Error BCP080: The expression is involved in a cycle ("networkSecurityGroups_nsg_saat_demo_name_allow_https_in" -> "networkSecurityGroups_nsg_saat_demo_name_resource").
exported.bicep(52,5) : Error BCP080: The expression is involved in a cycle ("networkSecurityGroups_nsg_saat_demo_name_resource" -> "networkSecurityGroups_nsg_saat_demo_name_allow_https_in").The export lists the rule twice. The decompiler kept both, and the inline copy refers to the separate rule’s ID while the separate rule depends on the NSG. Each waits for the other, so Bicep refuses to build.
Fix it: in exported.bicep, delete the line starting with “id:” inside securityRules, and delete the whole separate securityRules resource at the bottom. Or better, define rules in one place only, as nsg.bicep does. Then build:
az bicep build --file exported.bicepNo output means it builds. Rename the long generated names to short ones like nsg while you’re there.
2Break it: a parameter value Bicep can catch early
Someone sets the HTTPS rule’s priority to 50 to put it “on top”:
Set-Content -Path nsg.bicepparam -Value "using 'nsg.bicep'`n`nparam httpsPriority = 50"
az deployment group create --resource-group rg-saat-l41 --name nsg-deploy-3 --parameters nsg.bicepparamYou’ll get an error like this:
nsg.bicepparam(3,23) : Error BCP328: The provided value (which will always be less than or equal to 50) is too small to assign to a target for which the minimum allowable value is 100.NSG priorities run from 100 to 4096. Because nsg.bicep says so with @minValue(100), the mistake is caught on your laptop before anything reaches Azure. Without the decorator, Resource Manager would reject it later, mid-deployment.
Fix it: use a valid value and deploy, then clean up:
Set-Content -Path nsg.bicepparam -Value "using 'nsg.bicep'`n`nparam httpsPriority = 110"
az deployment group create --resource-group rg-saat-l41 --name nsg-deploy-4 --parameters nsg.bicepparam --query "properties.provisioningState" -o tsv
az group delete --name rg-saat-l41 --yes
C:\azure-lab\Find-LabResources.ps1 -Lesson 41You should see this:
Succeeded
Nothing tagged course=saatvedha-azure is left.
✅ Checkpoint: You fixed a decompile cycle and a parameter caught at build time. Keep the files in C:\azure-lab\bicep for Lesson 42.
This happened at work
The firewall rule that came back every night
A software company’s ops team removed an old “allow RDP from anywhere” rule from an NSG during an audit. The next morning, the rule was back. They removed it again; it came back again.
Months earlier, someone had exported the NSG from the portal, decompiled it to Bicep, fixed the errors and added it to a nightly pipeline. The exported template included every rule as it existed that day, including the risky one. Each night the pipeline put the NSG back to the template, as designed.
What they changed: the Bicep file became the only place rules change, with pull requests and review. Portal changes to resources managed by the pipeline are blocked with a resource lock and Azure Policy. Exported templates are now treated as a starting draft, never deployed as they are.
Common mistakes and troubleshooting
| Mistake | What you see | What to do |
|---|---|---|
| Deploying a decompiled export as-is | BCP080 cycles, hard-coded values | Clean it: one definition per rule, parameters for names and region. |
| Typo in a property name | Warning BCP089 (“Did you mean securityRules?”) | Read warnings; fail your pipeline on them. |
| No decorators on parameters | Bad values fail late in Azure | Use @minValue, @maxValue, @allowed. |
| Editing compiled JSON | Changes lost on next build | Edit only the .bicep file. |
| Changing resources in the portal and in Bicep | Changes undone at next deploy | One source of truth; lock or policy. |
| Hard-coded region | Template only works in one place | Use resourceGroup().location or a parameter. |
Interview questions
Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.
Fresher level
1. What is Bicep?
Why they ask this: The basic concept.
How you might answer: A language for deploying Azure resources that compiles to ARM template JSON. It’s shorter, has type checking and editor support, and deploys through the same Resource Manager engine.
2. How does Bicep relate to ARM templates?
Why they ask this: They want the link.
How you might answer: Bicep is a layer on top. Every Bicep file compiles to an ARM template, and you can decompile ARM JSON to Bicep. Anything ARM can deploy, Bicep can too.
3. What is a parameters file in Bicep?
Why they ask this: A practical check.
How you might answer: A .bicepparam file that sets values for a Bicep file’s parameters, with a using line pointing to the template. Bicep checks the values against the parameter types and decorators at build time.
2–5 years experience
4. You decompiled an exported template and got BCP080 errors. Why?
Why they ask this: A real trap.
How you might answer: Exported templates often define child resources twice, inline and separately, with references both ways. The decompiler keeps both, which creates a dependency cycle. I remove one copy, usually keeping the rules inline or as separate child resources, not both.
5. How do you stop bad parameter values reaching Azure?
Why they ask this: They want shift-left thinking.
How you might answer: Decorators like @minValue, @maxValue and @allowed, types and user-defined types, .bicepparam files that Bicep checks at build, and a linter run in the pipeline before deployment.
6. Incremental vs complete deployment mode: what’s the difference?
Why they ask this: A classic question.
How you might answer: Incremental, the default, adds and updates resources in the template and leaves others alone. Complete deletes resources in the group that aren’t in the template, so it’s risky; deployment stacks (Lesson 42) are the safer modern way to manage deletions.
Senior level
7. How would you move a team from portal changes to Bicep?
Why they ask this: They want change management.
How you might answer: Start with new resources only, export existing ones as drafts and clean them into modules, put templates in Git with pull requests, deploy through a pipeline, and use locks and policy to stop drift. Train people on reading what-if output.
8. How do you structure Bicep for a large environment?
Why they ask this: They want design.
How you might answer: Small reusable modules per resource type, composed in environment files, parameter files per environment, a module registry or template specs for sharing, and naming and tagging handled centrally.
9. What would make you choose Terraform instead of Bicep?
Why they ask this: They want honest trade-offs.
How you might answer: Multi-cloud resources or providers outside Azure, or an existing Terraform estate and skills. For Azure-only work, Bicep has day-zero support for new resource types and no state file to manage.
Quick quiz
Q1. What does Bicep compile to?
- PowerShell
- ARM template JSON
- YAML
- Terraform
Show answer
B — Resource Manager deploys the compiled JSON.
Q2. Which command converts ARM JSON to Bicep?
- az bicep build
- az bicep decompile
- az deployment group create
- az group export
Show answer
B — Decompile is best-effort and may need fixes.
Q3. BCP080 after decompiling means:
- A typo
- A dependency cycle
- A quota problem
- A missing region
Show answer
B — Two resources reference each other.
Q4. Which decorator rejects priorities below 100?
- @description
- @minValue(100)
- @secure
- @allowed
Show answer
B — @minValue checks numbers at build time.
Q5. A typo like secrityRules gives:
- Nothing
- Warning BCP089
- An Azure outage
- A deleted NSG
Show answer
B — Bicep suggests the right name.
Quick recap
You can now turn click-ops into code you can review and redeploy.
- Bicep compiles to ARM JSON; the engine is the same.
- Exported templates are drafts: duplicated children, hard-coded values.
- Decompile, then fix BCP errors by hand.
- Decorators and .bicepparam catch bad values before Azure sees them.
- The .bicep file is the source of truth; never edit the JSON.
Next lesson
Lesson 42: Bicep in practice: modules, what-if, deployment stacks → You split templates into modules, preview changes with what-if and manage deletions with deployment stacks.
Further reading: ARM template to Bicep
Want to go deeper into ARM template to Bicep? See Microsoft’s What is Bicep? and Decompile ARM template JSON to Bicep. You can also see the full plan for this course on the Azure Cloud Administration course page.
