Skip to content

Module 7.6: Azure Bicep


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)

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

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:

  1. Readability: 60% less code, actual syntax highlighting
  2. Type safety: Catches errors before deployment
  3. IntelliSense: VS Code knows Azure resource properties
  4. No string interpolation hell: Clean variable references
  5. Modules: Reusable, parameterized components

Lessons Learned:

  1. JSON is not a programming language
  2. Readability = maintainability = reliability
  3. “We don’t have time to rewrite” is often false
  4. Native tools get Azure features first

┌─────────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└─────────────────────────────────────────────────────────────────┘
AspectBicepTerraform
Cloud supportAzure onlyMulti-cloud
State managementAzure-managed (ARM)File-based
New Azure featuresDay-0 supportDays-to-weeks delay
LanguageBicep DSLHCL
Learning curveModerateModerate
IDE supportExcellent (VS Code)Excellent
Community modulesGrowingExtensive
Testing frameworksLimitedTerratest, etc.
DeploymentAzure CLI / PowerShellTerraform CLI
CostFreeFree (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

Terminal window
# Install Azure CLI (includes Bicep)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Verify Bicep installation
az bicep version
# Upgrade Bicep to latest
az bicep upgrade
# Install VS Code extension
code --install-extension ms-azuretools.vscode-bicep
main.bicep
// ============================================
// 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.id
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob
output storageAccountName string = storageAccount.name
Terminal window
# Login to Azure
az login
# Set subscription
az account set --subscription "My Subscription"
# Create resource group
az group create --name myapp-rg --location eastus
# Deploy Bicep file
az deployment group create \
--resource-group myapp-rg \
--template-file main.bicep \
--parameters storageAccountName=myappstg123 \
environment=dev
# Deploy with parameter file
az 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 outputs
az deployment group show \
--resource-group myapp-rg \
--name main \
--query properties.outputs

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.bicepparam
modules/networking/vnet.bicep
@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 Network
resource 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'
}
}]
}
}
// Outputs
output vnetId string = vnet.id
output vnetName string = vnet.name
output subnetIds array = [for (subnet, i) in subnets: vnet.properties.subnets[i].id]
main.bicep
targetScope = 'subscription'
param location string = 'eastus'
param environment string = 'dev'
// Create Resource Group
resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
name: 'myapp-${environment}-rg'
location: location
tags: {
Environment: environment
}
}
// Deploy networking module
module 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 module
module 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 secrets
resource keyVaultSecret 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
name: 'myapp-kv'
scope: resourceGroup('myapp-shared-rg')
}
output vnetId string = networking.outputs.vnetId
output storageEndpoint string = storage.outputs.blobEndpoint

Terminal window
# Create Azure Container Registry for Bicep modules
az acr create \
--name mycompanybicep \
--resource-group shared-rg \
--sku Basic
# Publish module to registry
az bicep publish \
--file modules/networking/vnet.bicep \
--target br:mycompanybicep.azurecr.io/bicep/modules/networking/vnet:v1.0.0
# Publish with latest tag
az bicep publish \
--file modules/networking/vnet.bicep \
--target br:mycompanybicep.azurecr.io/bicep/modules/networking/vnet:latest
main.bicep
// Using module from private registry
module 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'
// ...
}
}
{
"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"
}
}
}
}
}

params/prod.bicepparam
using '../main.bicep'
param location = 'eastus'
param environment = 'prod'
param storageAccountName = 'myappprodstg'
// Complex objects
param 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 secrets
param adminPassword = az.getSecret('<subscription-id>', 'myapp-kv-rg', 'myapp-kv', 'admin-password')
{
"$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"
}
}
}
}

param deployDiagnostics bool = true
param environment string
// Conditional resource
resource 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 output
output logAnalyticsId string = deployDiagnostics ? logAnalytics.id : ''
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 index
resource 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 output
output storageIds array = [for (account, i) in storageAccounts: storageLoop[i].id]
// Nested loops
resource multiRegionStorage 'Microsoft.Storage/storageAccounts@2023-01-01' = [for location in locations: {
name: 'stg${uniqueString(location)}'
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}]
// Reference existing resource in same resource group
resource existingVnet 'Microsoft.Network/virtualNetworks@2023-05-01' existing = {
name: 'myapp-vnet'
}
// Reference existing resource in different resource group
resource existingKeyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
name: 'myapp-kv'
scope: resourceGroup('myapp-shared-rg')
}
// Reference existing resource in different subscription
resource existingLogAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = {
name: 'central-logs'
scope: resourceGroup('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'central-monitoring-rg')
}
// Use existing resource properties
resource vm 'Microsoft.Compute/virtualMachines@2023-07-01' = {
name: 'myapp-vm'
location: resourceGroup().location
properties: {
networkProfile: {
networkInterfaces: [
{
id: existingNic.id
}
]
}
}
}
// 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 dependency
resource vm 'Microsoft.Compute/virtualMachines@2023-07-01' = {
name: 'myapp-vm'
location: resourceGroup().location
dependsOn: [
nic
diagnosticsExtension // Must wait for extension
]
// ...
}

// Resource Group scope (default)
// targetScope = 'resourceGroup' // implicit
resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
// ...
}
// Subscription scope
targetScope = 'subscription'
resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
name: 'myapp-rg'
location: 'eastus'
}
// Deploy to the new resource group
module storage 'modules/storage.bicep' = {
scope: rg
name: 'storage'
params: {
// ...
}
}
// Management Group scope
targetScope = 'managementGroup'
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: 'require-tags'
properties: {
policyType: 'Custom'
mode: 'Indexed'
// ...
}
}
// Tenant scope
targetScope = 'tenant'
resource managementGroup 'Microsoft.Management/managementGroups@2021-04-01' = {
name: 'mycompany-mg'
properties: {
displayName: 'My Company Management Group'
}
}

