Skip to content

Bicep & IaC

Key Points

  • IaC choices for Azure: Bicep (Microsoft DSL; ARM under the hood; native), Terraform (multi-cloud; HashiCorp; mature), Pulumi (real programming languages; .NET/TS/Python).
  • Bicep is the modern Azure default: clean syntax, no state file (uses Azure as state), first-party support, free.
  • Modules group resources; parameters + variables + outputs make them composable.
  • What-if previews changes before deploy. Validate without deploying.
  • For multi-cloud or rich abstractions, Terraform/Pulumi. Otherwise Bicep wins on Azure.

Concepts (deep dive)

What Bicep is (and how it relates to ARM and Terraform)

Bicep is a domain-specific language that compiles to ARM JSON. Under the hood, every Bicep deployment becomes an ARM template that Azure Resource Manager executes — the same engine, the same idempotent reconciliation, the same role assignments. Bicep exists because raw ARM JSON is unmaintainable at any non-trivial scale: 5000-line files, no type system, painful module composition, no looping primitives. Bicep gives you a real DSL on top (types, modules, conditionals, loops, output references) while keeping the runtime semantics of ARM.

   you write                       transpiled to                 Azure runs
   ─────────                       ─────────────                 ──────────
   main.bicep            ──►       main.json (ARM)        ──►    ARM engine
   modules/*.bicep                 (intermediate)                reconciles desired
   .bicepparam                                                   state into the
                                                                 subscription

Bicep vs Terraform on Azure:

  • Bicep wins when you're Azure-only, want first-party support, want no state file to manage (ARM tracks state implicitly), and want zero day-1 surface for new Azure features (Bicep usually has the new resource type the day it's announced).
  • Terraform wins when you span multiple clouds, when your team already has TF expertise, or when you want one tool for everything-as-code (Azure + AWS + Datadog + GitHub + Cloudflare). See Terraform with Azure.

The choice is rarely "Bicep is better than Terraform"; it's "what trade-off matches our shop." A greenfield Azure-only .NET app: Bicep. A multi-cloud platform team with TF expertise: Terraform.

What "no state file" actually means

In Terraform, the state file (terraform.tfstate) is the source of truth Terraform consults to compute drift — it maps your HCL to real cloud resource IDs and metadata. It's a real artifact you have to store somewhere (usually a blob with locking), back up, and protect (it contains secrets in plaintext).

Bicep doesn't have an equivalent because Azure Resource Manager is the state. Every deployment writes a "deployment record" to the resource group; Bicep's what-if reads the live cloud and computes the diff there. The trade-off: ARM only knows what's in Azure; Terraform's state can track non-Azure resources (DNS at Cloudflare, GitHub repos, Datadog monitors) in the same plan. For pure-Azure stacks, Bicep's no-state model is one fewer thing to operate.

Bicep basics

param appName string
param location string = resourceGroup().location
@allowed(['dev', 'test', 'prod'])
param env string

var planName = 'asp-${appName}-${env}'

resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: planName
  location: location
  sku: { name: 'P1v3' }
}

resource site 'Microsoft.Web/sites@2023-12-01' = {
  name: 'web-${appName}-${env}'
  location: location
  properties: { serverFarmId: plan.id, httpsOnly: true }
  identity: { type: 'SystemAssigned' }
}

output appUrl string = 'https://${site.properties.defaultHostName}'
az deployment group create -g rg --template-file main.bicep --parameters env=prod
az deployment group what-if -g rg --template-file main.bicep --parameters env=prod

Modules

module storage 'modules/storage.bicep' = {
  name: 'storageDeploy'
  params: { name: 'mystg' }
}

output storageUri string = storage.outputs.uri

Encapsulate reusable patterns. Share via Bicep Registry (ACR).

Conditions, loops

resource diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (env == 'prod') {
  // ...
}

resource subnets 'Microsoft.Network/virtualNetworks/subnets@2023-09-01' = [for s in subnets: {
  parent: vnet
  name: s.name
  properties: { addressPrefix: s.cidr }
}]

Parameters file

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "appName": { "value": "myapp" },
    "env": { "value": "prod" }
  }
}

Or .bicepparam (typed):

using 'main.bicep'

param appName = 'myapp'
param env = 'prod'

Existing resources

resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: 'mykv'
  scope: resourceGroup('shared-rg')
}

output kvUri string = kv.properties.vaultUri

Reference resources without managing them.

Subscription / tenant scope

targetScope = 'subscription'

resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: 'myrg'
  location: 'eastus'
}

Or targetScope = 'tenant', 'managementGroup'.

What-if

az deployment group what-if -g rg --template-file main.bicep

Shows: created, modified, deleted, unchanged, ignored, no-effect.

Terraform

resource "azurerm_resource_group" "rg" {
  name     = "myrg"
  location = "East US"
}

resource "azurerm_app_service" "app" {
  name                = "myapp"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  app_service_plan_id = azurerm_app_service_plan.plan.id
}

State file (.tfstate) tracks deployed state. Backend: Azure Storage, S3, Terraform Cloud.

Pulumi

using Pulumi;
using Pulumi.AzureNative.Resources;

return await Deployment.RunAsync(() =>
{
    var rg = new ResourceGroup("rg");
    var sa = new StorageAccount("stg", new() { ResourceGroupName = rg.Name, /* ... */ });
});

