Design and implement infrastructure as code (IaC)
Treating infrastructure and OS configuration as versioned, reviewed, tested code — provisioning resources with Bicep and ARM, keeping machines in their desired state, and letting developers self-serve environments from approved templates.
Configuration management technologies
The first decision is provisioning (creating cloud resources) vs configuration (setting up the OS and apps inside those resources). Different tools own each layer.
| Tool | Layer | Scope | Language | Model |
|---|---|---|---|---|
| Bicep | Provisioning | Azure | Bicep DSL | Declarative, idempotent |
| ARM templates | Provisioning | Azure | JSON | Declarative, idempotent |
| Terraform | Provisioning | Multi-cloud | HCL | Declarative; external state file |
| Pulumi | Provisioning | Multi-cloud | Real languages (C#, TS, Python) | Declarative |
| PowerShell DSC / Machine Configuration | Configuration | OS (Windows/Linux) | PowerShell DSC | Declarative, continuously enforced |
| Ansible | Configuration (+ some provisioning) | Multi-cloud + OS | YAML | Mostly declarative, agentless (SSH) |
| Chef / Puppet | Configuration | OS | Ruby / Puppet DSL | Declarative, agent-based |
Caveat — choosing the technology
Azure-only resource provisioning → Bicep (preferred) or ARM. Multi-cloud provisioning → Terraform. OS / in-VM configuration → DSC / Machine Configuration or Ansible. Declarative ("describe the end state") beats imperative ("list the steps") in almost every AZ-400 answer, because declarative tooling is idempotent and self-healing.
Idempotency and drift
Idempotent = applying the same definition repeatedly converges to the same state with no side effects. Configuration drift = reality diverging from the definition (someone changed a setting by hand). Declarative IaC both prevents drift (re-apply to correct it) and detects it (what-if for resources, DSC/Machine Configuration compliance for the OS).
Defining an IaC strategy
infrastructure/
├── environments/
│ ├── dev/ main.bicep dev.bicepparam
│ ├── staging/ main.bicep staging.bicepparam
│ └── prod/ main.bicep prod.bicepparam
├── modules/
│ ├── network/vnet.bicep
│ ├── compute/appservice.bicep
│ └── data/sql.bicep
└── tests/
└── validate.ps1
- Source-control everything; review infrastructure changes with PRs exactly like application code (branch policies from Domain 2 apply).
- Externalise per-environment values into parameter files — one template, many environments. Never hardcode.
- Modularise reusable components; version shared modules and consume them from a registry.
- Automate the test-and-deploy loop — lint, validate, preview, security-scan, then deploy on approval.
bicep build · lint (PSRule)"] V --> WI["Preview
az deployment what-if"] WI --> SEC["Security scan
PSRule · checkov · tfsec"] SEC --> A{Approve} A --> DEV[Deploy dev] --> IT[Integration tests] IT --> STG[Deploy staging] --> SMK[Smoke tests] SMK --> PRD[Deploy prod · manual approval]
Scan IaC for misconfiguration before it ships
PSRule for Azure, checkov and tfsec catch insecure defaults (public storage, open NSGs, missing encryption) at PR time. Microsoft Defender for Cloud's DevOps security also surfaces IaC findings and annotates PRs — see Domain 4. This is the "shift-left" answer for infrastructure.
Azure Bicep
Bicep is a transparent abstraction over ARM: it compiles (transpiles) to ARM JSON, so anything ARM can do, Bicep can do — with far less ceremony, no need to hand-manage dependsOn, and modules as a first-class feature.
@description('Environment name')
@allowed(['dev', 'staging', 'prod'])
param environment string = 'dev'
param location string = resourceGroup().location
@secure()
param sqlAdminPassword string // never surfaced in logs/outputs
var appServicePlanName = 'asp-${environment}-${uniqueString(resourceGroup().id)}'
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: appServicePlanName
location: location
sku: {
name: environment == 'prod' ? 'P1v3' : 'B1' // ternary for env-specific sizing
capacity: environment == 'prod' ? 3 : 1
}
}
resource web 'Microsoft.Web/sites@2023-12-01' = {
name: 'app-${environment}-${uniqueString(resourceGroup().id)}'
location: location
properties: {
serverFarmId: plan.id // implicit dependency — no dependsOn needed
httpsOnly: true
}
}
output webAppUrl string = 'https://${web.properties.defaultHostName}'
// modules/storage.bicep
param name string
param location string = resourceGroup().location
param sku string = 'Standard_LRS'
resource sa 'Microsoft.Storage/storageAccounts@2023-04-01' = {
name: name
location: location
sku: { name: sku }
kind: 'StorageV2'
}
output blobEndpoint string = sa.properties.primaryEndpoints.blob
// main.bicep — consume it (loops with @batchSize, conditions with 'if' also supported)
module storage 'modules/storage.bicep' = {
name: 'storageDeploy'
params: { name: 'st${uniqueString(resourceGroup().id)}', sku: 'Standard_GRS' }
}
output blobUrl string = storage.outputs.blobEndpoint
Bicep features worth naming
Modules (reuse + implicit ordering), loops (for, @batchSize for serial deployment), conditions (if), existing keyword (reference resources you do not manage), .bicepparam typed parameter files, a public/private module registry (ACR-backed), and deployment stacks (manage a set of resources as one unit with deny-settings to prevent drift/deletion).
ARM templates and deployment modes
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": { "webAppName": { "type": "string" } },
"variables": { "planName": "[concat('plan-', parameters('webAppName'))]" },
"resources": [
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2023-12-01",
"name": "[variables('planName')]",
"location": "[resourceGroup().location]",
"sku": { "name": "B1" }
}
],
"outputs": {
"appUrl": { "type": "string",
"value": "[concat('https://', parameters('webAppName'), '.azurewebsites.net')]" }
}
}
| Deployment mode | Behaviour | Risk |
|---|---|---|
| Incremental (default) | Adds/updates resources in the template; leaves resources not in the template untouched | Safe |
| Complete | Adds/updates in the template AND deletes anything in the resource group not in the template | Can delete resources |
Caveat — Complete mode deletes
Complete mode removes resources that exist in the resource group but are absent from the template. Bicep and the CLI default to Incremental. Complete mode is the correct answer to "the resource group should contain exactly what the template declares, nothing more" — but it is a trap if the scenario has other resources sharing the group.
Previewing with what-if
# Preview the change set WITHOUT applying it
az deployment group what-if \
--resource-group myRG \
--template-file infrastructure/main.bicep \
--parameters infrastructure/environments/prod/prod.bicepparam
# Deploy (incremental by default)
az deployment group create \
--resource-group myRG \
--template-file infrastructure/main.bicep \
--parameters infrastructure/environments/prod/prod.bicepparam
# Convert an existing ARM JSON template to Bicep
az bicep decompile --file azuredeploy.json
- task: AzureCLI@2
displayName: What-if preview
inputs:
azureSubscription: 'MyServiceConnection'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az deployment group what-if -g myRG \
--template-file infrastructure/main.bicep \
--parameters infrastructure/environments/$(env)/main.bicepparam
- task: AzureResourceManagerTemplateDeployment@3
displayName: Deploy Bicep
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: 'MyServiceConnection'
resourceGroupName: myRG
location: uksouth
csmFile: 'infrastructure/main.bicep'
csmParametersFile: 'infrastructure/environments/$(env)/main.bicepparam'
deploymentMode: 'Incremental'
Deployment scopes
Deployments target a scope: resource group (most common), subscription (create resource groups, policy/RBAC assignments), management group (org-wide policy), or tenant. The targetScope keyword in Bicep declares it.
Desired State Configuration
DSC keeps the inside of a machine (features, files, services, registry) in a declared state and continuously corrects drift. AZ-400 lists two hosting models.
Azure Automation State Configuration
Configuration WebServerConfig {
Import-DscResource -ModuleName PSDesiredStateConfiguration
Node 'WebServer01' {
WindowsFeature IIS {
Ensure = 'Present'
Name = 'Web-Server'
}
File WebContent {
Ensure = 'Present'
Type = 'Directory'
DestinationPath = 'C:\inetpub\wwwroot\myapp'
DependsOn = '[WindowsFeature]IIS'
}
Service W3SVC {
Name = 'W3SVC'
State = 'Running'
}
}
}
WebServerConfig -OutputPath ./mof # compile to a MOF document
- Author the configuration (PowerShell) and compile it to a MOF.
- Upload/import it into the Azure Automation account (the pull server).
- Register nodes (Azure VMs, on-prem or Arc-enabled servers) against the account.
- The Local Configuration Manager pulls the config on an interval and enforces it (
ApplyAndAutoCorrect). - Compliance is reported centrally in the portal.
Azure Machine Configuration (formerly Guest Configuration)
- Built into Azure Policy — no Automation account required.
- Audit mode reports compliance; DeployIfNotExists can remediate.
- Works on Windows and Linux, on Azure VMs and Arc-enabled servers, via the Azure Monitor / guest configuration extension.
- Uses DSC v3 under the hood; it is the modern successor to Automation State Configuration for auditing OS settings at scale.
Caveat — Automation DSC vs Machine Configuration
"Continuously enforce a configuration with a pull server and an Automation account" → Azure Automation State Configuration. "Audit (and optionally remediate) OS settings across many VMs and Arc servers through Azure Policy, no Automation account" → Azure Machine Configuration. The phrase "at scale via Azure Policy" is the tell for Machine Configuration.
Azure Deployment Environments
Azure Deployment Environments (ADE) lets developers self-serve complete, pre-approved environments on demand — platform engineering curates the templates, developers press a button, and governance/cost controls are enforced automatically.
catalogs · environment types · identity"] --> CAT["Catalog
Git repo of environment definitions (IaC)"] DC --> PROJ["Project
maps env types → subscriptions"] PROJ --> DEV["Developer
portal / CLI / VS Code"] DEV -->|"self-serve, from an approved definition"| ENV["Environment
provisioned in the mapped subscription"] ENV -.->|delete when done| GONE[Resources removed]
| Component | Role |
|---|---|
| Dev Center | Top-level container: registers catalogs, environment types and identities |
| Catalog | A GitHub or Azure Repos repository of environment definitions (IaC + manifest) |
| Environment definition | An IaC template (ARM/Bicep) plus an environment.yaml manifest and parameters |
| Project | Represents a team/product; grants developer access |
| Environment type | Maps a stage (Dev/Test/Prod) to a target subscription and permissions |
name: Web Application Environment
version: 1.0.0
summary: App Service + SQL Database + Key Vault
runner: ARM
templatePath: main.bicep
parameters:
- id: appName
name: Application name
type: string
required: true
Caveat — ADE vs a self-service pipeline
When the requirement is "let developers spin up on-demand environments from pre-approved templates, governed and cost-tracked, without giving them subscription access", the answer is Azure Deployment Environments — not "give them a pipeline" and not "grant Contributor". ADE is also distinct from Microsoft Dev Box (managed developer workstations); both hang off a Dev Center, but ADE provisions infrastructure.
3.5 rapid recap
| The requirement says… | Answer |
|---|---|
| Azure-native IaC, simpler than ARM JSON | Bicep |
| Provision across AWS and Azure with one tool | Terraform |
| Configure the OS inside VMs continuously | DSC / Machine Configuration (or Ansible) |
| Preview infrastructure changes before applying | az deployment group what-if |
| Template must delete resources not defined in it | ARM deployment mode Complete |
| Add/update only, never delete | Deployment mode Incremental (default) |
| Convert an ARM JSON template to Bicep | az bicep decompile |
| Audit OS settings across many VMs via Azure Policy | Azure Machine Configuration |
| Pull-server DSC with an Automation account | Azure Automation State Configuration |
| Scan IaC for security misconfigurations | PSRule for Azure, checkov, tfsec |
| Developers self-serve governed environments from approved templates | Azure Deployment Environments |
| Manage a set of resources as one unit, block deletion | Bicep deployment stacks with deny settings |