.github/workflows/bicep-deploy.yml
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-pipelines.yml
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.bicepparam

MistakeProblemSolution
Hardcoded resource namesName conflicts, not reusableUse uniqueString() and parameters
No API version pinningBreaking changes on upgradePin specific API versions in bicepconfig.json
Secrets in parametersCredentials exposed in deployment historyUse Key Vault references
Missing dependsOnResources created before dependenciesUse resource references (implicit) or explicit dependsOn
No what-if before deployUnexpected changes in productionAlways run az deployment what-if first
Ignoring linter warningsPotential bugs or security issuesConfigure bicepconfig.json rules as errors
Copy-pasting resourcesInconsistency, maintenance burdenCreate modules for reusable components
No output valuesCan’t use resources from other templatesAdd outputs for IDs, endpoints, names
Wrong scopeResources created in wrong locationSet targetScope and use scope parameter
Not using existing resourcesRecreating resources that already existUse 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:

  1. Bicep CLI converts .bicep to ARM JSON
  2. ARM JSON is sent to Azure Resource Manager
  3. 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)
Terminal window
# See the generated ARM template
az bicep build --file main.bicep --stdout
2. 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:

Terminal window
az deployment group what-if \
--resource-group myapp-rg \
--template-file main.bicep \
--parameters @params/prod.bicepparam

Output 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:

Terminal window
az bicep publish \
--file modules/vnet.bicep \
--target br:myregistry.azurecr.io/bicep/modules/vnet:v1.0.0

Use 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:

  1. resourceGroup (default): Deploy to a resource group

    // targetScope = 'resourceGroup' // implicit
    resource storage 'Microsoft.Storage/...' = { }
  2. subscription: Deploy resource groups, policies

    targetScope = 'subscription'
    resource rg 'Microsoft.Resources/resourceGroups@...' = { }
  3. managementGroup: Deploy policies across subscriptions

    targetScope = 'managementGroup'
    resource policy 'Microsoft.Authorization/policyDefinitions@...' = { }
  4. 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 group
resource existingVnet 'Microsoft.Network/virtualNetworks@2023-05-01' existing = {
name: 'my-existing-vnet'
}
// Different resource group
resource existingKeyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
name: 'my-keyvault'
scope: resourceGroup('other-rg')
}
// Different subscription
resource existingStorage 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
name: 'mystorageaccount'
scope: resourceGroup('sub-id', 'storage-rg')
}
// Use existing resource
resource newSubnet 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = {
parent: existingVnet
name: 'new-subnet'
properties: {
addressPrefix: '10.0.99.0/24'
}
}

  1. Bicep transpiles to ARM: Same deployment engine, cleaner syntax
  2. Day-0 Azure support: New Azure features work immediately
  3. No state files: Azure manages state—no corruption risk
  4. Type-safe with IntelliSense: VS Code catches errors before deployment
  5. Modules for reusability: Build a library of composable components
  6. Registry for sharing: Publish versioned modules to Azure Container Registry
  7. What-if before deploy: Always preview changes before production
  8. Scopes control where: resourceGroup, subscription, managementGroup, tenant
  9. Existing keyword: Reference resources created outside your template
  10. Parameter files: Separate configuration from code

  1. 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.

  2. 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).

  3. The name “Bicep” is a playful reference to ARM (Azure Resource Manager)—biceps are part of your arm. Microsoft’s naming humor at work.

  4. 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.


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:

  1. Create the project structure:
Terminal window
mkdir -p bicep-lab/{modules,params}
cd bicep-lab
  1. Create a networking module:
modules/networking.bicep
param vnetName string
param location string = resourceGroup().location
param 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.id
output webSubnetId string = vnet.properties.subnets[0].id
  1. Create main template:
main.bicep
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
}
}
}
  1. Create parameter files:
params/dev.bicepparam
using '../main.bicep'
param location = 'eastus'
param environment = 'dev'
  1. Validate and deploy:
Terminal window
# Validate
az deployment sub validate \
--location eastus \
--template-file main.bicep \
--parameters @params/dev.bicepparam
# What-if
az deployment sub what-if \
--location eastus \
--template-file main.bicep \
--parameters @params/dev.bicepparam
# Deploy
az deployment sub create \
--location eastus \
--template-file main.bicep \
--parameters @params/dev.bicepparam

Success Criteria:

  • Bicep builds without errors
  • Linter passes with no warnings
  • What-if shows expected resources
  • Deployment completes successfully
  • Resources tagged correctly

You’ve completed the IaC Toolkit! Continue your learning: