Module 7.6: Azure Bicep
Complexity: [MEDIUM]
Section titled “Complexity: [MEDIUM]”Time to Complete: 75 minutes
Section titled “Time to Complete: 75 minutes”Prerequisites
Section titled “Prerequisites”Before starting this module, you should have completed:
- Module 6.1: IaC Fundamentals
- Azure subscription with Contributor access
- Azure CLI installed (
az --version) - Basic understanding of Azure Resource Manager (ARM)
What You’ll Be Able to Do
Section titled “What You’ll Be Able to Do”After completing this module, you will be able to:
- Deploy Azure infrastructure using Bicep templates with modules and parameter files
- Implement Bicep deployment stacks for multi-resource-group and subscription-level deployments
- Configure Bicep with Azure DevOps and GitHub Actions for automated infrastructure pipelines
- Compare Bicep’s Azure-native approach against Terraform for Azure-centric infrastructure teams
Why This Module Matters
Section titled “Why This Module Matters”The ARM template sprawled across 2,847 lines of impenetrable JSON.
When the fintech startup’s platform engineer opened the infrastructure code inherited from their acquisition, she found ARM templates that would make a seasoned developer weep. Nested JSON objects 15 levels deep. Copy-pasted resource definitions with subtle differences. No comments, no documentation, no hope of understanding what it actually deployed.
The team had two choices: continue the suffering or rewrite everything. They chose to rewrite—but not in Terraform. Their Azure-only environment, compliance requirements, and need for same-day Azure feature support pointed to one solution: Bicep.
In three weeks, they converted 47 ARM templates into 12 Bicep modules. Lines of code dropped by 60%. The DevOps team could finally read their infrastructure. New engineers onboarded in days instead of weeks.
Bicep isn’t just “ARM templates made readable.” It’s Azure’s answer to the HCL question: what if infrastructure code could be concise, type-safe, and actually pleasant to write?
This module teaches you Bicep’s syntax, module system, and deployment patterns. You’ll learn why Azure shops increasingly choose Bicep over Terraform—and when you might choose differently.
War Story: The ARM Template That Ate Christmas
Section titled “War Story: The ARM Template That Ate Christmas”Characters:
- Diego: Azure Cloud Architect (7 years experience)
- Team: 4 engineers managing Azure infrastructure
- Stack: 200+ Azure resources across 3 environments
The Incident:
December 23rd. Diego’s team needed to deploy a critical security patch before the holiday freeze. The patch required modifying a single parameter in an ARM template.
Timeline:
10:00 AM: Simple task - update VM SKU parameter Opens main.json (ARM template) 2,847 lines of JSON
10:30 AM: Finds the parameter definition Nested inside... something JSON brackets everywhere
11:00 AM: Makes the change ARM template validation fails "Expected ',' at line 1,847"
11:30 AM: Hunting the missing comma JSON has no comments Can't tell where it broke
12:00 PM: Lunch skipped Found it - a bracket, not a comma
12:30 PM: Deploys to dev Different error: "Resource not found" Template references resources via copy index Copy index math is wrong somewhere
2:00 PM: Realizes the copy loop was modified last month By someone who's now on vacation No documentation
3:00 PM: Diego: "What if we just... rewrote this in Bicep?" Team: "We don't have time" Diego: "The ARM template IS the time sink"
3:30 PM: Starts converting to Bicep ARM JSON: 2,847 lines Bicep: 412 lines (same resources!)
5:00 PM: Bicep template complete Clear variable names Actual comments Type checking catches 3 errors
5:30 PM: Deploys to dev - SUCCESS Deploys to staging - SUCCESS Deploys to prod - SUCCESS
6:00 PM: Holiday freeze begins Team goes home on time
January: All new infrastructure is Bicep ARM templates converted over Q1 Deployment incidents drop 73%What Bicep Fixed:
- Readability: 60% less code, actual syntax highlighting
- Type safety: Catches errors before deployment
- IntelliSense: VS Code knows Azure resource properties
- No string interpolation hell: Clean variable references
- Modules: Reusable, parameterized components
Lessons Learned:
- JSON is not a programming language
- Readability = maintainability = reliability
- “We don’t have time to rewrite” is often false
- Native tools get Azure features first
Bicep vs. ARM Templates: The Evolution
Section titled “Bicep vs. ARM Templates: The Evolution”Why Bicep Exists
Section titled “Why Bicep Exists”┌─────────────────────────────────────────────────────────────────┐│ ARM TEMPLATE EVOLUTION │├─────────────────────────────────────────────────────────────────┤│ ││ 2014: ARM Templates (JSON) ││ ════════════════════════ ││ ││ { ││ "resources": [{ ││ "type": "Microsoft.Storage/storageAccounts", ││ "apiVersion": "2021-09-01", ││ "name": "[parameters('storageAccountName')]", ││ "location": "[resourceGroup().location]", ││ "sku": { ││ "name": "[parameters('storageSKU')]" ││ }, ││ "kind": "StorageV2" ││ }] ││ } ││ ││ ↓ 7 years of pain ↓ ││ ││ 2020: Bicep (Domain-Specific Language) ││ ═══════════════════════════════════════ ││ ││ resource storage 'Microsoft.Storage/storageAccounts@2021-09-01'││ = { ││ name: storageAccountName ││ location: resourceGroup().location ││ sku: { name: storageSKU } ││ kind: 'StorageV2' ││ } ││ ││ ✅ Same deployment engine (ARM) ││ ✅ Transpiles to ARM JSON ││ ✅ Day-0 Azure feature support ││ ✅ Type-safe, IntelliSense ││ │└─────────────────────────────────────────────────────────────────┘Bicep vs. Terraform: Comparison
Section titled “Bicep vs. Terraform: Comparison”| Aspect | Bicep | Terraform |
|---|---|---|
| Cloud support | Azure only | Multi-cloud |
| State management | Azure-managed (ARM) | File-based |
| New Azure features | Day-0 support | Days-to-weeks delay |
| Language | Bicep DSL | HCL |
| Learning curve | Moderate | Moderate |
| IDE support | Excellent (VS Code) | Excellent |
| Community modules | Growing | Extensive |
| Testing frameworks | Limited | Terratest, etc. |
| Deployment | Azure CLI / PowerShell | Terraform CLI |
| Cost | Free | Free (open-source) |
When to choose Bicep:
- Azure-only environment
- Need immediate access to new Azure features
- Team already knows ARM concepts
- Compliance requires Azure-native tooling
- Don’t want to manage state files
When to choose Terraform:
- Multi-cloud environment
- Team has Terraform expertise
- Need extensive community modules
- Complex state manipulation needed
Bicep Fundamentals
Section titled “Bicep Fundamentals”Installation and Setup
Section titled “Installation and Setup”# Install Azure CLI (includes Bicep)curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Verify Bicep installationaz bicep version
# Upgrade Bicep to latestaz bicep upgrade
# Install VS Code extensioncode --install-extension ms-azuretools.vscode-bicepBasic Syntax
Section titled “Basic Syntax”// ============================================// PARAMETERS - Input values// ============================================@description('The Azure region for resources')param location string = resourceGroup().location
@description('Environment name')@allowed([ 'dev' 'staging' 'prod'])param environment string = 'dev'
@description('Storage account name')@minLength(3)@maxLength(24)param storageAccountName string
@secure()@description('Administrator password')param adminPassword string
// ============================================// VARIABLES - Computed values// ============================================var tags = { Environment: environment ManagedBy: 'Bicep' CostCenter: environment == 'prod' ? 'PROD-001' : 'DEV-001'}
var storageSkuName = environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
// ============================================// RESOURCES - Azure resources to deploy// ============================================resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: storageAccountName location: location tags: tags sku: { name: storageSkuName } kind: 'StorageV2' properties: { accessTier: 'Hot' supportsHttpsTrafficOnly: true minimumTlsVersion: 'TLS1_2' allowBlobPublicAccess: false networkAcls: { defaultAction: 'Deny' bypass: 'AzureServices' } }}
resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { parent: storageAccount name: 'default' properties: { deleteRetentionPolicy: { enabled: true days: environment == 'prod' ? 30 : 7 } }}
resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = { parent: blobServices name: 'data' properties: { publicAccess: 'None' }}
// ============================================// OUTPUTS - Values to expose// ============================================output storageAccountId string = storageAccount.idoutput blobEndpoint string = storageAccount.properties.primaryEndpoints.bloboutput storageAccountName string = storageAccount.nameDeploying Bicep
Section titled “Deploying Bicep”# Login to Azureaz login
# Set subscriptionaz account set --subscription "My Subscription"
# Create resource groupaz group create --name myapp-rg --location eastus
# Deploy Bicep fileaz deployment group create \ --resource-group myapp-rg \ --template-file main.bicep \ --parameters storageAccountName=myappstg123 \ environment=dev
# Deploy with parameter fileaz deployment group create \ --resource-group myapp-rg \ --template-file main.bicep \ --parameters @params/dev.bicepparam
# What-if deployment (preview changes)az deployment group what-if \ --resource-group myapp-rg \ --template-file main.bicep \ --parameters @params/dev.bicepparam
# Export outputsaz deployment group show \ --resource-group myapp-rg \ --name main \ --query properties.outputsBicep Modules
Section titled “Bicep Modules”Module Structure
Section titled “Module Structure”infrastructure/├── main.bicep # Entry point├── modules/│ ├── networking/│ │ ├── vnet.bicep│ │ └── nsg.bicep│ ├── compute/│ │ ├── vm.bicep│ │ └── vmss.bicep│ ├── storage/│ │ └── storage.bicep│ └── database/│ └── sql.bicep└── params/ ├── dev.bicepparam ├── staging.bicepparam └── prod.bicepparamCreating a Module
Section titled “Creating a Module”@description('VNet name')param vnetName string
@description('VNet address space')param addressPrefix string = '10.0.0.0/16'
@description('Location')param location string = resourceGroup().location
@description('Subnets configuration')param subnets array = [ { name: 'web' addressPrefix: '10.0.1.0/24' } { name: 'app' addressPrefix: '10.0.2.0/24' } { name: 'data' addressPrefix: '10.0.3.0/24' }]
@description('Tags')param tags object = {}
// Virtual Networkresource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = { name: vnetName location: location tags: tags properties: { addressSpace: { addressPrefixes: [ addressPrefix ] } subnets: [for subnet in subnets: { name: subnet.name properties: { addressPrefix: subnet.addressPrefix privateEndpointNetworkPolicies: 'Disabled' privateLinkServiceNetworkPolicies: 'Enabled' } }] }}
// Outputsoutput vnetId string = vnet.idoutput vnetName string = vnet.nameoutput subnetIds array = [for (subnet, i) in subnets: vnet.properties.subnets[i].id]Using Modules
Section titled “Using Modules”targetScope = 'subscription'
param location string = 'eastus'param environment string = 'dev'
// Create Resource Groupresource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { name: 'myapp-${environment}-rg' location: location tags: { Environment: environment }}
// Deploy networking modulemodule networking 'modules/networking/vnet.bicep' = { scope: rg name: 'networking-deployment' params: { vnetName: 'myapp-${environment}-vnet' location: location addressPrefix: '10.0.0.0/16' subnets: [ { name: 'web', addressPrefix: '10.0.1.0/24' } { name: 'app', addressPrefix: '10.0.2.0/24' } { name: 'data', addressPrefix: '10.0.3.0/24' } ] tags: { Environment: environment } }}
// Deploy storage modulemodule storage 'modules/storage/storage.bicep' = { scope: rg name: 'storage-deployment' params: { storageAccountName: 'myapp${environment}stg${uniqueString(rg.id)}' location: location subnetId: networking.outputs.subnetIds[2] // data subnet }}
// Deploy compute module (depends on networking)module compute 'modules/compute/vm.bicep' = { scope: rg name: 'compute-deployment' params: { vmName: 'myapp-${environment}-vm' location: location subnetId: networking.outputs.subnetIds[1] // app subnet adminUsername: 'azureuser' adminPassword: keyVaultSecret.getSecret('vm-admin-password') } dependsOn: [ networking ]}
// Reference existing Key Vault for secretsresource keyVaultSecret 'Microsoft.KeyVault/vaults@2023-02-01' existing = { name: 'myapp-kv' scope: resourceGroup('myapp-shared-rg')}
output vnetId string = networking.outputs.vnetIdoutput storageEndpoint string = storage.outputs.blobEndpointBicep Registry and Versioning
Section titled “Bicep Registry and Versioning”Publishing to Registry
Section titled “Publishing to Registry”# Create Azure Container Registry for Bicep modulesaz acr create \ --name mycompanybicep \ --resource-group shared-rg \ --sku Basic
# Publish module to registryaz bicep publish \ --file modules/networking/vnet.bicep \ --target br:mycompanybicep.azurecr.io/bicep/modules/networking/vnet:v1.0.0
# Publish with latest tagaz bicep publish \ --file modules/networking/vnet.bicep \ --target br:mycompanybicep.azurecr.io/bicep/modules/networking/vnet:latestUsing Registry Modules
Section titled “Using Registry Modules”// Using module from private registrymodule networking 'br:mycompanybicep.azurecr.io/bicep/modules/networking/vnet:v1.0.0' = { name: 'networking' params: { vnetName: 'myapp-vnet' // ... }}
// Using module from public registry (Microsoft)module appService 'br/public:avm/res/web/site:0.3.0' = { name: 'appService' params: { name: 'myapp-web' // ... }}bicepconfig.json
Section titled “bicepconfig.json”{ "moduleAliases": { "br": { "myregistry": { "registry": "mycompanybicep.azurecr.io" }, "public": { "registry": "mcr.microsoft.com", "modulePath": "bicep" } } }, "analyzers": { "core": { "enabled": true, "rules": { "no-unused-params": { "level": "warning" }, "no-unused-vars": { "level": "warning" }, "prefer-interpolation": { "level": "warning" }, "secure-secrets-in-params": { "level": "error" }, "use-recent-api-versions": { "level": "warning" } } } }}Parameter Files
Section titled “Parameter Files”Bicep Parameter File (.bicepparam)
Section titled “Bicep Parameter File (.bicepparam)”using '../main.bicep'
param location = 'eastus'param environment = 'prod'param storageAccountName = 'myappprodstg'
// Complex objectsparam subnets = [ { name: 'web' addressPrefix: '10.0.1.0/24' nsgRules: [ { name: 'AllowHTTPS' priority: 100 direction: 'Inbound' access: 'Allow' protocol: 'Tcp' destinationPortRange: '443' } ] } { name: 'app' addressPrefix: '10.0.2.0/24' }]
// Reference Key Vault secretsparam adminPassword = az.getSecret('<subscription-id>', 'myapp-kv-rg', 'myapp-kv', 'admin-password')JSON Parameter File (Legacy)
Section titled “JSON Parameter File (Legacy)”{ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "location": { "value": "eastus" }, "environment": { "value": "prod" }, "storageAccountName": { "value": "myappprodstg" }, "adminPassword": { "reference": { "keyVault": { "id": "/subscriptions/.../resourceGroups/myapp-kv-rg/providers/Microsoft.KeyVault/vaults/myapp-kv" }, "secretName": "admin-password" } } }}Advanced Bicep Patterns
Section titled “Advanced Bicep Patterns”Conditional Resources
Section titled “Conditional Resources”param deployDiagnostics bool = trueparam environment string
// Conditional resourceresource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = if (deployDiagnostics) { name: 'myapp-logs' location: resourceGroup().location properties: { retentionInDays: environment == 'prod' ? 90 : 30 sku: { name: 'PerGB2018' } }}
// Use conditional outputoutput logAnalyticsId string = deployDiagnostics ? logAnalytics.id : ''Loops and Iterations
Section titled “Loops and Iterations”param locations array = [ 'eastus' 'westus2' 'westeurope']
param storageAccounts array = [ { name: 'data', sku: 'Standard_LRS' } { name: 'logs', sku: 'Standard_GRS' } { name: 'backup', sku: 'Standard_RAGRS' }]
// Loop with indexresource storageLoop 'Microsoft.Storage/storageAccounts@2023-01-01' = [for (account, i) in storageAccounts: { name: '${account.name}${uniqueString(resourceGroup().id)}${i}' location: resourceGroup().location sku: { name: account.sku } kind: 'StorageV2'}]
// Loop for array outputoutput storageIds array = [for (account, i) in storageAccounts: storageLoop[i].id]
// Nested loopsresource multiRegionStorage 'Microsoft.Storage/storageAccounts@2023-01-01' = [for location in locations: { name: 'stg${uniqueString(location)}' location: location sku: { name: 'Standard_LRS' } kind: 'StorageV2'}]Existing Resources
Section titled “Existing Resources”// Reference existing resource in same resource groupresource existingVnet 'Microsoft.Network/virtualNetworks@2023-05-01' existing = { name: 'myapp-vnet'}
// Reference existing resource in different resource groupresource existingKeyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = { name: 'myapp-kv' scope: resourceGroup('myapp-shared-rg')}
// Reference existing resource in different subscriptionresource existingLogAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { name: 'central-logs' scope: resourceGroup('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'central-monitoring-rg')}
// Use existing resource propertiesresource vm 'Microsoft.Compute/virtualMachines@2023-07-01' = { name: 'myapp-vm' location: resourceGroup().location properties: { networkProfile: { networkInterfaces: [ { id: existingNic.id } ] } }}Resource Dependencies
Section titled “Resource Dependencies”// Implicit dependency (using resource reference)resource nic 'Microsoft.Network/networkInterfaces@2023-05-01' = { name: 'myapp-nic' location: resourceGroup().location properties: { ipConfigurations: [ { name: 'ipconfig1' properties: { subnet: { id: vnet.properties.subnets[0].id // Implicit dependency } } } ] }}
// Explicit dependencyresource vm 'Microsoft.Compute/virtualMachines@2023-07-01' = { name: 'myapp-vm' location: resourceGroup().location dependsOn: [ nic diagnosticsExtension // Must wait for extension ] // ...}Deployment Scopes
Section titled “Deployment Scopes”Different Deployment Levels
Section titled “Different Deployment Levels”// Resource Group scope (default)// targetScope = 'resourceGroup' // implicit
resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = { // ...}// Subscription scopetargetScope = 'subscription'
resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { name: 'myapp-rg' location: 'eastus'}
// Deploy to the new resource groupmodule storage 'modules/storage.bicep' = { scope: rg name: 'storage' params: { // ... }}// Management Group scopetargetScope = 'managementGroup'
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = { name: 'require-tags' properties: { policyType: 'Custom' mode: 'Indexed' // ... }}// Tenant scopetargetScope = 'tenant'
resource managementGroup 'Microsoft.Management/managementGroups@2021-04-01' = { name: 'mycompany-mg' properties: { displayName: 'My Company Management Group' }}CI/CD with Bicep
Section titled “CI/CD with Bicep”GitHub Actions Pipeline
Section titled “GitHub Actions Pipeline”name: Deploy Bicep
on: push: branches: [main] paths: - 'infrastructure/**' pull_request: branches: [main] paths: - 'infrastructure/**'
permissions: id-token: write contents: read pull-requests: write
jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Azure Login uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Bicep Build run: az bicep build --file infrastructure/main.bicep
- name: Run Bicep Linter run: | az bicep lint --file infrastructure/main.bicep
- name: Validate Template run: | az deployment sub validate \ --location eastus \ --template-file infrastructure/main.bicep \ --parameters @infrastructure/params/dev.bicepparam
what-if: needs: validate runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Azure Login uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: What-If id: whatif run: | az deployment sub what-if \ --location eastus \ --template-file infrastructure/main.bicep \ --parameters @infrastructure/params/dev.bicepparam \ --no-pretty-print > whatif.txt
- name: Comment PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const whatif = fs.readFileSync('whatif.txt', 'utf8');
github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `## Bicep What-If Results\n\`\`\`\n${whatif}\n\`\`\`` });
deploy: needs: what-if if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4
- name: Azure Login uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy run: | az deployment sub create \ --location eastus \ --template-file infrastructure/main.bicep \ --parameters @infrastructure/params/prod.bicepparam \ --name "deploy-${{ github.sha }}"Azure DevOps Pipeline
Section titled “Azure DevOps Pipeline”trigger: branches: include: - main paths: include: - infrastructure/*
pool: vmImage: ubuntu-latest
stages: - stage: Validate jobs: - job: ValidateBicep steps: - task: AzureCLI@2 displayName: Bicep Build inputs: azureSubscription: 'Azure-Connection' scriptType: bash scriptLocation: inlineScript inlineScript: | az bicep build --file infrastructure/main.bicep
- task: AzureCLI@2 displayName: Validate Deployment inputs: azureSubscription: 'Azure-Connection' scriptType: bash scriptLocation: inlineScript inlineScript: | az deployment sub validate \ --location eastus \ --template-file infrastructure/main.bicep \ --parameters @infrastructure/params/$(Environment).bicepparam
- stage: WhatIf dependsOn: Validate jobs: - job: WhatIf steps: - task: AzureCLI@2 displayName: What-If Analysis inputs: azureSubscription: 'Azure-Connection' scriptType: bash scriptLocation: inlineScript inlineScript: | az deployment sub what-if \ --location eastus \ --template-file infrastructure/main.bicep \ --parameters @infrastructure/params/$(Environment).bicepparam
- stage: Deploy dependsOn: WhatIf condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) jobs: - deployment: DeployBicep environment: production strategy: runOnce: deploy: steps: - checkout: self - task: AzureCLI@2 displayName: Deploy Bicep inputs: azureSubscription: 'Azure-Connection' scriptType: bash scriptLocation: inlineScript inlineScript: | az deployment sub create \ --location eastus \ --template-file infrastructure/main.bicep \ --parameters @infrastructure/params/prod.bicepparamCommon Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Solution |
|---|---|---|
| Hardcoded resource names | Name conflicts, not reusable | Use uniqueString() and parameters |
| No API version pinning | Breaking changes on upgrade | Pin specific API versions in bicepconfig.json |
| Secrets in parameters | Credentials exposed in deployment history | Use Key Vault references |
| Missing dependsOn | Resources created before dependencies | Use resource references (implicit) or explicit dependsOn |
| No what-if before deploy | Unexpected changes in production | Always run az deployment what-if first |
| Ignoring linter warnings | Potential bugs or security issues | Configure bicepconfig.json rules as errors |
| Copy-pasting resources | Inconsistency, maintenance burden | Create modules for reusable components |
| No output values | Can’t use resources from other templates | Add outputs for IDs, endpoints, names |
| Wrong scope | Resources created in wrong location | Set targetScope and use scope parameter |
| Not using existing resources | Recreating resources that already exist | Use existing keyword for references |
Test your Bicep knowledge:
1. What is the relationship between Bicep and ARM templates?
Answer: Bicep is a domain-specific language (DSL) that transpiles to ARM JSON templates. When you deploy Bicep:
- Bicep CLI converts .bicep to ARM JSON
- ARM JSON is sent to Azure Resource Manager
- ARM deploys the resources
Benefits:
- Same deployment engine = same capabilities
- Day-0 support for new Azure features
- Can decompile ARM JSON to Bicep
- State managed by Azure (no state files)
# See the generated ARM templateaz bicep build --file main.bicep --stdout2. How do you reference secrets from Key Vault in Bicep?
Answer: Two methods:
Method 1: In parameter file (.bicepparam)
param adminPassword = az.getSecret( 'subscription-id', 'keyvault-rg', 'keyvault-name', 'secret-name')Method 2: Using existing resource
resource kv 'Microsoft.KeyVault/vaults@2023-02-01' existing = { name: 'mykeyvault' scope: resourceGroup('keyvault-rg')}
module vm 'vm.bicep' = { params: { adminPassword: kv.getSecret('vm-password') }}Secrets are never exposed in deployment history with these methods.
3. What is the difference between implicit and explicit dependencies?
Answer:
Implicit dependency (preferred): Bicep automatically creates dependencies when you reference another resource:
resource nic 'Microsoft.Network/networkInterfaces@2023-05-01' = { properties: { ipConfigurations: [{ properties: { subnet: { id: vnet.properties.subnets[0].id // Implicit dependency on vnet } } }] }}Explicit dependency: Use dependsOn when there’s no property reference:
resource extension 'Microsoft.Compute/virtualMachines/extensions@2023-07-01' = { parent: vm dependsOn: [ storageAccount // VM extension needs storage but doesn't reference it ]}Prefer implicit dependencies—they’re cleaner and self-documenting.
4. How do you deploy resources to multiple regions with Bicep?
Answer: Use loops over a locations array:
param locations array = ['eastus', 'westeurope', 'southeastasia']
resource storageAccounts 'Microsoft.Storage/storageAccounts@2023-01-01' = [for location in locations: { name: 'stg${uniqueString(resourceGroup().id, location)}' location: location sku: { name: 'Standard_LRS' } kind: 'StorageV2'}]
output storageEndpoints array = [for (location, i) in locations: { location: location endpoint: storageAccounts[i].properties.primaryEndpoints.blob}]For more complex scenarios, use modules with scope to deploy to different resource groups:
module regionalDeployment 'regional.bicep' = [for location in locations: { scope: resourceGroup('myapp-${location}-rg') name: 'deploy-${location}' params: { location: location }}]5. What is the purpose of `what-if` deployment?
Answer: What-if shows what changes Azure would make without actually making them—similar to Terraform plan:
az deployment group what-if \ --resource-group myapp-rg \ --template-file main.bicep \ --parameters @params/prod.bicepparamOutput shows:
- Create: New resources to be created
- Delete: Resources to be removed
- Modify: Properties that will change
- NoChange: Resources unchanged
Always run what-if before production deployments to catch unexpected changes.
6. How do you use Bicep modules from a registry?
Answer:
Publish module:
az bicep publish \ --file modules/vnet.bicep \ --target br:myregistry.azurecr.io/bicep/modules/vnet:v1.0.0Use module:
module networking 'br:myregistry.azurecr.io/bicep/modules/vnet:v1.0.0' = { name: 'networking' params: { vnetName: 'myapp-vnet' }}Configure alias in bicepconfig.json:
{ "moduleAliases": { "br": { "mymodules": { "registry": "myregistry.azurecr.io", "modulePath": "bicep/modules" } } }}Then use: module vnet 'br/mymodules:vnet:v1.0.0'
7. What are the different deployment scopes in Bicep?
Answer: Four deployment scopes:
-
resourceGroup (default): Deploy to a resource group
// targetScope = 'resourceGroup' // implicitresource storage 'Microsoft.Storage/...' = { } -
subscription: Deploy resource groups, policies
targetScope = 'subscription'resource rg 'Microsoft.Resources/resourceGroups@...' = { } -
managementGroup: Deploy policies across subscriptions
targetScope = 'managementGroup'resource policy 'Microsoft.Authorization/policyDefinitions@...' = { } -
tenant: Deploy management groups
targetScope = 'tenant'resource mg 'Microsoft.Management/managementGroups@...' = { }
8. How do you reference existing resources that weren't created by your template?
Answer: Use the existing keyword:
// Same resource groupresource existingVnet 'Microsoft.Network/virtualNetworks@2023-05-01' existing = { name: 'my-existing-vnet'}
// Different resource groupresource existingKeyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = { name: 'my-keyvault' scope: resourceGroup('other-rg')}
// Different subscriptionresource existingStorage 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { name: 'mystorageaccount' scope: resourceGroup('sub-id', 'storage-rg')}
// Use existing resourceresource newSubnet 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = { parent: existingVnet name: 'new-subnet' properties: { addressPrefix: '10.0.99.0/24' }}Key Takeaways
Section titled “Key Takeaways”- Bicep transpiles to ARM: Same deployment engine, cleaner syntax
- Day-0 Azure support: New Azure features work immediately
- No state files: Azure manages state—no corruption risk
- Type-safe with IntelliSense: VS Code catches errors before deployment
- Modules for reusability: Build a library of composable components
- Registry for sharing: Publish versioned modules to Azure Container Registry
- What-if before deploy: Always preview changes before production
- Scopes control where: resourceGroup, subscription, managementGroup, tenant
- Existing keyword: Reference resources created outside your template
- Parameter files: Separate configuration from code
Did You Know?
Section titled “Did You Know?”-
Bicep was created by a single engineer (Anthony Martin) at Microsoft who was frustrated with ARM template JSON. It started as a side project before becoming an official Azure product.
-
You can decompile ARM to Bicep with
az bicep decompile --file template.json. This makes migration from existing ARM templates straightforward (though manual cleanup is often needed). -
The name “Bicep” is a playful reference to ARM (Azure Resource Manager)—biceps are part of your arm. Microsoft’s naming humor at work.
-
Azure Verified Modules (AVM) is Microsoft’s official repository of production-ready Bicep modules. These are maintained by Microsoft and follow strict quality standards—check them before writing your own modules.
Hands-On Exercise
Section titled “Hands-On Exercise”Exercise: Multi-Environment Deployment with Modules
Section titled “Exercise: Multi-Environment Deployment with Modules”Objective: Create a modular Bicep deployment for a web application with environment-specific configurations.
Tasks:
- Create the project structure:
mkdir -p bicep-lab/{modules,params}cd bicep-lab- Create a networking module:
param vnetName stringparam location string = resourceGroup().locationparam addressPrefix string = '10.0.0.0/16'param tags object = {}
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = { name: vnetName location: location tags: tags properties: { addressSpace: { addressPrefixes: [addressPrefix] } subnets: [ { name: 'web' properties: { addressPrefix: '10.0.1.0/24' } } { name: 'app' properties: { addressPrefix: '10.0.2.0/24' } } ] }}
output vnetId string = vnet.idoutput webSubnetId string = vnet.properties.subnets[0].id- Create main template:
targetScope = 'subscription'
param location string = 'eastus'param environment string
resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { name: 'webapp-${environment}-rg' location: location}
module networking 'modules/networking.bicep' = { scope: rg name: 'networking' params: { vnetName: 'webapp-${environment}-vnet' location: location tags: { Environment: environment } }}- Create parameter files:
using '../main.bicep'param location = 'eastus'param environment = 'dev'- Validate and deploy:
# Validateaz deployment sub validate \ --location eastus \ --template-file main.bicep \ --parameters @params/dev.bicepparam
# What-ifaz deployment sub what-if \ --location eastus \ --template-file main.bicep \ --parameters @params/dev.bicepparam
# Deployaz deployment sub create \ --location eastus \ --template-file main.bicep \ --parameters @params/dev.bicepparamSuccess Criteria:
- Bicep builds without errors
- Linter passes with no warnings
- What-if shows expected resources
- Deployment completes successfully
- Resources tagged correctly
Next Steps
Section titled “Next Steps”You’ve completed the IaC Toolkit! Continue your learning:
- Module 6.1: IaC Fundamentals - Review core concepts
- Platform Engineering Track - Apply IaC in platform contexts
- GitOps Discipline - Combine IaC with GitOps practices