Real C# (or TS, Python). State similar to Terraform.

Bicep vs Terraform vs Pulumi

Aspect Bicep Terraform Pulumi
Syntax DSL DSL Code
State Azure-native File File
Multi-cloud No Yes Yes
.NET native Yes No Yes
Maturity Mid (3+ yrs) High Mid
Cost Free Free / Cloud Free / Cloud

For Azure-only: Bicep. For multi-cloud or polyglot: Terraform. For rich abstractions in code: Pulumi.

Azure Verified Modules (AVM)

Microsoft-curated Bicep / Terraform modules. Pre-built best practices. Use them — don't reinvent.

module avmStorage 'br/public:avm/res/storage/storage-account:0.9.0' = {
  name: 'stg'
  params: { name: 'mystg', location: location }
}

Tagging

resource site 'Microsoft.Web/sites@2023-12-01' = {
  tags: {
    Environment: env
    CostCenter: '1234'
    Owner: 'team-x'
  }
}

Tags drive cost reporting, governance, automation. Standardize early.

Policy

Azure Policy enforces rules:

resource policy 'Microsoft.Authorization/policyAssignments@2023-04-01' = {
  // require 'Environment' tag
}

Prevents drift; enforces compliance.

CI/CD

- name: Bicep Lint
  run: az bicep lint --file main.bicep
- name: What-if
  run: az deployment group what-if -g $RG --template-file main.bicep
- name: Deploy
  run: az deployment group create -g $RG --template-file main.bicep

Or azd up for Aspire-driven deploys.

Secrets in Bicep

@secure()
param dbPassword string

Never echo secure params. Or pull from Key Vault:

resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: 'mykv'
}

resource site '...' = {
  properties: {
    siteConfig: {
      appSettings: [
        { name: 'DbPwd', value: '@Microsoft.KeyVault(SecretUri=...)' }
      ]
    }
  }
}

Code: correct vs wrong

❌ Wrong: hard-coded values

resource site '...' = { location: 'eastus' }

✅ Correct: parameters

param location string = resourceGroup().location

❌ Wrong: secrets in Bicep

param dbPassword string = 'hunter2'

✅ Correct: secure + KV

@secure() param dbPassword string

❌ Wrong: no what-if before deploy

Surprise deletions.

✅ Correct: what-if in CI

Review changes before apply.


Design patterns for this topic

Pattern 1 — "Modules for reuse"

  • Intent: DRY templates.

Pattern 2 — "Parameters + bicepparam files"

  • Intent: typed; per-env config.

Pattern 3 — "What-if in CI"

  • Intent: preview before deploy.

Pattern 4 — "Azure Verified Modules"

  • Intent: best-practice templates.

Pattern 5 — "Standardized tags"

  • Intent: cost + governance.

Pros & cons / trade-offs

Tool Pros Cons
Bicep Azure-native; free; clean Azure-only
Terraform Multi-cloud; mature State complexity
Pulumi Real code Niche
ARM (raw) Lowest level Verbose JSON

When to use / when to avoid

  • Use Bicep for Azure-only.
  • Use Terraform for multi-cloud.
  • Use Pulumi for code-as-IaC fans.
  • Avoid raw ARM JSON.

Interview Q&A

Q1. Bicep vs Terraform? Bicep: Azure-native, free, no state file. Terraform: multi-cloud; state file; richer ecosystem.

Q2. Where does Bicep state live? Azure tracks deployment state. No .tfstate-equivalent.

Q3. What-if? Previews changes (create/update/delete) before apply. Avoids surprises.

Q4. Modules? Reusable groups of resources. Share via Bicep Registry (ACR).

Q5. .bicepparam? Typed parameter file. Bicep-flavored alternative to JSON params.

Q6. Azure Verified Modules? MS-curated modules. Best practices baked in. Use over hand-rolled.

Q7. Secure params? @secure(). Not echoed in deployment output.

Q8. Existing resource? existing keyword. Reference without managing.

Q9. Target scopes? resourceGroup (default), subscription, tenant, managementGroup.

Q10. Tags strategy? Environment, CostCenter, Owner. Standardize early; enforce via Azure Policy.

Q11. Drift? Resource changed outside Bicep → next deploy reverts. What-if shows diff.

Q12. Pulumi advantage? Real programming language. Loops, conditionals, abstractions natural in C#/TS.


Gotchas / common mistakes

  • ⚠️ Hardcoded values instead of parameters.
  • ⚠️ Secrets in plain text params.
  • ⚠️ No what-if in CI.
  • ⚠️ No tagging strategy — chaos at scale.
  • ⚠️ Re-inventing AVM modules.

Further reading