Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Infrastructure as Code By James Joyner IV · · 8 min read

Azure Bicep: Cleaner Infrastructure Code Than ARM Templates Ever Were

Bicep is Microsoft's domain-specific language that compiles to ARM JSON — with modules, type safety, and readable syntax. Here's how to use it well on Azure.

  • #iac
  • #bicep
  • #azure
  • #arm-templates
  • #cloud
  • #modules

Anyone who’s hand-written an Azure ARM template knows the pain: hundreds of lines of deeply nested JSON, string-concatenated resource IDs, no editor help, and a dependsOn array you have to maintain by hand. Bicep is Microsoft’s answer — a domain-specific language that compiles down to that same ARM JSON but is actually pleasant to write. It’s a transparent abstraction: Bicep in, ARM out, deployed by the same Azure Resource Manager engine.

If you’re building on Azure, Bicep is the path of least resistance now. Here’s how to use it well rather than just writing ARM with nicer syntax.

Why Bicep over raw ARM

The improvements are concrete and immediate:

  • Implicit dependencies. Reference one resource’s property from another and Bicep infers the deployment order. No more hand-maintained dependsOn.
  • Type safety and IntelliSense. The VS Code extension autocompletes resource properties and flags invalid ones before you deploy.
  • Modules. Real, composable units instead of nested-template JSON gymnastics.
  • No state file. Like ARM, Bicep is stateless — Azure Resource Manager is the source of truth. There’s no state file to lock, corrupt, or lose, which is a genuine operational simplification compared to state-based tools.

A storage account in Bicep versus the equivalent ARM JSON is roughly a third the lines and infinitely more readable:

param location string = resourceGroup().location

resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: 'st${uniqueString(resourceGroup().id)}'
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
    allowBlobPublicAccess: false
  }
}

output storageId string = storage.id

Parameters, variables, and decorators

Bicep gives you real input validation through decorators — far better than ARM’s freeform parameters:

@allowed(['dev', 'staging', 'prod'])
param environment string

@minLength(3)
@maxLength(24)
param appName string

@secure()
param sqlAdminPassword string

var tags = {
  environment: environment
  managedBy: 'bicep'
}

The @allowed decorator constrains inputs at deploy time, @secure() keeps secrets out of logs and deployment history, and the validation runs before any resource is touched. Encode your constraints here and bad parameters get rejected immediately rather than failing halfway through a deployment.

Modules: the real structuring tool

Modules are how Bicep scales beyond a single file. A module is just a Bicep file you call from another, passing parameters and consuming outputs. Factor reusable infrastructure — a standard storage account, a networking baseline — into modules and compose them:

module network './modules/network.bicep' = {
  name: 'networkDeploy'
  params: {
    location: location
    addressSpace: '10.0.0.0/16'
  }
}

module app './modules/app-service.bicep' = {
  name: 'appDeploy'
  params: {
    location: location
    subnetId: network.outputs.appSubnetId   // implicit dependency
  }
}

Notice app consumes network.outputs.appSubnetId — Bicep deploys the network module first automatically. You can publish modules to an Azure Container Registry and version them, giving your platform team a private module library the whole org pulls from.

Loops and conditionals

Bicep has the iteration and conditional constructs ARM made you fake with copy blocks and nested templates:

// Deploy N identical resources
resource disks 'Microsoft.Compute/disks@2023-04-02' = [for i in range(0, diskCount): {
  name: 'data-disk-${i}'
  location: location
  sku: { name: 'Premium_LRS' }
  properties: { diskSizeGB: 128, creationData: { createOption: 'Empty' } }
}]

// Conditionally deploy
resource cdn 'Microsoft.Cdn/profiles@2023-05-01' = if (environment == 'prod') {
  name: 'cdn-${appName}'
  location: 'global'
  sku: { name: 'Standard_Microsoft' }
}

This is the kind of logic that turned ARM templates into unreadable JSON. In Bicep it reads like normal code.

Always run what-if before deploying

Bicep’s equivalent of a plan is what-if, and it’s non-negotiable before any production deploy:

az deployment group what-if \
  --resource-group my-rg \
  --template-file main.bicep \
  --parameters environment=prod

It shows you exactly what will be created, modified, or deleted — color-coded — so you catch a destructive change before it happens. Pair it with az bicep lint in CI to catch issues at the source. The what-if output isn’t perfect (some resource providers report changes imperfectly), but it catches the deletions and replacements that matter most.

Migrating from existing ARM

You don’t have to rewrite everything. The az bicep decompile command converts existing ARM JSON to Bicep:

az bicep decompile --file azuredeploy.json

Treat the output as a starting point, not a finished product — decompilation is mechanical and often produces awkward Bicep you’ll want to clean up by hand. But it gets you off raw JSON without starting from scratch.

Where AI helps

Bicep’s resource-type strings and property schemas (Microsoft.Storage/storageAccounts@2023-01-01 and the exact properties shape) are the fiddly part, and AI handles them well.

  • Scaffolding a deployment from a description: “a Bicep template for an App Service with a SQL database and a Key Vault, parametrized by environment.”
  • Converting ARM JSON to clean Bicep — better than raw decompile because the model produces idiomatic structure, though you verify against az bicep build.
  • Writing module interfaces. Describe the inputs and outputs and let it generate the parameter and output blocks.

The model can reference outdated API versions, so always az bicep build to validate and what-if before deploying. We keep a set of IaC prompts for Azure and Bicep scaffolding.

The bottom line

Bicep is what ARM templates should have been: readable, type-safe, modular, and stateless, compiling to the same JSON Azure already trusts. Use decorators to validate inputs, modules to structure and share infrastructure, loops and conditionals instead of JSON tricks, and what-if before every deploy. If you’re building on Azure, there’s almost no reason to hand-write ARM anymore — Bicep gives you all of its reliability with none of its misery.

AI-generated Bicep is assistive, not authoritative. Always run az bicep build and what-if to review changes before deploying to production.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.