AzureBicepIaCDevOps

How to Write Your First Azure Bicep Template

Parveen Singh
May 14, 2026
12 min read
How to Write Your First Azure Bicep Template

All right, so in the first post of this series you installed the Azure CLI, added the Bicep extension to VS Code, and deployed a resource group. That's the plumbing. The very next thing people ask me is the thing that actually scares them: "Okay, but how am I supposed to know how to write a resource? I don't know the type names, I don't know the properties, and every example online looks different."

Here's the good news, and it's the whole point of this post. You don't memorize any of that. Bicep is not a language where you learn a thousand resources. It's a language where you learn one shape, and then every resource you'll ever write is that same shape with a different label on it. A storage account, a virtual network, a key vault, an AKS cluster: structurally, they are identical. Only two things change between them, and the tooling fills in both of those for you.

So in this post we're going to write one real resource, a storage account, from a blank file. But the storage account isn't the lesson. The lesson is the shape, and the habit of letting the editor do the part you were dreading.


Every Bicep Resource Is the Same Shape

Open a new file called main.bicep and type this out. Don't paste it yet. Typing it once is how the shape sticks.

resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = {
  name: 'stdemobicep001'
  location: 'canadacentral'
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

That's a complete, deployable storage account. Now look at what it's actually made of, because these four parts are every resource you will ever declare in Bicep.

First, the resource keyword. Every resource declaration starts with it, every time.

Second, the symbolic name, which here is storageAccount. This is a name you invent, and it only exists inside this file. We'll come back to why it matters, because it's more useful than it looks.

Third, the type and API version, the string 'Microsoft.Storage/storageAccounts@2025-06-01'. The part before the @ is the resource type, which tells Azure what kind of thing you're building. The part after the @ is the API version, which tells Azure which version of that resource's schema you want to use.

Fourth, everything inside the curly braces, the properties. This is the configuration: what to name the actual Azure resource, where to put it, which SKU, which kind.

That's it. That's the shape. A virtual network is the same four parts. A key vault is the same four parts. When you move from one resource to the next, the only things that change are the type string and the properties inside the braces. The structure never moves. Once you genuinely believe that, Bicep stops being a language to study and becomes a form to fill in.

Pro tip: Notice the name property is not the symbolic name. storageAccount is the internal label; 'stdemobicep001' is what actually shows up in the Azure portal. Keeping those two distinct in your head clears up about half the confusion beginners have with Bicep.


Stop Memorizing Type Strings and API Versions

Now, that type string, 'Microsoft.Storage/storageAccounts@2025-06-01', is exactly the kind of thing people assume they need to memorize. You don't. You shouldn't even try. This is the part of episode one that pays off now.

Delete the type string and start retyping it. The moment you type Microsoft.Storage/, the Bicep extension's IntelliSense drops down a list of every resource type in that namespace. Pick storageAccounts. Then type the @, and it offers you a list of valid API versions, newest at the top. You never have to know these. The editor knows them, because the extension is built by the Azure team and reads the same resource schemas Azure itself publishes.

The same thing happens inside the braces. Start a new line in the properties and the editor suggests the valid property names for a storage account, marks which ones are required, and for properties with a fixed set of values, like kind, it lists the allowed options. You're not recalling configuration from memory. You're choosing from a menu the editor hands you.

One real decision does fall to you, though: which API version to pick. The list will usually include preview versions, with names ending in something like -preview. For anything you intend to actually deploy and rely on, choose the latest stable version, not a preview. A stable API version is supported, predictable, and won't change underneath you. A preview can change or disappear, which is exactly what you don't want a deployment depending on.

Warning: It's tempting to grab the very top item in the API version list to feel current. Read it first. If the top one is a -preview build, scroll down to the newest version without that suffix. Pinning a stable version is the difference between a template that deploys the same way next quarter and one that surprises you.


The Symbolic Name Is Yours, Not Azure's

Let me come back to that symbolic name, storageAccount, because this is where it earns its keep.

The symbolic name never appears in Azure. You won't find it in the portal, in the resource ID, or anywhere in your subscription. It exists purely so the rest of your Bicep file can refer to this resource. And the second you need one resource to know something about another, that's how you do it.

Say you want your deployment to hand back the storage account's resource ID and its blob endpoint when it finishes. You add an output for each, and you reference them through the symbolic name:

output storageId string = storageAccount.id
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob

Here's the part that matters. When you type storageAccount., the editor autocompletes .id, .name, .location, and .properties. Keep going into .properties. and it walks you straight down primaryEndpoints to blob, because it knows the full output schema for that API version. You are not guessing that a storage account exposes a blob endpoint at properties.primaryEndpoints.blob. The editor reads it off the same schema, the same way it offered you the property names earlier.

This is why the symbolic name is worth caring about. It's the handle that makes everything else, outputs, references between resources, dependencies, possible, and it plugs straight into the autocomplete so you discover what a resource exposes instead of memorizing it.

Gotcha: A symbolic name has to be unique within the file and can't start with a number, but it has no relationship to the deployed resource's name. You can call the symbol sa and name the actual account 'stdemobicep001' and Azure neither knows nor cares. Pick symbolic names that read well in your own code, because they're for you, not for Azure.


The Template Describes What, Not Where or Who

Look back at the file and notice what's missing. There's no subscription ID. There's no tenant. There are no credentials. Nothing in the template says which Azure to deploy into or who is allowed to.

That's deliberate, and it's one of the things that makes Bicep feel right once it clicks. Your template only ever describes the desired end state: a storage account that looks like this. The questions of which subscription, which tenant, and who's authorized all come from your az login session at deploy time, not from the file.

# The session decides the "where" and "who". Confirm it before deploying
az account show --output table

Whatever subscription that command reports is where this template will land. Switch accounts or switch subscriptions and the exact same file deploys somewhere completely different, without changing a character.

The mental model I give people is a Dockerfile. A Dockerfile describes the image you want; it doesn't say which machine to run on or who's allowed to run it. The runtime handles that. A Bicep file works the same way. It describes the resource you want, and the deployment environment, your authenticated session, supplies the rest. That separation is what keeps the template portable: the same file works for your dev subscription, your team's, and production, because the file describes the what and the session decides the where.

If you came from the Terraform world, this is a familiar idea with less ceremony around it. If Terraform is where you're actually headed, I've got a beginner's walkthrough for Terraform on Azure too, but for Azure-only work this provider-free, session-driven model is one of Bicep's quiet advantages.


Errors Are a Skill, Not a Failure

Before we deploy, let's break it on purpose, because learning to read errors is the single most useful Bicep skill there is, and the tooling makes it almost pleasant.

Change the kind to something invalid:

kind: 'SuperStorage'

You don't have to deploy to find out that's wrong. The editor puts a red squiggle under it immediately, and hovering shows you the problem along with the list of values kind actually accepts, things like StorageV2, BlobStorage, and FileStorage. The mistake and the fix arrive together, before you've run anything. That's the extension doing for you exactly what episode one promised it would.

The extension catches most of these structural mistakes up front. The ones it can't catch are the ones that depend on the live state of Azure, like a storage account name that's already taken globally, since names have to be unique across all of Azure. Those surface at deploy time instead. And here's the thing people brace for and rarely need to: Azure's deploy-time errors are readable. A name collision tells you the name is already in use. A bad location tells you it's not a valid region. The error names the property and the problem.

So the actual skill isn't avoiding errors. You won't. The skill is the loop: read the message, find the property it names, fix that property, redeploy. That's it. The first time you fix a Bicep error by calmly reading it instead of pasting it into a search engine in a panic, you've crossed the line from someone who fears Infrastructure as Code to someone who uses it.

Pro tip: Fix the kind back to 'StorageV2' before moving on. And when a deploy-time error scrolls a wall of JSON at you, look for the message field near the top. The one human sentence you need is almost always right there.


Deploy It, Then Prove Idempotency

With the file valid again, deploy it into the resource group you created in episode one:

az deployment group create \
  --resource-group rg-demo-bicep \
  --template-file main.bicep

The storage account shows up. Satisfying, but not the point. The point is what happens when you run that exact command a second time.

Nothing. No second storage account, no duplicate, no error. Bicep compares what your file describes against what's already in Azure, sees they match, and does nothing. That's idempotency, and it's the entire payoff of writing infrastructure as code: running the same template is always safe.

Now change one property and only one. Bump the SKU:

sku: {
  name: 'Standard_GRS'
}

Redeploy with the same command, and Bicep updates that one property on the existing account and leaves everything else exactly as it was. You didn't write a script that says "if the account exists, find it, then change its SKU." You changed the desired end state and let the engine work out the difference. That's the whole shift: you declare what you want the world to look like, and Bicep figures out the steps to get there.

And if you want to see those steps before they happen, ask for the diff first:

# Preview exactly what will change before applying it
az deployment group what-if \
  --resource-group rg-demo-bicep \
  --template-file main.bicep

That shows you what will be created, modified, or left alone, color-coded, before anything runs. I use it before every deployment that touches something I'd be unhappy to break.


The Bigger Lesson

If you take one thing from this post, make it this: Infrastructure as Code is the move from scripting the steps to describing the end state. The old way was a list of instructions, do this, then this, then check if that exists, then do the other thing. Bicep is a description of the result you want, and the engine owns the steps. That's why the same file is safe to run once or fifty times, and it's why a deployment that reports no changes is a feature, not a letdown.

Everything else in this post serves that one idea. The repeating four-part shape means describing a resource is mechanical, not memorized. IntelliSense means you don't carry type strings and schemas in your head. The symbolic name means resources can describe their relationships to each other. And reading errors calmly means the occasional wrong description is a two-minute fix, not a crisis.

Next in the series, we'll stop hardcoding values like the name and location into the file and pull them out into parameters and variables, so one template can deploy a dev environment and a production environment without you editing it. Once you've got the shape down, that's the natural next step, and it's where Bicep starts to feel less like a config file and more like real, reusable infrastructure. If you want to see where this road eventually leads, it's pipelines: building CI/CD pipelines on Azure is the same describe-the-end-state thinking, just automated end to end.

You wrote a real resource from a blank file today, and more importantly you learned that there was only ever one shape to learn. The storage account was never the point. Being able to write any resource, calmly, is.

This article is based on episode two of my Bicep from Scratch series on YouTube (cloudwithsingh).

Recommended Readings