Domain 3.5 · part of 50–55%

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.

ToolLayerScopeLanguageModel
BicepProvisioningAzureBicep DSLDeclarative, idempotent
ARM templatesProvisioningAzureJSONDeclarative, idempotent
TerraformProvisioningMulti-cloudHCLDeclarative; external state file
PulumiProvisioningMulti-cloudReal languages (C#, TS, Python)Declarative
PowerShell DSC / Machine ConfigurationConfigurationOS (Windows/Linux)PowerShell DSCDeclarative, continuously enforced
AnsibleConfiguration (+ some provisioning)Multi-cloud + OSYAMLMostly declarative, agentless (SSH)
Chef / PuppetConfigurationOSRuby / Puppet DSLDeclarative, agent-based

Caveat — choosing the technology

Azure-only resource provisioningBicep (preferred) or ARM. Multi-cloud provisioningTerraform. OS / in-VM configurationDSC / 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

layout repository structure for IaC
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
flowchart LR PR[IaC change · PR] --> V["Validate
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]
An IaC pipeline: validate → preview → scan → approve → progressively deploy, same shape as an app pipeline.

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.

main.bicep parameters, resources, outputs
@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}'
bicep a module and its consumer
// 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

azuredeploy.json the ARM shape Bicep compiles to
{
  "$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 modeBehaviourRisk
Incremental (default)Adds/updates resources in the template; leaves resources not in the template untouchedSafe
CompleteAdds/updates in the template AND deletes anything in the resource group not in the templateCan 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

azure cli preview, then deploy
# 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
azure-pipelines.yml what-if gate before deploy
- 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

PowerShell a DSC 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
  1. Author the configuration (PowerShell) and compile it to a MOF.
  2. Upload/import it into the Azure Automation account (the pull server).
  3. Register nodes (Azure VMs, on-prem or Arc-enabled servers) against the account.
  4. The Local Configuration Manager pulls the config on an interval and enforces it (ApplyAndAutoCorrect).
  5. Compliance is reported centrally in the portal.

Azure Machine Configuration (formerly Guest Configuration)

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.

flowchart TB DC["Dev Center
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]
Platform team owns the Dev Center and catalog; developers get governed, disposable environments.
ComponentRole
Dev CenterTop-level container: registers catalogs, environment types and identities
CatalogA GitHub or Azure Repos repository of environment definitions (IaC + manifest)
Environment definitionAn IaC template (ARM/Bicep) plus an environment.yaml manifest and parameters
ProjectRepresents a team/product; grants developer access
Environment typeMaps a stage (Dev/Test/Prod) to a target subscription and permissions
environment.yaml an environment definition manifest
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 JSONBicep
Provision across AWS and Azure with one toolTerraform
Configure the OS inside VMs continuouslyDSC / Machine Configuration (or Ansible)
Preview infrastructure changes before applyingaz deployment group what-if
Template must delete resources not defined in itARM deployment mode Complete
Add/update only, never deleteDeployment mode Incremental (default)
Convert an ARM JSON template to Bicepaz bicep decompile
Audit OS settings across many VMs via Azure PolicyAzure Machine Configuration
Pull-server DSC with an Automation accountAzure Automation State Configuration
Scan IaC for security misconfigurationsPSRule for Azure, checkov, tfsec
Developers self-serve governed environments from approved templatesAzure Deployment Environments
Manage a set of resources as one unit, block deletionBicep deployment stacks with deny settings