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):
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
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
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
✅ Correct: parameters
❌ Wrong: secrets in Bicep
✅ Correct: secure + KV
❌ 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.