The quickest option to deploy Azure infrastructure is to write down much less of it. That sounds just like the form of line a marketing consultant says proper earlier than billing you for a six-month DevOps transformation, however it occurs to be all the premise behind Azure Bicep.
In case you’ve ever opened an Azure Useful resource Supervisor (ARM) JSON template and watched a single storage account definition sprawl previous 100 traces of nested brackets, you already know the issue Bicep solves. One misplaced comma in that file breaks the entire deployment, and monitoring it down means scrolling by means of bracket after bracket. Bicep compiles all the way down to that very same JSON construction, however you by no means write the JSON by hand once more. For the click-by-click walkthrough of organising your first deployment, see our step-by-step information to getting began with Azure Bicep. This publish covers the why behind Bicep’s design, then goes additional into what a manufacturing Infrastructure as Code setup really wants: Microsoft-maintained modules as a substitute of hand-rolled ones, a security web that catches a foul deployment earlier than it runs, and a CI/CD pipeline that doesn’t depend upon somebody’s laptop computer.
Stipulations: What You Want Earlier than You Begin
The whole lot beneath assumes a working Bicep setup, not a recent set up. You’ll want the Azure CLI with the Bicep CLI put in (az bicep set up, or az bicep improve if you have already got an older model), the VS Code Bicep extension for inline validation and IntelliSense, and Contributor rights on the Azure infrastructure you’re deploying into. The instructions on this publish had been examined towards Azure CLI 2.80.0 and Bicep CLI 0.40.2.
What Azure Bicep Replaces (and Why It Exists)
One thing nonetheless has to write down that JSON, and one thing nonetheless has to obtain it. Azure Useful resource Supervisor is Azure’s native deployment engine, the one on the receiving finish of each ARM template. Each useful resource you create, whether or not by means of the portal, the CLI, or a script, ultimately turns into an ARM template submitted to that engine. For many of Azure’s historical past, authoring that template meant writing uncooked JSON by hand: an information format constructed for machines to parse, not for people to learn or keep.
From ARM JSON to a Area-Particular Language
Azure Bicep is a domain-specific language, which means it was constructed for precisely one job as a substitute of general-purpose programming. That job is describing Azure assets. Once you run a deployment command towards a .bicep file, the Bicep CLI compiles, or transpiles, your code into the identical ARM JSON that Azure has at all times accepted. Azure Useful resource Supervisor by no means sees Bicep syntax straight; it solely ever sees the JSON that comes out the opposite facet.
That transpilation step issues for a cause past comfort. As a result of Bicep is a skinny layer over ARM quite than a separate platform, it inherits ARM’s day-zero assist for brand new capabilities. Microsoft’s Bicep overview documentation states it straight for each preview and customarily out there (GA) providers: “Bicep instantly helps all preview and GA variations for Azure providers.” The second Azure ships a brand new useful resource kind or API model, you possibly can reference it in Bicep with out ready for a separate supplier replace. Contemplate a useful resource declaration:
useful resource myStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
identify: 'myuniquestorage001'
location: resourceGroup().location
variety: 'StorageV2'
sku: {
identify: 'Standard_GRS'
}
}
myStorageAccount is a symbolic identify right here. It exists solely inside this file, letting you reference the useful resource’s properties elsewhere in your code (myStorageAccount.id, for example).
Bicep and ARM templates have similar functionality by building, since one compiles straight into the opposite, so the one actual Infrastructure as Code choice left is whether or not you additionally handle non-Azure assets, the place a cross-provider instrument like Terraform wins outright.
The Symbolic Title Isn’t the Actual Useful resource Title
The symbolic identify isn’t the precise Azure useful resource identify, which is the myuniquestorage001 worth contained in the properties block. Complicated the 2 is the one most typical mistake new Bicep authors make, and it produces deployment errors that time on the incorrect line. Written this manner, the identical storage account that takes dozens of traces in ARM JSON, with its nested properties object and bracketed apiVersion string, collapses to roughly seven readable traces.
How a .bicep File Turns into Working Infrastructure
Transpilation handles readability. Dependency administration handles deployment order.
Why Deployment Order Issues
In uncooked ARM JSON, if a subnet depends upon a digital community that doesn’t exist but, you need to declare that relationship manually with a dependsOn array. Neglect it, and Azure tries to create each assets directly, and the subnet deployment fails as a result of its guardian doesn’t exist.
How Bicep Infers the Dependency Graph
Bicep infers useful resource dependencies mechanically. When one useful resource’s symbolic identify reveals up inside one other, the compiler works out the dependency graph itself:
useful resource vnet 'Microsoft.Community/virtualNetworks@2023-09-01' = {
identify: 'app-vnet'
location: resourceGroup().location
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/16'
]
}
}
}
useful resource subnet 'Microsoft.Community/virtualNetworks/subnets@2023-09-01' = {
guardian: vnet
identify: 'app-subnet'
properties: {
addressPrefix: '10.0.1.0/24'
}
}
The guardian property hyperlinks the subnet to the digital community’s symbolic identify, vnet. Bicep sees that hyperlink throughout compilation and injects the equal dependsOn array into the ARM JSON it produces, within the appropriate order, with out you writing it. Express dependsOn declarations nonetheless work in Bicep and stay the one choice when two assets are associated in a manner the compiler can’t infer from a direct reference, however for anything they add a line of upkeep for no profit. Get the parent-child relationship backward, or reference a useful resource that was by no means declared, and the deployment fails with a dependency error at runtime quite than a syntax warning whilst you’re modifying, which is why it’s price double-checking each guardian and cross-resource reference earlier than you deploy, not after. The compile step, the injected dependsOn array, and the ensuing deployment order appear to be this:

