What Is Azure Bicep and Why You Should Learn It

All right, so most of the people I work with who are still nervous about Infrastructure as Code aren't nervous about the concept. They're nervous about the JSON. They opened an ARM template once, saw 400 lines of nested brackets to deploy a single storage account, and quietly went back to clicking through the portal. I get it. I did the same thing for longer than I want to admit.
But here's the thing about clicking through the portal: you can do it, and it works, and then six months later someone asks you to spin up the same environment in a second region, and you can't. You don't remember every checkbox you ticked. You can't hand the steps to a junior engineer. You can't put it in a pull request and review it. That is the actual problem with portal deployments. It isn't that the JSON is ugly. It's that nothing you do in the portal can be repeated, handed off, or version-controlled.
Azure Bicep fixes exactly that, and it does it without making you learn a whole new deployment system. That last part is the piece almost everyone misses, so let me start there.

Bicep Is Not a New Deployment System
The single most useful thing I can tell you about Bicep is that it is not a separate engine. It is a friendlier language that compiles down to ARM JSON and then submits that JSON to the exact same Azure Resource Manager that the portal uses, that the CLI uses, that everything in Azure uses.
When you run a Bicep deployment, the Bicep tooling transpiles your .bicep file into a standard ARM template behind the scenes, and that template is what Azure actually processes. You can see this yourself with one command:
# Compile a Bicep file to its underlying ARM JSON
az bicep build --file main.bicep
That produces a main.json sitting right next to your Bicep file. Open it and you'll find the same verbose ARM template you were dreading. The difference is you didn't write it, and you never have to read it.
This has a consequence that matters more than it sounds: anything ARM can do, Bicep can do. There is no feature gap, no "Bicep doesn't support that yet" for core deployments, because Bicep is just ARM with a better front end. You're not betting on a new abstraction that might fall behind. You're removing the one part of ARM that everybody hated, and keeping everything else exactly as it was.
Here's the same storage account that ate 30-plus lines of JSON, written in Bicep:
resource storage 'Microsoft.Storage/storageAccounts@2025-01-01' = {
name: 'stdemobicep001'
location: 'canadacentral'
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
That's the whole thing. It reads top to bottom, the resource type and API version are right there on the first line, and you get the same result Azure would build from the JSON.
Pro tip: Pin the API version (the
@2025-01-01part) deliberately rather than chasing the newest one. A known API version means your template behaves the same way every time you deploy it, which is half the point of doing this in the first place.
Why Bicep Over Terraform for Azure-Only Work
The first is state. Terraform keeps a state file that records what it thinks it has deployed, and you are responsible for storing that file, locking it, and keeping it from getting corrupted or out of sync. Bicep has no state file at all. It asks Azure Resource Manager what's actually deployed and works from that, so there's nothing for you to store and nothing for you to corrupt.
The second is timing. When Microsoft ships a new Azure service or a new property on an existing one, Bicep can use it on day zero, because it talks directly to the same Resource Manager APIs the service team published. Terraform's Azure provider has to add support for it first, which usually lands later.
The third is tooling. The Bicep language and its VS Code extension are built by the Azure team, so the IntelliSense knows about resource types, valid properties, and allowed values straight from the source. That's not a nice-to-have. As you'll see in a second, that tooling is genuinely half of why Bicep feels good to write.
Gotcha: "No state file" does not mean "no source of truth." Your Bicep files in source control ARE the source of truth for what you intend. Azure is the source of truth for what exists. The whole workflow is about keeping those two in agreement, which is exactly what you'll do at the end of this post.
Getting Set Up Is Genuinely Two Things
People expect this part to be a chore. It isn't. To start writing and deploying Bicep, you need exactly two things.
The first is the Azure CLI, because the CLI is what actually submits your deployment. If you've used Azure from the terminal at all, you already have it. If not, install it and sign in:
# Sign in and confirm which subscription you're pointed at
az login
az account show --output table
That second command matters more than it looks. Always confirm which subscription you're about to deploy into before you run anything, because nothing about Bicep stops you from deploying to the wrong one.
The second thing is the official Microsoft Bicep extension for VS Code. Install it from the marketplace, or from the command line:
code --install-extension ms-azuretools.vscode-bicep
This extension is the part I'd argue you can't skip. It gives you IntelliSense for every resource type, so when you type Microsoft.Storage/storageAccounts@ it offers you the valid API versions instead of making you guess. It autocompletes property names and tells you which ones are required. And it does real-time error checking, so a typo in a property name gets a red squiggle while you type instead of a deployment failure ten minutes later. Writing Bicep without this extension is like writing code with the linter turned off. You can do it, but you're choosing to find your mistakes the slow way.
Pro tip: The extension also formats your file on save and surfaces the official docs for any resource type on hover. Two minutes of reading the hover docs has saved me more time than any blog post on a specific resource ever has.
Your First Deployment and the Scope Trap
Here's where almost everyone trips on their first real deployment, so I want to walk through it carefully.
When you deploy Bicep, you have to tell Azure what scope you're deploying at. Most tutorials start you off deploying resources into an existing resource group, which looks like this:
# Deploy into an EXISTING resource group
az group create --name rg-demo-bicep --location canadacentral
az deployment group create \
--resource-group rg-demo-bicep \
--template-file main.bicep
That's the group scope, and it's the right command when the resource group already exists and you just want to put things in it.
But a resource group is itself a resource, and a resource group does not live inside another resource group. It lives at the subscription level. So if you want your Bicep file to create the resource group, you cannot use az deployment group create, because there's no group to target yet. You have to deploy at the subscription scope.
That means two changes. First, you declare the scope at the top of your Bicep file:
targetScope = 'subscription'
resource rg 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: 'rg-demo-bicep'
location: 'canadacentral'
}
And second, you use the subscription-scoped deployment command, which is a different command than the one above:
# Deploy at the SUBSCRIPTION scope, so Bicep can create the resource group
az deployment sub create \
--location canadacentral \
--template-file main.bicep
The two things that change together are the targetScope = 'subscription' line in the file and the switch from az deployment group create to az deployment sub create on the command line. Notice that the subscription-scoped command takes a --location instead of a --resource-group, because the deployment record itself has to live somewhere even though the resources it creates land wherever you tell them to.
This is the number one reason a first Bicep deployment fails with a confusing error. The file and the command have to agree on the scope. If your error message mentions a scope mismatch or complains that a resource group wasn't found, this is what to check first.
The Real Payoff Is Idempotency
So you run that subscription deployment, the resource group and your resources show up, and that's satisfying. But the part that actually changes how you work is what happens when you run it again.
Run the exact same command a second time:
az deployment sub create \
--location canadacentral \
--template-file main.bicep
Nothing changes. No second resource group, no duplicate storage account, no error. Bicep compares what your file describes against what already exists in Azure, sees that they match, and does nothing. That property is called idempotency, and it's the whole reason this approach lets you sleep at night.
The mental model I give people is git status on a clean repository. You run it expecting to see your changes, and instead it tells you there's nothing to commit, and that "nothing" is exactly the confidence you wanted. A Bicep deployment that reports no changes is telling you your environment matches your code. If you tweak one property in the file and redeploy, Bicep changes only that one thing and leaves everything else alone.
If you want to see what a deployment would do before committing to it, ask for the diff first:
# Preview the changes without applying them
az deployment sub what-if \
--location canadacentral \
--template-file main.bicep
That's your git diff. It shows you what will be created, modified, or deleted, color-coded, before anything actually happens. I run this before every deployment that touches something I care about.
The Bigger Lesson
The reason I push people toward Bicep isn't that it's trendy. It's that it changes what you can be sure of. When your infrastructure lives in a portal, the honest answer to "what's actually deployed right now?" is "I think I know." When it lives in Bicep files in source control, the answer is "let me run what-if and show you," and that answer is the same whether it's been five minutes or five months.
That's the same shift I wrote about back when ARM templates were the only option, and I still think ARM templates are worth understanding precisely because they're what Bicep compiles into. Bicep didn't replace ARM. It made ARM usable. And once your deployments are repeatable and reviewable, the next obvious step is making them automatic, which is exactly what a CI/CD pipeline with GitHub Actions gets you.
This post is the first in a Bicep from Scratch series, and the goal across all of it is the same one I started with. Stop clicking. Start writing infrastructure you can repeat, review, and trust. The JSON was never the point. Being able to prove what you've deployed is.



