Level: Intermediate
Estimated duration: 4–5 hours
Last updated: June 2026
Table of Contents
- Introduction to IaC with Azure
- Module 1 – ARM Templates and Bicep
- Module 2 – Deployments with Azure CLI and PowerShell
- Module 3 – What-if and Change Tracking
- Module 4 – Testing and Drift Detection
- Complete Code Examples
- Comparison Tables
- Glossary
Introduction to IaC with Azure
What is Infrastructure as Code?
Infrastructure as Code (IaC) is the practice of defining and managing IT infrastructure (servers, networks, databases, etc.) through source code rather than through graphical interfaces or manual processes. This approach brings:
- Reproducibility: deploy exactly the same infrastructure again and again
- Versioning: change history in Git, rollback possible
- Collaboration: infrastructure changes go through Pull Requests
- Automation: integration into CI/CD pipelines
- Living documentation: the code is the documentation of the infrastructure
- Drift elimination: the state declared in code must match the real state
IaC Workflow with Azure
flowchart LR
subgraph CODE["IaC Development"]
BICEP[".bicep file\nor ARM JSON"]
PARAMS["Parameter\nfile"]
GIT["Git Repository\nVersioning"]
end
subgraph VALIDATION["Validation"]
LINT["az bicep lint\nBicep Linter"]
TTK["ARM-TTK\nBest practices"]
WHATIF["What-if\nDry run"]
end
subgraph DEPLOY["Deployment"]
ARM["Azure Resource\nManager API"]
PROVIDERS["Resource\nProviders"]
RESOURCES["Azure Resources\ncreated"]
end
subgraph MONITOR["Monitoring"]
DRIFT["Drift Detection\nExport + Compare"]
CHANGE["Change Analysis\nHistory"]
end
CODE --> VALIDATION
VALIDATION --> DEPLOY
DEPLOY --> MONITOR
MONITOR -->|Feedback| CODE
style CODE fill:#1e3a5f,color:#fff
style VALIDATION fill:#f59e0b,color:#000
style DEPLOY fill:#10b981,color:#fff
style MONITOR fill:#3b82f6,color:#fff
ARM vs Bicep vs Terraform: When to use which?
| Criterion | ARM (JSON) | Bicep | Terraform |
|---|---|---|---|
| Readability | ⭐ Low | ⭐⭐⭐⭐ Excellent | ⭐⭐⭐ Good |
| Azure native | ✅ 100% native | ✅ 100% native | ⚠️ Via providers |
| Multi-cloud | ❌ Azure only | ❌ Azure only | ✅ AWS, GCP, Azure… |
| Learning curve | ⭐⭐⭐⭐ High | ⭐⭐ Moderate | ⭐⭐⭐ Moderate |
| Tooling | Built-in Azure | Az CLI, VS Code ext. | Terraform CLI |
| State management | Azure RM manages all | Azure RM manages all | .tfstate file |
| Reusable modules | Linked (nested/linked) | Modules + AVM | Modules Registry |
| Recommendation | Legacy, migration | ✅ New Azure projects | Multi-cloud |
Module 1 – ARM Templates and Bicep
ARM Template Structure (JSON)
An ARM template is a JSON file that describes Azure infrastructure declaratively. Azure Resource Manager reads this file and orchestrates resource creation.
graph TD
subgraph ARM["ARM Template Structure"]
SCHEMA["$schema\nDefines JSON format\nand template version"]
PARAMS["parameters\nValues entered at deployment\n(environment, region, names)"]
VARS["variables\nCalculated values\n(based on params/functions)"]
FUNCS["functions\nUser-Defined Functions\n(reusable logic)"]
RESOURCES["resources\nAzure Resources\nto deploy"]
OUTPUTS["outputs\nReturned values\n(URLs, IDs, etc.)"]
end
SCHEMA --> PARAMS
PARAMS --> VARS
VARS --> FUNCS
FUNCS --> RESOURCES
RESOURCES --> OUTPUTS
Main components:
| Component | Required | Description |
|---|---|---|
| $schema | ✅ Yes | Defines JSON format and version — template foundation |
| contentVersion | ✅ Yes | Template version (e.g. “1.0.0.0”) |
| parameters | ❌ No | Values entered at runtime — make the template reusable |
| variables | ❌ No | Calculated values — reduce repetition |
| functions | ❌ No | Custom functions (User-Defined Functions) |
| resources | ✅ Yes | Azure resources to deploy (can be empty) |
| outputs | ❌ No | Values returned after deployment (URL, IDs) |
Complete ARM Template: Storage Account
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"type": "string",
"defaultValue": "[resourceGroup().location]",
"metadata": {
"description": "Resource location. Defaults to the Resource Group region."
}
},
"environment": {
"type": "string",
"allowedValues": ["dev", "staging", "prod"],
"defaultValue": "dev",
"metadata": {
"description": "Deployment environment."
}
},
"storageSkuName": {
"type": "string",
"defaultValue": "Standard_LRS",
"allowedValues": [
"Standard_LRS",
"Standard_GRS",
"Standard_ZRS",
"Premium_LRS"
],
"metadata": {
"description": "Storage account SKU."
}
}
},
"variables": {
"storageAccountName": "[toLower(concat(parameters('environment'), 'storage', uniqueString(resourceGroup().id)))]",
"tags": {
"Environment": "[parameters('environment')]",
"ManagedBy": "ARM Template",
"DeployedAt": "[utcNow()]"
}
},
"functions": [
{
"namespace": "contoso",
"members": {
"uniqueName": {
"parameters": [
{
"name": "namePrefix",
"type": "string"
}
],
"output": {
"type": "string",
"value": "[toLower(concat(parameters('namePrefix'), uniqueString(resourceGroup().id)))]"
}
}
}
}
],
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[variables('storageAccountName')]",
"location": "[parameters('location')]",
"tags": "[variables('tags')]",
"sku": {
"name": "[parameters('storageSkuName')]"
},
"kind": "StorageV2",
"properties": {
"accessTier": "Hot",
"supportsHttpsTrafficOnly": true,
"minimumTlsVersion": "TLS1_2",
"allowBlobPublicAccess": false,
"networkAcls": {
"defaultAction": "Deny",
"bypass": "AzureServices"
}
}
}
],
"outputs": {
"storageAccountName": {
"type": "string",
"value": "[variables('storageAccountName')]"
},
"storageAccountId": {
"type": "string",
"value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
},
"primaryEndpoints": {
"type": "object",
"value": "[reference(variables('storageAccountName')).primaryEndpoints]"
}
}
}
Built-in ARM Functions
// String functions
"[toLower('MyString')]" // "mystring"
"[concat('prefix-', parameters('name'))]" // "prefix-value"
"[uniqueString(resourceGroup().id)]" // Unique 13-char hash
"[replace('hello-world', '-', '_')]" // "hello_world"
// Resource functions
"[resourceId('Microsoft.Storage/storageAccounts', 'myaccount')]"
"[resourceGroup().name]" // Resource group name
"[resourceGroup().location]" // Resource group region
"[subscription().subscriptionId]" // Subscription ID
// Numeric functions
"[add(parameters('instances'), 1)]" // instances + 1
"[mul(parameters('size'), 2)]" // size * 2
// Conditional functions
"[if(equals(parameters('env'), 'prod'), 'Premium_LRS', 'Standard_LRS')]"
// Array functions
"[first(parameters('regions'))]" // First element
"[last(parameters('regions'))]" // Last element
"[length(parameters('regions'))]" // Array length
Bicep Templates
Why Bicep?
Bicep is a DSL (Domain-Specific Language) created by Microsoft to replace ARM JSON templates in development workflows. It is more readable, more concise, and offers a better developer experience while generating the exact same underlying ARM JSON.
graph LR
subgraph BICEP[".bicep File"]
B1["Readable syntax\nNo verbose JSON"]
B2["IntelliSense\nVS Code Extension"]
B3["Reusable modules\nAzure Verified Modules"]
end
subgraph TRANSPILE["Transpilation"]
T1["az bicep build\nOR az deployment group create"]
end
subgraph ARM["ARM JSON"]
A1["Full verbose\nJSON format"]
end
subgraph AZURE["Azure Resource Manager"]
AZ1["Resource\norchestration"]
end
BICEP --> TRANSPILE
TRANSPILE --> ARM
ARM --> AZURE
style BICEP fill:#8b5cf6,color:#fff
style TRANSPILE fill:#f59e0b,color:#000
style ARM fill:#6b7280,color:#fff
style AZURE fill:#1e40af,color:#fff
ARM JSON vs Bicep comparison for the same Storage Account:
// ARM JSON (35 lines)
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[variables('storageAccountName')]",
"location": "[parameters('location')]",
"sku": {
"name": "[parameters('storageSkuName')]"
},
"kind": "StorageV2",
"properties": {
"accessTier": "Hot",
"supportsHttpsTrafficOnly": true
}
}
// Equivalent Bicep (10 lines)
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: storageSkuName
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
}
}
Complete Bicep Template: Web Infrastructure
// main.bicep - Complete infrastructure: App Service + Storage + Key Vault
// ===== PARAMETERS =====
@description('Deployment environment')
@allowed(['dev', 'staging', 'prod'])
param environment string = 'dev'
@description('Azure region for all resources')
param location string = resourceGroup().location
@description('App Service plan SKU')
@allowed(['F1', 'B1', 'B2', 'S1', 'S2', 'P1v3', 'P2v3'])
param appServicePlanSku string = environment == 'prod' ? 'P1v3' : 'B1'
@description('Number of App Service replicas')
@minValue(1)
@maxValue(10)
param appServiceInstances int = environment == 'prod' ? 3 : 1
@description('Enable access logs')
param enableAccessLogs bool = true
// ===== VARIABLES =====
var uniqueSuffix = uniqueString(resourceGroup().id)
var storageAccountName = '${environment}storage${uniqueSuffix}'
var keyVaultName = '${environment}-kv-${uniqueSuffix}'
var appServicePlanName = '${environment}-asp-${uniqueSuffix}'
var webAppName = '${environment}-webapp-${uniqueSuffix}'
var commonTags = {
Environment: environment
ManagedBy: 'Bicep'
DeployedAt: utcNow()
Repository: 'https://github.com/my-org/my-repo'
}
// ===== RESOURCES =====
// Storage Account
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
tags: commonTags
sku: {
name: environment == 'prod' ? 'Standard_ZRS' : 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
}
}
}
// Azure Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
location: location
tags: commonTags
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
enablePurgeProtection: environment == 'prod' ? true : null
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
}
}
}
// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: appServicePlanName
location: location
tags: commonTags
sku: {
name: appServicePlanSku
capacity: appServiceInstances
}
kind: 'linux'
properties: {
reserved: true // Required for Linux
}
}
// Web App
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: webAppName
location: location
tags: commonTags
identity: {
type: 'SystemAssigned' // Managed Identity
}
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'NODE|20-lts'
alwaysOn: environment == 'prod' ? true : false
ftpsState: 'Disabled'
http20Enabled: true
minTlsVersion: '1.2'
appSettings: [
{
name: 'STORAGE_ACCOUNT_NAME'
value: storageAccount.name
}
{
name: 'KEY_VAULT_URI'
value: keyVault.properties.vaultUri
}
{
name: 'NODE_ENV'
value: environment
}
]
}
}
}
// Assign Key Vault Secrets User role to the Web App
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVault.id, webApp.id, 'Key Vault Secrets User')
scope: keyVault
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'4633458b-17de-408a-b874-0445c86b69e6' // Key Vault Secrets User role ID
)
principalId: webApp.identity.principalId
principalType: 'ServicePrincipal'
}
}
// Diagnostic settings (logs to Log Analytics) - if prod
resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableAccessLogs) {
name: 'webapp-diagnostics'
scope: webApp
properties: {
logs: [
{
category: 'AppServiceHTTPLogs'
enabled: true
retentionPolicy: {
enabled: true
days: environment == 'prod' ? 90 : 30
}
}
{
category: 'AppServiceConsoleLogs'
enabled: true
retentionPolicy: {
enabled: true
days: 30
}
}
]
metrics: [
{
category: 'AllMetrics'
enabled: true
retentionPolicy: {
enabled: true
days: 30
}
}
]
}
}
// ===== OUTPUTS =====
output webAppName string = webApp.name
output webAppUrl string = 'https://${webApp.properties.defaultHostName}'
output webAppPrincipalId string = webApp.identity.principalId
output storageAccountName string = storageAccount.name
output keyVaultName string = keyVault.name
output keyVaultUri string = keyVault.properties.vaultUri
Bicep Parameters File (.bicepparam)
// main.bicepparam - Parameter file for the production environment
using './main.bicep'
param environment = 'prod'
param location = 'eastus'
param appServicePlanSku = 'P1v3'
param appServiceInstances = 3
param enableAccessLogs = true
Bicep Modules
Modules allow splitting templates into reusable components and promote separation of concerns.
graph TD
MAIN["main.bicep\n(Main template)"]
subgraph MODULES["Modules"]
M1["storage.bicep\nStorage Account"]
M2["keyvault.bicep\nAzure Key Vault"]
M3["webapp.bicep\nApp Service"]
M4["br/public:\nstorage-account:0.4.1\n(Azure Verified Module)"]
end
MAIN -->|"module storageModule"| M1
MAIN -->|"module kvModule"| M2
MAIN -->|"module appModule"| M3
MAIN -->|"module avm"| M4
style MAIN fill:#1e40af,color:#fff
style MODULES fill:#0f4c75,color:#fff
style M4 fill:#7c3aed,color:#fff
Example using custom modules:
// main.bicep with modules
param environment string = 'dev'
param location string = resourceGroup().location
// Storage module (local file)
module storageModule './modules/storage.bicep' = {
name: 'storageDeployment'
params: {
environment: environment
location: location
}
}
// Key Vault module (local file)
module kvModule './modules/keyvault.bicep' = {
name: 'keyVaultDeployment'
params: {
environment: environment
location: location
}
}
// Azure Verified Module from public registry
module storageAvm 'br/public:storage/storage-account:0.4.1' = {
name: 'storageAvmDeployment'
params: {
name: 'myavmstorage${uniqueString(resourceGroup().id)}'
location: location
}
}
// Use outputs from one module in another
output storageAccountId string = storageModule.outputs.storageAccountId
output keyVaultUri string = kvModule.outputs.keyVaultUri
Storage module (modules/storage.bicep):
// modules/storage.bicep
param environment string
param location string
var storageAccountName = '${environment}storage${uniqueString(resourceGroup().id)}'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: environment == 'prod' ? 'Standard_ZRS' : 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
}
}
output storageAccountName string = storageAccount.name
output storageAccountId string = storageAccount.id
output primaryBlobEndpoint string = storageAccount.properties.primaryEndpoints.blob
Module 2 – Deployments with Azure CLI and PowerShell
Azure CLI for IaC Deployments
# ===== RESOURCE GROUPS =====
# Create a resource group with tags
az group create \
--name app-rg \
--location eastus \
--tags Environment=dev ManagedBy=Bicep Owner=cloud-team
# List resource groups
az group list --output table
# Delete a resource group and all its resources
az group delete --name app-rg --yes --no-wait
# ===== BICEP DEPLOYMENTS =====
# Deploy a Bicep template
az deployment group create \
--resource-group app-rg \
--template-file main.bicep \
--parameters environment=dev location=eastus
# Deploy with a parameter file
az deployment group create \
--resource-group app-rg \
--template-file main.bicep \
--parameters @parameters.json
# Deploy with .bicepparam file
az deployment group create \
--resource-group app-rg \
--template-file main.bicep \
--parameters main.bicepparam
# Deploy at subscription level
az deployment sub create \
--location eastus \
--template-file subscription-level.bicep \
--parameters @sub-params.json
# ===== VALIDATE A TEMPLATE =====
# Validate without deploying
az deployment group validate \
--resource-group app-rg \
--template-file main.bicep \
--parameters environment=dev
# Compile Bicep → ARM JSON (for review)
az bicep build --file main.bicep --outdir ./compiled
# ===== TRACK DEPLOYMENTS =====
# List deployments in a resource group
az deployment group list \
--resource-group app-rg \
--output table
# View details of a deployment
az deployment group show \
--resource-group app-rg \
--name main \
--query "properties.{Status:provisioningState, Duration:duration}" \
--output table
# View operations of a deployment
az deployment group operation list \
--resource-group app-rg \
--name main \
--output table
Azure PowerShell for IaC Deployments
# ===== RESOURCE GROUPS =====
# Create a resource group
New-AzResourceGroup `
-Name "app-rg" `
-Location "eastus" `
-Tag @{
Environment = "dev"
ManagedBy = "Bicep"
Owner = "cloud-team"
}
# List resource groups
Get-AzResourceGroup | Format-Table Name, Location, ProvisioningState
# ===== BICEP DEPLOYMENTS =====
# Deploy a Bicep template
New-AzResourceGroupDeployment `
-ResourceGroupName "app-rg" `
-TemplateFile "main.bicep" `
-environment "dev" `
-location "eastus"
# Deploy with a parameter file
New-AzResourceGroupDeployment `
-ResourceGroupName "app-rg" `
-TemplateFile "main.bicep" `
-TemplateParameterFile "parameters.json"
# Deploy at subscription level
New-AzSubscriptionDeployment `
-Location "eastus" `
-TemplateFile "subscription-level.bicep"
# ===== VALIDATE =====
# Validate without deploying
Test-AzResourceGroupDeployment `
-ResourceGroupName "app-rg" `
-TemplateFile "main.bicep" `
-environment "dev"
# ===== TRACK =====
# View operations of a deployment
Get-AzResourceGroupDeploymentOperation `
-ResourceGroupName "app-rg" `
-DeploymentName "main" |
Select-Object OperationId, ProvisioningState, StatusCode |
Format-Table
Azure CLI vs Azure PowerShell Comparison
| Operation | Azure CLI | Azure PowerShell |
|---|---|---|
| Create RG | az group create --name X --location Y | New-AzResourceGroup -Name X -Location Y |
| Deploy Bicep | az deployment group create --template-file | New-AzResourceGroupDeployment -TemplateFile |
| What-if | az deployment group what-if | New-AzResourceGroupDeployment -WhatIf |
| Validate | az deployment group validate | Test-AzResourceGroupDeployment |
| List deployments | az deployment group list | Get-AzResourceGroupDeployment |
| Delete RG | az group delete --name X | Remove-AzResourceGroup -Name X |
| Login | az login | Connect-AzAccount |
| Output format | --output table/json/yaml | | Format-Table/ConvertTo-Json |
Module 3 – What-if and Change Tracking
What-if Deployments: Infrastructure Dry Run
A What-if deployment is the equivalent of a git diff for your infrastructure: it shows you exactly what will change if you deploy, without making any real changes.
sequenceDiagram
participant DEV as Engineer
participant CLI as Azure CLI
participant ARM as Azure Resource Manager
participant AZURE as Azure Resources
DEV->>CLI: az deployment group what-if\n--template-file main.bicep
CLI->>ARM: Deployment simulation
ARM->>AZURE: Analyze current state
AZURE-->>ARM: Current resource state
ARM-->>CLI: Expected changes (JSON)
CLI-->>DEV: Change report\n(+ create, ~ modify, - delete)
Note over DEV: Review changes
alt Changes approved
DEV->>CLI: az deployment group create\n--template-file main.bicep
CLI->>ARM: Real deployment
ARM->>AZURE: Create/Modify/Delete
AZURE-->>DEV: Resources deployed ✅
else Changes not approved
DEV->>DEV: Modify template
DEV->>CLI: Re-run what-if
end
What-if report symbols:
| Symbol | Color | Meaning |
|---|---|---|
+ | Green | Resource/property created |
~ | Yellow | Resource/property modified |
- | Red | Resource/property deleted |
= | Gray | No change detected |
* | Orange | Property ignored (not present in template) |
# ===== WHAT-IF WITH AZURE CLI =====
# Basic what-if
az deployment group what-if \
--resource-group app-rg \
--template-file main.bicep \
--parameters environment=prod
# What-if with automatic confirmation if changes detected
az deployment group what-if \
--resource-group app-rg \
--template-file main.bicep \
--confirm-with-what-if # Pauses if changes detected
# What-if with JSON output (for automation/CI)
az deployment group what-if \
--resource-group app-rg \
--template-file main.bicep \
--output json | jq '.properties.changes'
# Example what-if output:
# Resource changes: 2 to create, 1 to modify, 0 to delete.
#
# The deployment will update the following scope:
# Scope: /subscriptions/{sub}/resourceGroups/app-rg
#
# + Microsoft.Storage/storageAccounts/prodstorage123 [2023-01-01]
# apiVersion: "2023-01-01"
# id: "/subscriptions/.../storageAccounts/prodstorage123"
# kind: "StorageV2"
# location: "eastus"
# name: "prodstorage123"
#
# ~ Microsoft.Web/sites/prod-webapp-123 [2023-01-01]
# properties.siteConfig.alwaysOn: false => true
# ===== WHAT-IF WITH POWERSHELL =====
# Basic what-if
New-AzResourceGroupDeployment `
-ResourceGroupName "app-rg" `
-TemplateFile "main.bicep" `
-environment "prod" `
-WhatIf
# What-if filtering change types (exclude NoChange and Ignore)
New-AzResourceGroupDeployment `
-ResourceGroupName "app-rg" `
-TemplateFile "main.bicep" `
-WhatIf `
-WhatIfExcludeChangeType NoChange, Ignore
# What-if with minimal format (only IDs of impacted resources)
New-AzResourceGroupDeployment `
-ResourceGroupName "app-rg" `
-TemplateFile "main.bicep" `
-WhatIf `
-WhatIfResultFormat ResourceIdOnly
# Capture what-if result for analysis
$whatIfResult = New-AzResourceGroupDeployment `
-ResourceGroupName "app-rg" `
-TemplateFile "main.bicep" `
-WhatIf `
-ResultObject
# Analyze changes
$whatIfResult.Changes | Where-Object { $_.ChangeType -eq 'Delete' } |
Select-Object ResourceId, ChangeType | Format-Table
Azure Change Analysis: Modification History
Azure Change Analysis allows viewing all changes made to Azure resources over a given period. It is an essential tool for:
- Understanding what changed after an incident
- Auditing unplanned modifications
- Validating that deployments had the intended effect
graph TD
subgraph SOURCES["Change sources"]
DEPLOY["ARM/Bicep\nDeployments"]
PORTAL["Azure portal\nmodifications"]
CLI["Azure CLI\ncommands"]
POLICY["Azure Policy\nauto-remediation"]
end
subgraph STORAGE["Storage"]
RP["Azure Resource\nManager API"]
CHANGE_DB["Change Analysis\nDatabase"]
end
subgraph VISUALIZATION["Visualization"]
BLADE["Change Analysis\nportal blade"]
FILTER["Filters:\n- Subscription\n- Resource Group\n- Period\n- Resource type"]
DETAIL["Detailed view\nModified properties"]
end
SOURCES --> RP
RP --> CHANGE_DB
CHANGE_DB --> BLADE
BLADE --> FILTER
FILTER --> DETAIL
style SOURCES fill:#1e3a5f,color:#fff
style STORAGE fill:#f59e0b,color:#000
style VISUALIZATION fill:#10b981,color:#fff
Module 4 – Testing and Drift Detection
ARM Template Toolkit (ARM-TTK)
ARM-TTK is an open-source PowerShell module that validates your ARM and Bicep templates against Microsoft best practices. It is the linting/quality assurance tool for Azure IaC.
flowchart TD
TEMPLATE["ARM/Bicep Template"]
TTK["ARM-TTK\nTest-AzTemplate"]
subgraph TESTS["ARM-TTK Tests"]
T1["✅ Location = parameter\nreferenced by all resources"]
T2["✅ Used parameters/variables\nno orphan declarations"]
T3["✅ Recent API versions\nno hardcoded obsolete versions"]
T4["✅ No concat() for IDs\nuse resourceId() instead"]
T5["✅ Unique names\nno naming collisions"]
T6["✅ Outputs without secrets\nno passwords in outputs"]
end
TEMPLATE --> TTK
TTK --> TESTS
TESTS --> PASS["✅ All tests pass\nValid template"]
TESTS --> FAIL["❌ Failed tests\nCorrections needed"]
style TEMPLATE fill:#1e3a5f,color:#fff
style TTK fill:#f59e0b,color:#000
style PASS fill:#10b981,color:#fff
style FAIL fill:#ef4444,color:#fff
# ===== ARM-TTK INSTALLATION AND USAGE =====
# Download ARM-TTK from GitHub
# https://github.com/Azure/arm-ttk/releases
# Unzip and import the module
$armTtkPath = "C:\arm-ttk\arm-ttk.psd1"
Import-Module $armTtkPath
# Test an ARM JSON template
Test-AzTemplate -TemplatePath ./main.json
# Test a Bicep template (compiled to ARM before test)
Test-AzTemplate -TemplatePath ./main.bicep
# Test an entire folder
Test-AzTemplate -TemplatePath ./templates/ -Recurse
# Test with filters on specific tests
Test-AzTemplate `
-TemplatePath ./main.json `
-Test "apiVersions-should-be-recent"
# Example ARM-TTK output:
# Template : main.json
# Pass : adminUsername Should Not Be A Literal
# Pass : artifacts parameter
# Pass : CommandToExecute Must Use ProtectedSettings For Secrets
# FAIL : DependsOn Best Practices
# >> main.json (line 45): "dependsOn" should not contain "[concat(...)]"
# Pass : Deployment Resources Must Not Be Debug
# Pass : DeploymentTemplate Schema Is Correct
# Pass : Dynamic Variable References Should Not Use Concat
# FAIL : IDs Should Be Derived From ResourceIDs
# >> main.json (line 78): Use resourceId() instead of concat() for IDs
# Pass : Location Should Not Be Hardcoded
# Pass : ManagedIdentityExtension must not be used
# Pass : Min And Max Value Are The Correct Type
# Pass : Outputs Must Not Contain Secrets
# Pass : Parameter Types Should Be Consistent
# Pass : Parameters Must Be Referenced
# Integration in a CI/CD script
$testResult = Test-AzTemplate -TemplatePath ./main.bicep
$failedTests = $testResult | Where-Object { $_.Passed -eq $false }
if ($failedTests.Count -gt 0) {
Write-Error "ARM-TTK: $($failedTests.Count) test(s) failed!"
$failedTests | Format-Table Name, Passed, Errors
exit 1
}
Write-Output "ARM-TTK: All tests passed!"
Most Important ARM-TTK Rules
| Rule | Description | Example violation |
|---|---|---|
| Location must be a parameter | Location must be defined via a parameter, not hardcoded | "location": "eastus" instead of "location": "[parameters('location')]" |
| Recent API versions | Do not use outdated API versions | apiVersion: "2018-02-01" for a resource with a 2023 version |
| Used parameters | Every declared parameter must be used in the template | Declaring param dbPassword string without using it |
| Used variables | Same for variables | Variable storagePrefix declared but never referenced |
| No concat for IDs | Use resourceId() rather than concat() to build resource IDs | concat('/subscriptions/', subscription().id, '...') |
| Outputs without secrets | Outputs must not expose sensitive data | output dbPassword string = 'mypassword' |
| Descriptive parameter names | Parameters should have metadata descriptions | Parameter without metadata.description |
Configuration Drift Detection
Configuration drift occurs when the actual state of resources diverges from the state declared in templates. This happens via:
- Manual modifications in the Azure portal
- Emergency fixes via CLI without updating the template
- Automatic modifications by Azure Policy
- Auto-scaling or automatic modifications by managed services
# ===== METHOD 1: WHAT-IF ON EXISTING TEMPLATE =====
# Run what-if on an already-deployed template
# If state differs, changes are detected = possible drift
az deployment group what-if \
--resource-group app-rg \
--template-file main.bicep \
--parameters @prod-params.json
# Note: Some "drift" properties are false positives
# (properties added by Azure after deployment)
# ===== METHOD 2: EXPORT + COMPARISON (more accurate) =====
# Step 1: Export current state of the resource group
az group export \
--name app-rg \
--include-parameter-default-value \
> current-state.json
# Step 2: Compile source template to ARM JSON
az bicep build \
--file main.bicep \
--outfile desired-state.json
# Step 3: Compare the two files
# Option 1: classic diff
diff <(jq . desired-state.json) <(jq . current-state.json)
# Option 2: VS Code - open both files side by side
code --diff desired-state.json current-state.json
Automated drift detection script:
#!/bin/bash
# drift-detection.sh - Detects configuration drift
RESOURCE_GROUP="production-rg"
TEMPLATE_FILE="main.bicep"
PARAMS_FILE="prod-params.json"
echo "=== Drift Detection: $RESOURCE_GROUP ==="
# Run what-if and capture output
WHATIF_OUTPUT=$(az deployment group what-if \
--resource-group $RESOURCE_GROUP \
--template-file $TEMPLATE_FILE \
--parameters @$PARAMS_FILE \
--output json 2>&1)
# Check if changes are detected
CHANGES_COUNT=$(echo $WHATIF_OUTPUT | jq '.properties.changes | length')
if [ "$CHANGES_COUNT" -eq "0" ]; then
echo "✅ No drift detected. Current state matches the template."
else
echo "⚠️ DRIFT DETECTED! $CHANGES_COUNT change(s) detected:"
echo $WHATIF_OUTPUT | jq '.properties.changes[] | {Resource: .resourceId, Type: .changeType}'
# Send an alert (example with curl to Teams/Slack)
# curl -X POST "https://hooks.slack.com/services/..." \
# -H 'Content-type: application/json' \
# --data "{\"text\":\"⚠️ Configuration drift detected in $RESOURCE_GROUP: $CHANGES_COUNT change(s)\"}"
exit 1
fi
CI/CD Pipeline Integration
# .github/workflows/iac-pipeline.yml
name: Infrastructure as Code Pipeline
on:
push:
branches: [main]
paths:
- 'infrastructure/**'
pull_request:
branches: [main]
paths:
- 'infrastructure/**'
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
validate:
name: Validate Templates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Azure Login (OIDC)
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Bicep Lint
run: |
az bicep install
az bicep lint --file infrastructure/main.bicep
- name: Validate Deployment
run: |
az deployment group validate \
--resource-group dev-rg \
--template-file infrastructure/main.bicep \
--parameters infrastructure/params.dev.json
- name: What-If Analysis
id: whatif
run: |
WHATIF_OUTPUT=$(az deployment group what-if \
--resource-group ${{ vars.RESOURCE_GROUP }} \
--template-file infrastructure/main.bicep \
--parameters infrastructure/params.prod.json \
--output json)
echo "whatif-output=$WHATIF_OUTPUT" >> $GITHUB_OUTPUT
CHANGES=$(echo $WHATIF_OUTPUT | jq '.properties.changes | length')
echo "changes-count=$CHANGES" >> $GITHUB_OUTPUT
echo "Change summary:"
echo $WHATIF_OUTPUT | jq '.properties.changes[] | {resource: .resourceId, type: .changeType}'
- name: Comment PR with What-If Results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const output = `#### 🔍 What-if Analysis
**Expected changes:** ${{ steps.whatif.outputs.changes-count }}
<details><summary>View details</summary>
\`\`\`json
${{ steps.whatif.outputs.whatif-output }}
\`\`\`
</details>`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
deploy-dev:
name: Deploy to Dev
needs: validate
runs-on: ubuntu-latest
environment: dev
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Azure Login (OIDC)
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Dev
run: |
az deployment group create \
--resource-group dev-rg \
--template-file infrastructure/main.bicep \
--parameters infrastructure/params.dev.json \
--verbose
deploy-prod:
name: Deploy to Production
needs: deploy-dev
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- uses: actions/checkout@v4
- name: Azure Login (OIDC)
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Production
run: |
az deployment group create \
--resource-group prod-rg \
--template-file infrastructure/main.bicep \
--parameters infrastructure/params.prod.json \
--verbose
Complete Code Examples
Complete Multi-tier Infrastructure in Bicep
// infrastructure/main.bicep
// Complete infrastructure: VNet + Subnets + NSG + App Service + SQL + Key Vault
targetScope = 'resourceGroup'
@description('Target environment')
@allowed(['dev', 'staging', 'prod'])
param environment string
@description('Azure region')
param location string = resourceGroup().location
@description('SQL administrator login')
param sqlAdminLogin string
@secure()
@description('SQL administrator password')
param sqlAdminPassword string
// Variables
var prefix = '${environment}-${uniqueString(resourceGroup().id)}'
var vnetName = '${prefix}-vnet'
var nsgName = '${prefix}-nsg'
var appSubnetName = 'app-subnet'
var dbSubnetName = 'db-subnet'
var tags = {
Environment: environment
ManagedBy: 'Bicep'
DeployedAt: utcNow()
}
// ===== NETWORK =====
// NSG for the app subnet
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-05-01' = {
name: nsgName
location: location
tags: tags
properties: {
securityRules: [
{
name: 'AllowHTTPS'
properties: {
priority: 100
protocol: 'Tcp'
access: 'Allow'
direction: 'Inbound'
sourceAddressPrefix: 'Internet'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '443'
}
}
{
name: 'DenyAllInbound'
properties: {
priority: 4096
protocol: '*'
access: 'Deny'
direction: 'Inbound'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '*'
}
}
]
}
}
// VNet with 2 subnets
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: vnetName
location: location
tags: tags
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
subnets: [
{
name: appSubnetName
properties: {
addressPrefix: '10.0.1.0/24'
networkSecurityGroup: {
id: nsg.id
}
delegations: [
{
name: 'appServiceDelegation'
properties: {
serviceName: 'Microsoft.Web/serverFarms'
}
}
]
}
}
{
name: dbSubnetName
properties: {
addressPrefix: '10.0.2.0/24'
privateEndpointNetworkPolicies: 'Disabled'
}
}
]
}
}
// ===== SQL DATABASE =====
resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = {
name: '${prefix}-sql'
location: location
tags: tags
properties: {
administratorLogin: sqlAdminLogin
administratorLoginPassword: sqlAdminPassword
minimalTlsVersion: '1.2'
publicNetworkAccess: 'Disabled'
}
}
resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = {
parent: sqlServer
name: '${environment}-db'
location: location
tags: tags
sku: {
name: environment == 'prod' ? 'GP_S_Gen5_2' : 'Basic'
tier: environment == 'prod' ? 'GeneralPurpose' : 'Basic'
}
properties: {
collation: 'SQL_Latin1_General_CP1_CI_AS'
maxSizeBytes: environment == 'prod' ? 107374182400 : 2147483648
}
}
// ===== APP SERVICE =====
resource appPlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: '${prefix}-plan'
location: location
tags: tags
sku: {
name: environment == 'prod' ? 'P1v3' : 'B1'
}
kind: 'linux'
properties: {
reserved: true
}
}
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: '${prefix}-app'
location: location
tags: tags
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appPlan.id
virtualNetworkSubnetId: '${vnet.id}/subnets/${appSubnetName}'
httpsOnly: true
siteConfig: {
linuxFxVersion: 'NODE|20-lts'
alwaysOn: environment == 'prod'
vnetRouteAllEnabled: true
appSettings: [
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
{
name: 'ApplicationInsightsAgent_EXTENSION_VERSION'
value: '~3'
}
]
}
}
}
// ===== APPLICATION INSIGHTS =====
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: '${prefix}-logs'
location: location
tags: tags
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: environment == 'prod' ? 90 : 30
}
}
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: '${prefix}-ai'
location: location
tags: tags
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalytics.id
}
}
// ===== OUTPUTS =====
output webAppUrl string = 'https://${webApp.properties.defaultHostName}'
output webAppPrincipalId string = webApp.identity.principalId
output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName
output vnetId string = vnet.id
Comparison Tables
ARM/Bicep Deployment Scopes
| Scope | CLI | PowerShell | Template $schema | Use case |
|---|---|---|---|---|
| Resource Group | az deployment group create | New-AzResourceGroupDeployment | ...deploymentTemplate.json# | Most resources |
| Subscription | az deployment sub create | New-AzSubscriptionDeployment | ...subscriptionDeploymentTemplate.json# | Policies, RBAC, Resource Groups |
| Management Group | az deployment mg create | New-AzManagementGroupDeployment | ...managementGroupDeploymentTemplate.json# | Cross-subscription policies |
| Tenant | az deployment tenant create | New-AzTenantDeployment | ...tenantDeploymentTemplate.json# | AAD, Top-level MGs |
Deployment Modes
| Mode | Description | Behavior | Risk |
|---|---|---|---|
| Incremental (default) | Adds/updates resources in the template | Does not delete resources not present | ⭐ Low |
| Complete | Deletes resources in the RG not present in the template | May delete unexpected resources | ⚠️ High |
# Complete mode (caution: may delete resources!)
az deployment group create \
--resource-group app-rg \
--template-file main.bicep \
--mode Complete
Glossary
| Term | Definition |
|---|---|
| ARM | Azure Resource Manager — Azure resource orchestrator, receives templates and deploys them |
| ARM Template | JSON file describing Azure resources to deploy |
| Bicep | Microsoft DSL for writing readable IaC templates, transpiled to ARM JSON |
| Resource Provider | Azure service managing a type of resources (e.g. Microsoft.Storage) |
| Resource Group | Logical container for grouping and managing Azure resources |
| Deployment | Instance of an ARM deployment in a scope (RG, Sub, MG, Tenant) |
| Parameters | Dynamic values passed to the template at deployment time |
| Variables | Calculated internal template values, not modifiable from outside |
| Outputs | Values returned by the template after deployment (URLs, IDs, etc.) |
| User-Defined Functions (UDF) | Custom functions in an ARM namespace |
| Bicep Module | Separate Bicep file referenced from a main template |
| Azure Verified Modules (AVM) | Official Microsoft modules for Bicep and Terraform |
| What-if | Deployment dry-run mode — previews changes without applying them |
| Configuration Drift | Gap between the state declared in templates and the actual resource state |
| ARM-TTK | ARM Template Test Toolkit — PowerShell tool for validating templates |
| Change Analysis | Azure service for visualizing the history of resource modifications |
| Symbolic Name | Internal identifier of a resource in a Bicep file (≠ Azure name) |
| targetScope | Declares the deployment scope of a Bicep template (resourceGroup by default) |
| IntelliSense | Autocomplete and validation in VS Code with the Bicep extension |
| Incremental Mode | ARM deployment mode that does not delete resources not present |
| Complete Mode | ARM deployment mode that deletes resources not present in the template |
| ResourceId() | ARM function for building an Azure resource ID |
| uniqueString() | ARM function generating a deterministic unique hash based on an input |
| utcNow() | ARM function returning the current UTC date/time |
| dependsOn | Explicit dependency declaration between ARM resources |
Module 1 – Authoring Infrastructure with ARM and Bicep
ARM Template Structure
Main Components
| Component | Description |
|---|---|
| $schema | Defines JSON format and version. Template foundation → structure and syntax rules. |
| parameters | Values entered at runtime (region, environment name). Make the template reusable. |
| variables | Calculated values based on parameters/functions. Reduce repetition. |
| functions | Custom functions (User-Defined Functions). Reusable logic within the template. |
| resources | Azure resources to deploy. |
| outputs | Values returned after deployment (e.g. URL, resource ID). |
ARM Functions
- Built-in functions:
toLower,concat,add,first,resourceId,split,uniqueString,utcnow. - Usable everywhere in the template (not only in variables).
User-Defined Functions (UDFs)
{
"functions": [{
"namespace": "contoso",
"members": {
"uniqueName": {
"parameters": [{"name": "namePrefix", "type": "string"}],
"output": {
"type": "string",
"value": "[toLower(concat(parameters('namePrefix'), uniqueString(resourceGroup().id)))]"
}
}
}
}]
}
- Rules:
- Require a dedicated namespace.
- Cannot access other UDFs or template variables.
- Parameters must be explicitly defined.
Bicep Templates
Why Bicep?
- ARM JSON = verbose, difficult to read (optimized for machines).
- Bicep = concise and readable language, designed for humans.
- Bicep is transpiled to ARM JSON before being sent to Azure Resource Manager.
- Benefits: simpler syntax, no excessive quotes, IntelliSense in VS Code.
ARM vs Bicep Workflow
ARM: JSON template → Azure Resource Manager → Resource Provider → Resource created
Bicep: .bicep file → transpiled to ARM JSON → Azure Resource Manager → Resource created
Bicep Parameters
// Parameter with a value calculated based on another parameter
param environment string = 'dev'
param storageAccountName string = '${environment}storage${uniqueString(resourceGroup().id)}'
- Max 256 parameters per template.
- Unique names (no collision between parameters, variables, resources).
- Possibility of custom data types for complex scenarios.
Bicep Resource (example: Storage Account)
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: skuName
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
}
}
- Type format:
ResourceProvider/ResourceType@APIVersion - Symbolic name: internal identifier in the Bicep file (≠ Azure name).
- Bicep templates written in YAML-like syntax, not JSON.
Demo: Deploying Bicep Resources
Deploy via VS Code
- Install the Bicep extension for VS Code → IntelliSense, validation.
- Create the
.bicepfile. - Deploy via Azure CLI or PowerShell.
Bicep Variables and Tags
param location string = resourceGroup().location
param skuName string = 'Standard_LRS'
var uniqueStorageName = uniqueString(resourceGroup().id)
var tags = union(resourceGroup().tags, {
'DeployedAt': utcNow()
'ManagedBy': 'Bicep'
})
Bicep Modules
Concept
- Factor reusable Bicep code into separate modules.
- A module = a
.bicepfile with its own parameters and outputs. - Reference a module from a main template.
Azure Verified Modules (AVM)
- Official Bicep (and Terraform) modules engineered and supported by Microsoft.
- Accelerate IaC adoption without developing custom modules.
- GitHub repository:
Azure/bicep-registry-modules. - Each module = README with examples (basic → complex).
Using an AVM
// Reference a public module (alias br/public = Bicep Public Module Registry)
module storageAccount 'br/public:storage/storage-account:0.4.1' = {
name: 'storageAccountDeployment'
params: {
name: storageAccountName
location: location
}
}
Module 2 – Managing Azure Deployments
Azure CLI
Installation
- Windows: WinGet, Chocolatey, MSI installer.
- Linux: apt, yum, official installation script.
- macOS: Homebrew.
- Browser: Azure Cloud Shell (always up to date).
- Docker:
docker run mcr.microsoft.com/azure-cli.
Resource Group Commands
# Create a resource group
az group create --name MyResourceGroup --location eastus
# List resource groups (table format)
az group list --output table
# Deploy an ARM/Bicep template
az deployment group create \
--resource-group MyResourceGroup \
--template-file main.bicep \
--parameters @parameters.json
# What-if (preview changes)
az deployment group what-if \
--resource-group MyResourceGroup \
--template-file main.bicep
Azure PowerShell
Resource Group Commands
# Create a resource group
New-AzResourceGroup -Name MyResourceGroup -Location eastus
# List resource groups
Get-AzResourceGroup | Format-Table
# Deploy a template
New-AzResourceGroupDeployment `
-ResourceGroupName MyResourceGroup `
-TemplateFile main.bicep `
-TemplateParameterFile parameters.json
# What-if
New-AzResourceGroupDeployment `
-ResourceGroupName MyResourceGroup `
-TemplateFile main.bicep `
-WhatIf
Module 3 – What-if Deployments and Change Tracking
What-if Deployments
Principle
- Dry run: simulate a deployment without making real changes.
- Preview exactly what will be created, modified, or deleted.
- Share proposed changes with stakeholders before deployment.
- Detect unintended consequences (e.g. accidental deletions, security deactivation).
Symbols in the what-if output
| Symbol | Meaning |
|---|---|
+ | Resource/property created |
~ | Resource/property modified |
- | Resource/property deleted |
= | No change |
PowerShell what-if options
New-AzResourceGroupDeployment `
-ResourceGroupName test-rg `
-TemplateFile main.bicep `
-WhatIf `
-WhatIfExcludeChangeType NoChange, Ignore `
-WhatIfResultFormat ResourceIdOnly
WhatIfExcludeChangeType: filter certain change types in the output.WhatIfResultFormat ResourceIdOnly: display only IDs of impacted resources.
Azure Change Analysis
- Azure service for viewing the change history of resources.
- Accessible from the Azure portal → “Change Analysis” blade.
- Use filters (subscription, resource group, period) to target changes.
- Detailed view of modified ARM properties.
Module 4 – ARM Template Toolkit (ARM-TTK) and Drift Detection
ARM-TTK
What is it?
- PowerShell module for validating ARM/Bicep templates against Microsoft best practices.
- Works on Windows, Linux, macOS (PowerShell Core).
- Can be integrated into CI/CD pipelines.
Installation
# Download from GitHub (not available on PowerShell Gallery)
# Import the module
Import-Module /path/to/arm-ttk/arm-ttk.psd1
# Test a template
Test-AzTemplate -TemplatePath ./main.json
Rules validated by ARM-TTK
- Used parameters and variables: everything declared must be used.
- Recent API versions: do not use hardcoded outdated API versions.
- No concat for IDs: do not use
concatto build already-available Resource IDs. - Location as parameter: location must be a parameter referenced by all resources.
Drift Detection
Method 1: What-if on an existing template
- Issue: some properties are only set after deployment → false positives.
- Limitations for drift detection.
Method 2: Export + Comparison
- Export the template of the current resource state from the Azure portal.
- Resource Group → “Export template”.
- Compare the exported template with the source template.
- Identify differences = drift detected.
# Export from portal or CLI
az group export --name MyResourceGroup > current-state.json
# Compare with git diff or a diff tool
Key Points for Exam / Practice
ARM vs Bicep
- Bicep = recommended for new Azure-only projects.
- ARM JSON = still valid, still used in many environments.
- Bicep is transpiled to ARM JSON → same end result for Azure Resource Manager.
- An invalid Bicep template = a transpilation error BEFORE reaching Azure.
What-if
- Always run a what-if before deploying to production.
- Share results with stakeholders.
- Unexpected changes = do not deploy.
ARM-TTK
- Use in CI/CD pipelines for automatic template validation.
- Most important rules:
locationparameter referenced everywhere, recent API versions.
AVM (Azure Verified Modules)
- Prefer AVM over custom modules for standard resources.
- Documentation = README in the GitHub repo for each module.
- Access via the
br/public:alias in Bicep templates.
General Best Practices
- Do not hardcode values → use parameters.
- Tags: inherit tags from the resource group + add your own.
- allowed values on parameters: fail fast with a clear message (better than an Azure API fail).
- Variables for logic and calculated values, parameters for user inputs.
Search Terms
azure · infrastructure · arm · bicep · devops · iac · microsoft · what-if · deployments · template · arm-ttk · powershell · change · cli · detection · drift · avm · comparison · functions · resource · templates · account · analysis · commands