Parameters, Variables, and the Decorators That Maintain You Sincere
Parameters allow you to cross values right into a template at deployment time; variables retailer expressions you calculate as soon as and reuse. Each exist in ARM JSON too, however Bicep provides decorators: annotations that validate and doc a parameter earlier than Azure ever sees a deployment request.
Typed Parameters With Guardrails
A handful of decorators do many of the work:
-
@description()paperwork what a parameter is for, so the subsequent particular person modifying the file doesn’t need to guess -
@allowed()restricts a parameter to a particular listing of values, catching a typo’d surroundings identify throughout authoring as a substitute of after a failed API name -
@safe()masks a parameter’s worth from Azure CLI output and deployment logs -
@minLength()and@maxLength()implement string or array measurement constraints earlier than deployment begins
@description('Specifies the storage account surroundings kind.')
@allowed([
'dev'
'prod'
])
param envType string = 'dev'
@safe()
param adminPassword string
Professional Tip: @safe() hides a parameter from logs and CLI output, however it doesn’t encrypt the worth at relaxation inside a parameter file. Pull actual secrets and techniques from Key Vault with the getSecret() perform as a substitute of typing them into even a @safe() parameter.
Looping With out Overloading the Azure API
Bicep’s [for ... in ...] syntax deploys a number of copies of a useful resource from a single block, which is helpful till you attempt to create fifty of something directly and Azure Useful resource Supervisor begins returning 429 rate-limit errors partway by means of.
@batchSize(3)
useful resource storageAccounts 'Microsoft.Storage/storageAccounts@2022-09-01' = [for i in range(0, 10): {
name: 'stg${i}${uniqueString(resourceGroup().id)}'
location: resourceGroup().location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}]
The @batchSize() decorator caps what number of assets deploy concurrently from the loop, right here at three. With out that cap, a big array can journey Azure’s throttling limits earlier than the loop finishes. Some assets find yourself created, others fail, and the partial deployment is tougher to scrub up than a whole failure would have been.
Constructing Reusable Infrastructure with Modules
A Bicep module is nothing greater than a typical .bicep file referenced by one other .bicep file. There’s no particular syntax that marks a file as a module. You name it with the module key phrase as a substitute of useful resource.
module storage './modules/storageAccount.bicep' = {
identify: 'storageDeployment'
params: {
storageAccountName: 'mystorage001'
location: 'eastus'
}
}
The orchestrator file passes parameters in and may seize outputs again, with out figuring out or caring concerning the module’s inside useful resource properties. That encapsulation is what makes modules price the additional file: change how the storage module configures diagnostic settings, and each orchestrator that consumes it picks up the change with out being touched.
Skipping the Boilerplate with Azure Verified Modules
Writing your personal networking module means you’re additionally chargeable for holding it aligned with Azure’s safety and reliability steering as that steering evolves. Azure Verified Modules shift that upkeep to Microsoft. AVM is a library of pre-tested, Microsoft-maintained Bicep modules revealed to the general public Bicep registry and referenced with a br/public: alias:
module vnet 'br/public:avm/res/community/virtual-network:0.1.6' = {
identify: 'vnetDeployment'
params: {
identify: 'app-vnet'
addressPrefixes: [
'10.0.0.0/16'
]
}
}
AVM ships two sorts of modules: useful resource modules that deploy a single Azure useful resource with smart defaults baked in, and sample modules that deploy complete architectures directly, the form of turnkey setup our information to constructing Azure touchdown zones covers in additional depth. A sample module is likely to be a hub-and-spoke community topology, or a baseline set of RBAC assignments for a brand new subscription. A model pin like 0.1.6 above beats monitoring newest, as a result of an unpinned module reference means your infrastructure can change habits on a date you didn’t select.
Deploying and Validating Modifications with What-If
Deploying a Bicep file is one command, however the deployment engine can’t inform the distinction between a change you meant to make and a typo that deletes a manufacturing database. That’s what the What-If operation exists to catch, earlier than both one reaches Azure.
Working What-If Earlier than You Deploy
az login az account set --subscription "<subscription-id>" az deployment group what-if --resource-group rg-app-prod --template-file fundamental.bicep --parameters fundamental.bicepparam
az login authenticates the session towards Microsoft Entra ID, and az account set scopes each command that follows to the proper subscription so that you’re not unintentionally previewing modifications towards the incorrect surroundings. The what-if command then performs a dry run: it compiles your Bicep file, queries the dwell state of the useful resource group, and prints a color-coded diff categorized by change kind.
Studying the Change-Sort Desk
| Change Sort | Which means |
|---|---|
| Create | Outlined in Bicep, doesn’t exist in Azure but |
| Modify | Exists, however a property differs from the template |
| Ignore | Exists in Azure however isn’t in your template. In Incremental mode (the default, and what the command above runs) that is what you’ll see, and the useful resource is left alone |
| Delete | Full mode solely: exists in Azure however isn’t within the template, and will probably be eliminated |
| NoChange | Matches the template precisely |
| NoEffect | A property would change, however it’s a read-only property, so the change has no actual impact |
| Deploy | What-If doesn’t have sufficient info to find out the change kind |
Run the command above as written and Ignore is the class to look at, not Delete. The gotcha beneath explains why.
Warning: Azure deploys Bicep templates in Incremental mode by default, which by no means removes a useful resource lacking out of your file. Delete solely reveals up as an actual deletion in Full mode. Full mode is secure solely while you’re sure each useful resource in that group belongs to this deployment. What-If’s Delete warning is your final probability to catch a mistake earlier than it runs.
Automating Deployment with a GitHub Actions Pipeline
Working What-If by hand out of your laptop computer works for one engineer. It stops working the second a second particular person may merge to fundamental. A CI/CD pipeline turns that guide examine right into a gate everybody goes by means of, working on GitHub’s personal runners quite than anybody’s native machine. That’s the similar DevOps automation precept behind Azure Pipelines in case your staff lives in Azure DevOps as a substitute of GitHub.
A Two-Job Pipeline: Validate, Then Deploy
The workflow beneath runs two jobs towards a Bicep template in infra/: a validate job that builds the template and previews modifications with What-If, and a deploy job that solely runs after validate succeeds and a reviewer approves it.
identify: deploy-infrastructure
on:
push:
branches: [main]
paths: ['infra/**']
permissions:
id-token: write
contents: learn
jobs:
validate:
runs-on: ubuntu-latest
steps:
- makes use of: actions/checkout@v4
- makes use of: azure/login@v3
with:
client-id: ${{ secrets and techniques.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets and techniques.AZURE_TENANT_ID }}
subscription-id: ${{ secrets and techniques.AZURE_SUBSCRIPTION_ID }}
- identify: Lint and construct
run: az bicep construct --file infra/fundamental.bicep
- identify: Preview modifications
run: |
az deployment group what-if
--resource-group rg-app-prod
--template-file infra/fundamental.bicep
--parameters infra/fundamental.bicepparam
deploy:
wants: validate
runs-on: ubuntu-latest
surroundings: manufacturing
steps:
- makes use of: actions/checkout@v4
- makes use of: azure/login@v3
with:
client-id: ${{ secrets and techniques.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets and techniques.AZURE_TENANT_ID }}
subscription-id: ${{ secrets and techniques.AZURE_SUBSCRIPTION_ID }}
- identify: Deploy
run: |
az deployment group create
--resource-group rg-app-prod
--template-file infra/fundamental.bicep
--parameters infra/fundamental.bicepparam
Why the Permissions Block Is the Level
permissions: id-token: write is what makes OpenID Join federation doable: GitHub points a short-lived token that azure/login trades for an Entra ID entry token, so no shopper secret sits in your repository settings ready to be leaked. That token trade nonetheless wants an app registration on the Azure facet configured to belief GitHub’s federated credential. The surroundings: manufacturing line on the deploy job is the precise security gate; configure that GitHub Surroundings with required reviewers, and the deploy job pauses till somebody approves it, giving them the What-If output from the validate job because the factor to evaluate.
The three secrets and techniques.AZURE_* values are identifiers, not credentials. They inform azure/login which app registration and tenant to current the OIDC token to; none of them is usable by itself, which is the entire level of federation. contents: learn retains the remainder of the token least-privilege, as a result of naming any permission within the block drops each default you didn’t listing. And paths: ['infra/**'] retains software commits from triggering an infrastructure deploy.
Skip that gate and some concrete failure modes grow to be seemingly as a substitute of hypothetical:
-
A resource-group identify typo silently deploys infrastructure into the incorrect subscription, one no person’s watching
-
A merged pull request with an unreviewed delete change removes a useful resource earlier than anybody reads the diff
-
A stale Bicep CLI model on the runner builds a template that passes domestically and fails in CI with a version-mismatch error no person expects
The 2 jobs and the approval gate between them lay out as you possibly can see beneath:

Maintaining Secrets and techniques Out of Your Templates
A .bicepparam file provides parameter values exterior your fundamental template, utilizing plain Bicep syntax as a substitute of JSON. Pair it with the getSecret() perform and you’ll pull a secret straight from Key Vault at deployment time, as a substitute of typing it anyplace:
utilizing './fundamental.bicep' param environmentName="manufacturing" param sqlAdminPassword = getSecret( '<subscription-id>', 'rg-shared', 'kv-secrets', 'sql-admin-password-prod' )
For getSecret() to drag the worth at deployment time, the goal Key Vault wants its enabledForTemplateDeployment property set to true; skip that and Azure rejects the key lookup with an entry error that has nothing to do along with your Bicep syntax. Throughout deployment, Azure Useful resource Supervisor fetches the key server-side and injects it straight into the useful resource supplier name. It by no means touches the .bicepparam file, your supply management historical past, or your terminal.
Warning: By no means assign a secret worth to a Bicep output. Outputs are recorded in plain textual content within the Azure deployment historical past, and anybody with Reader entry on the useful resource group can learn that worth straight out of the portal, regardless of the way it was masked entering into.
Making Bicep Your Default Method to Contact Azure
In case you’re carrying an current library of ARM JSON templates, you don’t need to rewrite every part earlier than you get worth from Bicep. Run az bicep decompile towards a template to get a place to begin, deal with the output as a tough draft quite than manufacturing code (decompilation is best-effort and may go away behind linting warnings), and migrate one useful resource group at a time.
Begin with the components of this publish that compound: pin your Bicep CLI to a current model so day-zero API assist really applies to you, attain for an Azure Verified Module earlier than writing a networking or id useful resource from scratch, and put What-If in entrance of a pull request earlier than your first manufacturing deploy quite than after a change you didn’t evaluate causes an incident. None of that requires new tooling. It’s deployment automation constructed totally from what’s already in your Azure subscription and GitHub repo.

