Azure Troubleshooting

VM Extension Provisioning Failed: Debugging Custom Script, DSC and Azure Monitor Agent Errors

Your Terraform plan applies cleanly, the VM boots, and then the deployment hangs for forty minutes before failing with one of the most unhelpful strings in Azure: “VM has reported a failure when processing extension ‘CustomScript’. Error message: ‘Enable failed: …’”. The portal shows the VM as Running, the extension as Provisioning failed, your pipeline as red — and nothing tells you whether your script had a bug, the Azure VM Guest Agent never came up, the handler timed out pulling a file, or the VM has no outbound route to fetch what it needs. A VM extension is a small agent-driven payload — install software (CustomScript), apply configuration (DSC), or stream telemetry (AzureMonitorWindowsAgent / AzureMonitorLinuxAgent) — that Azure injects into a running VM through the guest agent. When it fails, the abstraction that made az vm extension set a one-liner becomes an opaque wall.

This is the diagnostic playbook for that wall. We treat “extension provisioning failed” not as one bug but as a layered failure: the Azure fabric asks the guest agent (waagent on Linux, WindowsAzureGuestAgent on Windows) to run a handler, the handler runs your payload, and any of those three layers can be the liar. You will learn to read the real signal — the handler status file, the per-extension log, the substatus and exit code returned to ARM — and localise a failure to exactly one layer: a dead guest agent, a handler that couldn’t download its package, a script that exited non-zero, a DSC configuration that threw mid-apply, or an Azure Monitor Agent that installed fine but has no managed identity or Data Collection Rule to talk to. Every diagnosis comes with the exact file to open or command to run, plus the fix in az CLI and Bicep.

By the end you will stop re-running the pipeline and hoping. You will know whether to look on the box (agent and handler logs at /var/log/azure/... or C:\WindowsAzure\Logs\...), in the resource’s instanceView, or in your own script’s stderr — and that an extension stuck at Creating/Transitioning for forty minutes is a different problem from one that returned Failed in ninety seconds. Knowing which layer within a few minutes turns a half-day stall into a five-minute fix.

What problem this solves

VM extensions are the seam between provisioning a VM and configuring a VM. Terraform, Bicep, ARM and az vm all lean on them: install the monitoring agent, run a bootstrap script, join a domain, apply DSC, onboard to Azure Arc. The seam is powerful because it runs inside the guest after boot — and fragile for the same reason. The control plane (ARM) only sees a provisioning state and a terse status bubbled up from the guest; the truth lives on the VM, in agent logs the control plane never shows you. The information you need is real and captured, but it is on the box, and if you don’t know which log maps to which layer you burn an hour clicking Refresh.

What breaks without this knowledge: an engineer re-runs the deployment (which sometimes “works” because a transient download succeeded the second time, teaching the wrong lesson), deletes and re-creates the VM (expensive and still failing), or files a ticket and waits — while the real cause (a script that exits 1 on a curl to a blocked endpoint, a guest agent that’s Not Ready, a DSC node reboot-looping, or an AMA with no DCR association shipping zero data) sits there, perfectly diagnosable, ignored. Worse, a failed extension can block the whole VM deployment — leaving the VM in a Failed state that downstream resources depend on, or hanging terraform apply.

Who hits this: anyone bootstrapping VMs as code. It bites hardest on Custom Script Extension (your script’s bugs plus network/download issues), DSC (long applies, reboots, the WMF/PowerShell version maze), and the Azure Monitor Agent migration (the agent installs cleanly but silently sends nothing because the identity or DCR is wrong). The fix is almost never “re-run it” — it’s “find the layer that’s lying and read its status file.”

To frame the whole field before the deep dive, here is every failure class this article covers, the layer it lives in, and the one place to look first:

Failure class Which layer is lying First question to ask First place to look Most common single cause
Stuck at Creating/Transitioning Guest agent or handler Is the guest agent even running and Ready? az vm get-instance-view → agent status Guest agent Not Ready / no outbound to fabric
Custom Script enable failed Your script (mostly) Did the script run, and what exit code? CSE log on the box; instanceView substatus Script exited non-zero, or download (storage/identity) failed
DSC apply failed / looping DSC handler + your config Did the config compile and apply, or reboot-loop? DSC handler log; Get-DscConfigurationStatus WMF/module mismatch, or a reboot the config never completes after
AMA installed but no data Configuration around the agent Does the agent have an identity and a DCR? AMA logs; az monitor data-collection rule association list No managed identity, or no DCR associated, or workspace egress blocked
Provisioning succeeded, app broken Your payload’s logic Did “exit 0” actually mean success? Your script’s own logging Script swallowed an error and returned 0

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already know how to create a VM and read JSON from az (Cloud Shell or a local CLI), and understand that an extension is a child resource of the VM (Microsoft.Compute/virtualMachines/extensions) that runs after the OS boots, injected through the VM Guest Agent — the service Azure pre-installs in marketplace images that talks to the fabric, downloads handler packages, runs them, and reports status. Familiarity with az vm run-command (a sibling mechanism that runs a command via the same agent, invaluable for diagnosis) helps. If VMs are new, start with Your First Azure Virtual Machine: A Step-by-Step Deployment in Portal, CLI and PowerShell.

This sits in the Compute & Troubleshooting track and pairs with two neighbours. When the failure turns out to be connectivity (the agent or handler can’t reach a fabric/storage/ingestion endpoint), cross into Cannot RDP or SSH into Your Azure VM? A Beginner’s Connectivity Troubleshooting Checklist and Diagnosing Connectivity with Network Watcher: Connection Monitor, Connection Troubleshoot and Next Hop. When it’s identity (Custom Script can’t read a blob, or AMA has no identity), cross into Managed Identities Demystified: System vs User-Assigned and When to Use Each. For the AMA half, the data side lives in Azure Monitor and Application Insights: Full-Stack Observability and No Logs Showing Up? Troubleshooting Empty Log Analytics Tables and Ingestion Gaps.

A quick map of who confirms what during an incident, so you open the right log fast:

Layer What lives here Where it logs Failure classes it owns
Azure fabric / ARM The “goal state”: which extensions to run Activity log; resource provisioningState Extension never dispatched; conflicting deployments
VM Guest Agent (waagent / WindowsAzureGuestAgent) Polls goal state, downloads handlers, reports status /var/log/waagent.log; C:\WindowsAzure\Logs\WaAppAgent.log Stuck at Creating; “agent not ready”; handler download
Handler (per extension) Runs the extension’s own logic /var/log/azure/<handler>/...; C:\WindowsAzure\Logs\Plugins\<handler>\... Handler timeout; bad handler version; exit codes
Your payload (script / DSC / config) The thing you actually want done Your own stdout/stderr (captured by the handler) Non-zero exit; DSC throw; logic that “succeeds” wrongly
Identity & network Storage auth, fabric/ingestion egress, DCR Status substatus; NSG/UDR; DCR association Download 403; AMA no data; agent can’t reach fabric

Core concepts

Five mental models make every later diagnosis obvious.

An extension is a goal-state contract executed by an agent. You declare “this VM should have extension X with these settings”; ARM records that as the goal state; the guest agent polls the fabric, sees the goal, downloads the handler package, runs it, and reports a status back to ARM. The portal’s Provisioning succeeded/failed is the top of a chain whose real detail lives in the on-box agent and handler logs. Read only the portal and you read the summary of a summary.

“Stuck” and “failed” are different bugs. A failed extension returned a terminal status — ARM has a substatus and (for script-like handlers) an exit code; the payload ran and reported something. A stuck extension (sitting at Creating/Transitioning for tens of minutes) usually means a layer below your payload never finished: the guest agent is Not Ready, can’t reach the fabric, or the handler is mid-retry. The first fork in every diagnosis is “terminal status, or hung?” — they send you to different logs.

The guest agent is the single point all extensions go through. On Linux it is waagent (the walinuxagent service); on Windows it is WindowsAzureGuestAgent plus WaAppAgent. It must be installed, running, and Ready before any extension can succeed. A Not Ready agent fails or stalls every extension at once — the tell: if all extensions on a VM are stuck, suspect the agent, not your script. The agent needs outbound access to the Azure fabric (host 168.63.129.16) to fetch goal state and handler packages.

The handler captures your payload’s output and exit code — that’s your real log. For Custom Script and DSC, the handler runs your script/config and records stdout, stderr, and the exit code into a status file. Exit code 0 = success; non-zero = failed, and that code is what bubbles up to ARM. So the most important thing your bootstrap script can do is log loudly and return honest exit codes — a script that does real work but exit 0s on error makes the extension “succeed” while the VM is broken, the worst class because nothing alerts.

Some extensions install fine and still do nothing. The Azure Monitor Agent (AMA) is the canonical case: the extension provisions successfully (binary installed and running) yet ships zero data, because it has no managed identity to authenticate, no Data Collection Rule (DCR) associated to tell it what to collect and where, or the ingestion endpoint is unreachable. “Provisioning succeeded” for AMA means “installed,” not “flowing.” You confirm flowing with a KQL query, never the provisioning state.

The vocabulary in one table

Before the deep sections, pin down every moving part. The glossary at the end repeats these for lookup; this table is the mental model side by side:

Concept One-line definition Where it lives Why it matters to a failed extension
VM extension Agent-run payload (install/config/telemetry) Child of the VM resource The thing that “failed to provision”
Guest agent Service that runs all extensions waagent / WindowsAzureGuestAgent Not Ready → every extension fails/stalls
Handler Per-extension program that runs your payload Downloaded by the agent Owns timeouts, exit codes, status files
Goal state “What should be installed” per VM Azure fabric Agent polls it; if unreachable, stuck
instanceView Runtime status of the VM + extensions az vm get-instance-view Shows substatus + message ARM saw
Substatus / message The terse reason bubbled to ARM In instanceView First text you read; often truncated
Exit code Script/handler return code Handler status file 0 = ok, non-zero = the failure
168.63.129.16 The Azure fabric “wire server” host Reachable from inside the VM Agent needs it for goal state/health
Protected settings Encrypted extension settings (secrets, keys) In the extension config Mis-set → download 403 / auth fail
DCR Data Collection Rule (what AMA collects) Microsoft.Insights/dataCollectionRules No DCR association → AMA sends nothing
Managed identity The VM’s Azure identity for auth On the VM AMA / storage download need it

The status and exit-code reference

Before the per-handler anatomy, here is the lookup table you scan first: the states and codes you realistically see, what each means on this platform, the likely cause, how to confirm it, and the first fix. The non-obvious ones are exit code 51 (CSE “not supported”/skipped), the difference between a provisioning state and the handler status, and that a successful provisioning state can still mean a broken outcome.

State / code Meaning Likely cause How to confirm First fix
ProvisioningState = Creating/Transitioning (stuck) Extension never reached a terminal status Guest agent Not Ready, no fabric egress, handler mid-retry az vm get-instance-viewvmAgent.statuses; waagent.log Get the agent Ready; restore outbound to 168.63.129.16
ProvisioningState = Failed, substatus Enable failed Handler ran but returned failure Script exit non-zero, download failure, DSC throw instanceView substatus message; on-box handler log Match the row in the playbook; fix the payload/auth
ProvisioningState = Succeeded but app broken Handler reported success, outcome wrong Script exit 0 on error; AMA installed but no DCR Your own script log; AMA data KQL Return honest exit codes; associate a DCR
Exit code 0 Success Handler status file code: 0 Nothing to fix at the extension layer
Exit code 1 (generic) Script/handler returned a generic error Any unhandled failure in your script CSE stdout/stderr in the log dir Read the script’s own error; fix it
Exit code 51 (“Enable not supported”/skipped) Handler refused to run the operation Unsupported OS/distro, or CSE asked to skip Handler log; OS vs supported matrix Use a supported image/handler version
“Extension execution timed out” Handler exceeded its time budget Long-running script (CSE default ~90 min wall, but timestamp/download limits bite) Handler log timestamps; script duration Make the script faster / async; background long work
“Handler … not found” / download failed Agent couldn’t fetch the handler package No fabric/CDN egress; agent too old waagent.log download lines; agent version Restore egress; upgrade the guest agent
“VMAgent status: Not Ready” Guest agent unhealthy Agent stopped, crashed, image lacks it, clock skew vmAgent.statuses in instanceView Restart/repair the agent; check time sync
DSC: “configuration … failed” DSC handler applied but a resource threw Bad config, missing module, WMF mismatch Get-DscConfigurationStatus; DSC handler log Fix the config/module; match WMF version
AMA: provisioned, Heartbeat empty Agent up, but not authenticated/configured No managed identity, no DCR, blocked egress Heartbeat KQL; DCR association list Add identity; associate DCR; open egress
Conflict / “another operation in progress” Concurrent extension/VM operation Overlapping deploys; agent busy Activity log; serialise the operation Wait/serialise; one extension op at a time

Three reading notes that save the most time:

Distinction The trap How to tell them apart
Provisioning state vs handler status “Succeeded” lulls you into thinking the work succeeded Provisioning state = “the handler ran and returned”; the handler status message (and your own log) says whether the work was right. Always read the substatus, not just the green tick
Stuck vs failed You wait on a hung extension as if it will recover, or re-run a deterministic failure A Failed state has a substatus + exit code (terminal — fix and re-run); a Creating/Transitioning state for 20+ min is a layer below (agent/egress) that won’t self-heal
Agent problem vs script problem Hours in the script log when the agent is the issue If every extension on the VM is stuck/failed and vmAgent is Not Ready, it’s the agent. If one extension fails with an exit code while the agent is Ready, it’s that handler/payload

Anatomy: the guest agent and the request pipeline

Every extension failure is, ultimately, a failure somewhere on this path. The request a deployment makes: (1) you declare the extension → ARM stores the goal state; (2) the guest agent polls the fabric over 168.63.129.16, sees the goal, and downloads the handler package; (3) the handler runs its enable operation, executing your payload; (4) the handler writes a status file, the agent reports status to ARM, and ARM flips the extension to Succeeded or Failed. Any hop can break, and the symptom tells you which:

Hop What can break Symptom you see Confirm on the box
1. Goal state dispatched Conflicting deploy; ARM didn’t dispatch Extension never appears / “another operation in progress” Activity log; az vm extension list
2. Agent polls fabric Agent Not Ready; no route to 168.63.129.16 Stuck at Creating; “VMAgent not ready” waagent.log; Test-NetConnection 168.63.129.16 -Port 80
2. Handler download Blocked CDN/storage egress; old agent “Handler not found”/download fail; stuck waagent.log download lines
3. Handler enable Bad handler version; OS unsupported Exit 51; handler crashes immediately Handler log under Plugins/azure
3. Payload runs Your script/config throws Exit 1/N; DSC “failed” Script stdout/stderr in handler log dir
4. Status reported Agent can’t report back Stuck despite payload finishing waagent.log status-upload lines

Confirm the guest agent first — it gates everything

Before touching your script, prove the agent is healthy. The instanceView carries the agent’s own status:

# The single most useful command: full runtime status of the VM, agent, and every extension
az vm get-instance-view --name vm-app01 --resource-group rg-app \
  --query "{provisioningState:provisioningState, agent:vmAgent.statuses[].displayStatus, ext:instanceView.extensions}" -o json

The vmAgent.statuses block should show Ready. If it shows Not Ready (or the agent version is conspicuously old), every extension on this VM is suspect for the same reason. On the box:

# Linux — is walinuxagent running, and is it talking to the fabric?
systemctl status walinuxagent      # or 'waagent' on some distros
sudo tail -n 100 /var/log/waagent.log
# Can the VM reach the Azure fabric wire server at all?
curl -s -m 5 -o /dev/null -w "%{http_code}\n" http://168.63.129.16/  # any response = reachable
# Windows — are the two agent services running, and is the fabric reachable?
Get-Service WindowsAzureGuestAgent, RdAgent | Format-Table Name, Status
Test-NetConnection -ComputerName 168.63.129.16 -Port 80
Get-Content 'C:\WindowsAzure\Logs\WaAppAgent.log' -Tail 100

The agent’s behaviour and where it logs, by OS — know your row before you go log-hunting:

Item Linux Windows
Agent service walinuxagent (a.k.a. waagent) WindowsAzureGuestAgent + RdAgent
Agent log /var/log/waagent.log C:\WindowsAzure\Logs\WaAppAgent.log
Per-extension logs /var/log/azure/<Publisher.Type>/... C:\WindowsAzure\Logs\Plugins\<Publisher.Type>\<version>\...
Handler status files /var/lib/waagent/<ext>/status/*.status C:\Packages\Plugins\<ext>\<version>\Status\*.status
Fabric health host 168.63.129.16 (HTTP/health) 168.63.129.16 (HTTP/health)
Restart the agent sudo systemctl restart walinuxagent Restart-Service WindowsAzureGuestAgent

The on-box layout you actually open

When the agent is Ready and one extension still failed, the truth is in two places: the handler log (what it did) and the status file (what it reported). The status file is JSON and mirrors what ARM saw:

# Linux — read the most recent handler status file (it's the source of the ARM message)
sudo cat /var/lib/waagent/Microsoft.Azure.Extensions.CustomScript-*/status/*.status | python3 -m json.tool

That JSON has a status (“success”/“error”/“transitioning”), a code, and a formattedMessage — the same string the portal shows, but untruncated. When in doubt, the status file is more honest than the portal.

Anatomy of a Custom Script Extension failure

The Custom Script Extension (CSE) downloads file(s) and runs a command on the VM — the workhorse for bootstrap. Its failures cluster into five causes. Scan the matrix, then read the detail for whichever row matches:

# CSE failure Tell-tale signal Confirm with Real fix Band-aid that masks it
1 Your script exited non-zero Substatus shows exit code 1/N + your stderr CSE log dir stdout/stderr; status file code Fix the script; return honest exit codes Re-run (works only if it was transient)
2 Download of the script failed “Failed to download”/403/timeout in log waagent.log/handler log download lines Fix storage auth (SAS/identity) or egress Embed script inline to dodge download
3 Storage auth / no managed identity 403 AuthorizationFailure on the blob Handler log; az role assignment list Grant identity Storage Blob Data Reader Public-blob the file (insecure)
4 Script ran too long → timeout Handler “timed out”; long gap in timestamps Handler log timestamps Background long work; make it idempotent Raise nothing — CSE budget is fixed-ish
5 Protected settings malformed Settings rejected / secrets visible/empty Extension definition; status message Put secrets in protectedSettings; valid JSON Plain settings (leaks/breaks)

Cause 1 — Your script exited non-zero

The most common CSE failure is the honest one: your script ran and returned a non-zero exit code, and the handler bubbled it up as the failure. The substatus often includes the tail of your stderr. Read the substatus from ARM, then the full stdout/stderr on the box:

# What ARM saw (the substatus carries your script's error text, often truncated)
az vm get-instance-view -n vm-app01 -g rg-app \
  --query "instanceView.extensions[?contains(name,'CustomScript')].substatuses[].message" -o tsv
# On the Linux box — the handler captures your script's real output here
sudo cat /var/lib/waagent/custom-script/download/0/stdout
sudo cat /var/lib/waagent/custom-script/download/0/stderr
# (older/newer layouts: /var/log/azure/custom-script/handler.log and the status dir above)
# On the Windows box
Get-Content 'C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\*\CustomScriptHandler.log' -Tail 200

Fix. Fix the throwing line. The deeper discipline: make the script idempotent (CSE may run more than once — on re-deploy, on a model change), log loudly, and set -euo pipefail (bash) so a failing command actually fails the script instead of silently continuing. The single biggest own-goal is a script that does apt-get install ... || true and then exit 0, hiding the real failure.

Cause 2 & 3 — The script (or its inputs) failed to download

CSE fetches the script from a storage blob (fileUris) before running it. If the blob is private and the VM can’t authenticate — or has no outbound route to storage — the download fails before your script ever runs. People stare at the script logic when the script never executed. The handler/agent log shows the attempt and HTTP code: a 403 means auth (private blob, missing identity, expired SAS); a timeout/refused means egress/routing.

sudo grep -iE "download|fileUris|403|denied|timed out" /var/log/waagent.log /var/log/azure/custom-script/*.log

Fix — auth. The secret-free way: give the VM a managed identity, grant it Storage Blob Data Reader on the blob, and reference the identity in the extension — no SAS tokens:

# 1) Give the VM a system-assigned identity and grant it read on the storage account
az vm identity assign -n vm-app01 -g rg-app
PRINCIPAL=$(az vm show -n vm-app01 -g rg-app --query identity.principalId -o tsv)
SA_ID=$(az storage account show -n stbootstrap -g rg-app --query id -o tsv)
az role assignment create --assignee "$PRINCIPAL" \
  --role "Storage Blob Data Reader" --scope "$SA_ID"

# 2) CSE pulls the blob using that identity (no SAS)
az vm extension set -n CustomScript --publisher Microsoft.Azure.Extensions \
  --vm-name vm-app01 -g rg-app \
  --settings '{"fileUris":["https://stbootstrap.blob.core.windows.net/scripts/bootstrap.sh"]}' \
  --protected-settings '{"managedIdentity":{},"commandToExecute":"bash bootstrap.sh"}'
resource cse 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = {
  parent: vm
  name: 'CustomScript'
  location: location
  properties: {
    publisher: 'Microsoft.Azure.Extensions'
    type: 'CustomScript'
    typeHandlerVersion: '2.1'
    autoUpgradeMinorVersion: true
    settings: {
      fileUris: [ 'https://stbootstrap.blob.core.windows.net/scripts/bootstrap.sh' ]
    }
    protectedSettings: {
      // Use the VM's managed identity to read the private blob — no SAS token in config
      managedIdentity: {}
      commandToExecute: 'bash bootstrap.sh'
    }
  }
}

If you understand why a managed identity is the right answer here (and when a user-assigned one is better for many VMs sharing the same bootstrap storage), Managed Identities Demystified: System vs User-Assigned and When to Use Each is the companion read.

Fix — egress. If it’s a connectivity failure, the VM has no route to storage or the agent’s CDN — an NSG/UDR/firewall problem, diagnosed like any other VM egress issue (Diagnosing Connectivity with Network Watcher: Connection Monitor, Connection Troubleshoot and Next Hop). Prefer a private blob + managed identity (403 if the role is missing, timeout if the route is); a SAS token works but is a credential to scope and expire; a public blob dodges auth but is world-readable (avoid); an inline commandToExecute with no download sidesteps the issue entirely.

Cause 4 — The script ran too long and timed out

A bootstrap that compiles software, pulls large images, or waits on a slow dependency can exceed the handler’s execution budget — the extension fails with a timeout even though the script was “working.” The handler log shows a long gap between the script’s start line and a “timed out” message; your own logging stops mid-way.

Fix. Don’t “raise the timeout” — make the bootstrap fast and the long work asynchronous: kick the slow part into the background (nohup/systemd unit/scheduled task), let the script return quickly, and have the background job report its own health. Keep the script idempotent so a retry doesn’t double-install. For genuinely long first-boot setup, bake it into a custom image rather than running CSE every time.

Cause 5 — Protected settings and secrets

CSE has two settings blocks: settings (plaintext, visible in the resource) and protectedSettings (encrypted, never returned). The commandToExecute and anything secret (SAS tokens, passwords) belong in protectedSettings. Putting a secret in settings leaks it; malformed JSON in either block rejects the extension.

Confirm. The status message complains about settings/JSON, or a secret you expected is empty/echoed. (Protected settings are write-only — you can’t read them back, which is the point.)

Fix. Keep the schema correct: non-secret config (fileUris, flags, anything you’d commit to git) in settings; everything sensitive (commandToExecute, SAS tokens, storageAccountKey, passwords, the managedIdentity block) in protectedSettings; both valid JSON. The CSE knobs you actually set, with their defaults and gotchas:

Setting What it does Where Gotcha
fileUris Files to download before running settings Private ones need identity/SAS + egress
commandToExecute The command CSE runs protectedSettings Put here (not settings) to avoid leaking args
managedIdentity Which identity authenticates blob downloads protectedSettings {} = system-assigned; or specify a user-assigned client/object id
timestamp Change this integer to force a re-run settings The only clean way to re-trigger CSE with unchanged content
skipDos2Unix (Linux) Skip line-ending conversion settings CRLF scripts from Windows can fail without it

A practical re-run note: CSE only re-runs when its configuration changes. If you fixed the script in storage but the extension config is byte-identical, the extension won’t re-execute. Bump the timestamp integer (or change any setting) to force it.

Anatomy of a DSC extension failure

The PowerShell DSC extension (Microsoft.Powershell.DSC, Windows) applies a Desired State Configuration to the VM and enforces it. DSC failures are richer (and slower) than CSE because DSC compiles, applies, and can reboot and resume. Five causes:

# DSC failure Tell-tale signal Confirm with Real fix
1 Configuration didn’t compile “Failed to compile”/MOF error early DSC handler log; compile output Fix the .ps1; compile locally first
2 A required module is missing/wrong version “module not found”/version conflict Get-DscConfigurationStatus; handler log Package the module with the config; pin versions
3 WMF/PowerShell version mismatch DSC resource fails on the OS’s WMF Handler log; $PSVersionTable on box Match WMF 5.1; update PowerShell if needed
4 A DSC resource threw mid-apply One resource “failed”, config “Failed” Get-DscConfigurationStatus per-resource Fix that resource’s parameters/preconditions
5 Reboot loop / never reaches Compliant Applies, reboots, re-applies forever DSC status; RebootNodeIfNeeded behaviour Make config converge; gate the reboot

Confirm: read the DSC configuration status, not just the extension

DSC keeps its own rich status separate from the extension’s. On the box, this is the most informative single command:

Get-DscConfigurationStatus | Format-List *                                  # per-resource pass/fail + why
Get-DscConfigurationStatus -All | Select StartDate, Status, RebootRequested  # history: applies + reboots

Status = Failure with a ResourcesNotInDesiredState list tells you which resource threw — far more useful than the extension’s one-line “configuration failed.” The handler log carries the rest:

Get-Content 'C:\WindowsAzure\Logs\Plugins\Microsoft.Powershell.DSC\*\*.log' -Tail 200

Causes 1–3 — Compile, modules, and WMF

DSC must compile your configuration to a MOF and have every DSC resource module present at the right version. Classic failures: a configuration that compiles on your laptop but not on the VM (different module versions), or a module the config Import-DscResources that was never packaged with it. DSC on Azure VMs targets WMF 5.1; resources written for a newer PowerShell can misbehave. The handler log shows the compile error or “could not find module X”; $PSVersionTable on the box shows the version.

Fix. Package modules with the configuration (supply a .zip containing the config and its modules) and pin module versions so the VM uses exactly what you tested. Compile locally first to catch MOF errors before they cost a 30-minute apply:

# Apply a DSC configuration packaged (config + modules) into a zip in blob storage
az vm extension set -n DSC --publisher Microsoft.Powershell.DSC \
  --vm-name vm-web01 -g rg-app \
  --settings '{
    "configuration": { "url": "https://stbootstrap.blob.core.windows.net/dsc/IIS.zip",
                       "script": "IIS.ps1", "function": "IISInstall" },
    "configurationArguments": { "WebsiteName": "shop" }
  }' \
  --protected-settings '{"configurationUrlSasToken":"?<sas>"}'
resource dsc 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = {
  parent: vm
  name: 'DSC'
  location: location
  properties: {
    publisher: 'Microsoft.Powershell.DSC'
    type: 'DSC'
    typeHandlerVersion: '2.83'      // pin the handler; do not float on a config you didn't test
    autoUpgradeMinorVersion: false
    settings: {
      configuration: {
        url: 'https://stbootstrap.blob.core.windows.net/dsc/IIS.zip'
        script: 'IIS.ps1'
        function: 'IISInstall'
      }
      configurationArguments: { WebsiteName: 'shop' }
    }
    protectedSettings: {
      configurationUrlSasToken: '?${dscSasToken}'   // secret → protected block
    }
  }
}

Causes 4–5 — A resource throws, or the node reboot-loops

A single DSC resource throwing (a path that doesn’t exist yet, a feature needing a reboot first, a service not installed) fails the whole apply. The nastier variant is a reboot loop: the config requests a reboot (RebootNodeIfNeeded), the VM reboots, DSC re-applies, finds it still not in desired state, reboots again — forever; the extension never reaches terminal success. Get-DscConfigurationStatus -All shows repeated applies with RebootRequested = True.

Fix. Make the configuration converge: ensure each resource’s preconditions are met in order (use DependsOn), and that a reboot actually resolves the “not in desired state” condition (a config that always reports drift will always reboot). The DSC settings that govern this:

Setting What it controls Default When to change
RebootNodeIfNeeded Whether DSC reboots to apply depends on config Disable while debugging a reboot loop
ConfigurationMode ApplyOnly / ApplyAndMonitor / ApplyAndAutoCorrect ApplyAndMonitor ApplyOnly for one-shot bootstrap
ActionAfterReboot ContinueConfiguration / StopConfiguration ContinueConfiguration Stop to break an investigation loop
configurationArguments Parameters passed to your config none Wrong/missing args → resource throws
Module packaging Modules shipped in the config zip none Always package + pin to avoid version drift

DSC is powerful but heavy; for new bootstrap work, many teams now prefer a CSE that runs a small idempotent script, or bake state into a custom image — DSC shines when you need ongoing enforcement, not one-time setup.

Anatomy of an Azure Monitor Agent (AMA) failure

The Azure Monitor Agent (AzureMonitorWindowsAgent / AzureMonitorLinuxAgent) replaces the retired Log Analytics agent (MMA/OMS). Its failures are special: the extension usually provisions successfully yet no data arrives, because AMA is config-driven by Data Collection Rules and authenticates with a managed identity. “Succeeded” is necessary but nowhere near sufficient. Four causes:

# AMA problem Tell-tale signal Confirm with Real fix
1 Extension install genuinely failed ProvisioningState Failed; install error instanceView substatus; AMA install log Fix prerequisite (OS support, agent deps)
2 No managed identity to authenticate Provisioned, but auth errors in AMA log az vm identity show; AMA log Assign identity; AMA needs it to send data
3 No DCR associated → nothing collected Provisioned, Heartbeat/tables empty az monitor data-collection rule association list Associate a DCR to the VM
4 Egress to workspace/ingestion blocked Provisioned, identity ok, still no data AMA log connection errors; UDR/NSG Open egress / use DCE / fix Private Link

Cause 1 — The install itself failed

Less common, but real: the AMA extension fails to install (unsupported distro/version, a missing dependency, an OS too old) — a genuine Failed provisioning state with an install error in the substatus. Read the substatus and the AMA install log:

az vm get-instance-view -n vm-app01 -g rg-app \
  --query "instanceView.extensions[?contains(name,'AzureMonitor')].substatuses[].message" -o tsv
# Linux AMA logs:
sudo cat /var/log/azure/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent/*.log | tail -100

Fix. Check the agent’s supported OS matrix and prerequisites; use a supported image and a current handler version.

Cause 2 — No managed identity (AMA can’t authenticate)

AMA does not use a workspace key (that was the old MMA model) — it authenticates with the VM’s managed identity to reach the ingestion endpoint. No identity → no authenticated channel → no data, even though the agent is installed and running.

az vm identity show -n vm-app01 -g rg-app -o json   # empty/null = no identity = AMA can't auth

Fix. Assign an identity (system-assigned is simplest for one VM; user-assigned is cleaner at fleet scale):

az vm identity assign -n vm-app01 -g rg-app
resource vm 'Microsoft.Compute/virtualMachines@2024-07-01' = {
  name: 'vm-app01'
  location: location
  identity: { type: 'SystemAssigned' }   // AMA needs an identity to authenticate to the ingestion endpoint
  properties: { /* ... */ }
}

Cause 3 — No Data Collection Rule associated (the #1 “no data” cause)

The canonical AMA trap. The agent installs, has an identity, is perfectly healthy — and collects nothing, because a Data Collection Rule is what tells AMA what to collect (perf counters, syslog, Windows events) and where to send it. An agent with no DCR association is a radio with no station tuned in. Unlike the old agent, AMA has no implicit “send everything to this workspace” default.

Confirm. List the DCR associations for the VM — an empty list is the smoking gun:

VM_ID=$(az vm show -n vm-app01 -g rg-app --query id -o tsv)
az monitor data-collection rule association list --resource "$VM_ID" -o table
# No rows = no DCR = AMA collects nothing, regardless of "Provisioning succeeded"

In the workspace, the Heartbeat table is the fastest proof of life — if AMA were configured and flowing, every agent sends a heartbeat:

Heartbeat
| where TimeGenerated > ago(30m)
| where Computer == "vm-app01"
| summarize LastSeen = max(TimeGenerated) by Computer, Category

No Heartbeat rows for the VM in the last 30 minutes ≈ the agent isn’t authenticated and configured to that workspace.

Fix. Create (or reuse) a DCR and associate it with the VM:

# Associate an existing DCR with the VM (the step everyone forgets)
az monitor data-collection rule association create \
  --name "vm-app01-dcr-assoc" \
  --resource "$VM_ID" \
  --rule-id "/subscriptions/<sub>/resourceGroups/rg-obs/providers/Microsoft.Insights/dataCollectionRules/dcr-linux-perf-syslog"
resource dcrAssoc 'Microsoft.Insights/dataCollectionRuleAssociations@2022-06-01' = {
  name: 'vm-app01-dcr-assoc'
  scope: vm
  properties: {
    dataCollectionRuleId: dcr.id   // without this, AMA installs but ships nothing
  }
}

The AMA “installed but no data” checklist — work it top to bottom:

Check Command / query Healthy result If not
Extension provisioned az vm extension list → AMA status Succeeded Fix install (Cause 1)
VM has an identity az vm identity show non-empty Assign identity (Cause 2)
A DCR is associated az monitor data-collection rule association list ≥ 1 row Associate a DCR (Cause 3)
DCR targets the right workspace DCR destinations your workspace Fix the DCR destination
Data is arriving Heartbeat KQL (last 30 min) rows for the VM Egress/DCE problem (Cause 4)

Cause 4 — Egress to the workspace/ingestion endpoint is blocked

Identity present, DCR associated, agent healthy — and still no data. Now it’s network: AMA can’t reach the ingestion endpoint (or, in a Private Link setup, the Data Collection Endpoint / AMPLS) because an NSG, UDR, or firewall blocks the egress.

Confirm. The AMA log shows connection/timeout errors to the ingestion host; from the box, test outbound to the relevant endpoints (and 168.63.129.16 for agent health). The deep version of this diagnosis is the ingestion-gap playbook: No Logs Showing Up? Troubleshooting Empty Log Analytics Tables and Ingestion Gaps.

Fix. Open the required egress, or wire a Data Collection Endpoint + Private Link for locked-down VNets. The pattern across all four AMA causes is the same ladder — install → identity → DCR association → egress — and you climb it in order, stopping at the first rung that fails. For how AMA, DCRs and workspaces fit into observability, Azure Monitor and Application Insights: Full-Stack Observability is the upstream read.

Architecture at a glance

Hold this mental model and every diagnosis becomes a question of which box on the path failed. Picture the flow left to right. Your deployment tool (Bicep, Terraform, az vm extension set) writes a goal state into ARM: “VM-app01 should have these extensions.” ARM hands the goal to the Azure fabric. Inside the VM — the pivot of the whole picture — sits the VM Guest Agent (waagent on Linux, WindowsAzureGuestAgent on Windows), the one component every extension passes through. It polls the fabric over 168.63.129.16, notices the goal, and downloads the handler package for each extension from an Azure CDN/storage endpoint.

From the agent, the path fans out into one handler per extension, and this is where the three families diverge. The Custom Script handler downloads your files (authenticating to private blob storage with the VM’s managed identity) and runs your command, capturing stdout, stderr and an exit code. The DSC handler compiles your configuration to a MOF, applies it resource-by-resource, and may reboot and resume. The Azure Monitor Agent handler installs the agent binary, which reaches back out — using the VM’s managed identity — to the ingestion endpoint, collecting only what an associated Data Collection Rule tells it to, landing data in a Log Analytics workspace.

Now overlay the failure map. A stuck-at-Creating symptom lives at the agent hop (agent Not Ready, or no route to 168.63.129.16) — nothing downstream started. A download 403 lives at the handler-fetch or blob-auth hop. A non-zero exit lives in your payload box. And the AMA “succeeded but no data” symptom is unique: the install box is green, but the identity, DCR-association, or egress box further along is red. Every path reports status back through the agent to ARM, which is why the agent log (waagent.log / WaAppAgent.log) is the spine: if the agent is sick, every box downstream is suspect; if it’s Ready, the failure is exactly one handler or payload, and the per-extension log tells you which. The first question on every incident — “terminal status, or hung below my payload?” — is just asking which half of this diagram you’re in.

Real-world scenario

Northwind Logistics runs a fleet of Linux web VMs (Ubuntu 22.04, Standard_D2s_v5) in Central India, provisioned by Terraform. Each VM gets two extensions on first boot: a Custom Script Extension that pulls bootstrap.sh from a private storage account and installs the app, and the Azure Monitor Linux Agent for telemetry. The platform team is three engineers; a typical terraform apply rolls five VMs.

The incident began during a routine scale-out. terraform apply ran for forty minutes then failed five VMs at once with “…processing extension ‘CustomScript’. Error message: ‘Enable failed: failed to download files…’”. The on-call engineer’s first reflex: re-run terraform apply. Identical failure. Second reflex: re-create one VM by hand — same failure. An hour in, the scale-out was blocked and a deploy window was closing.

The breakthrough came from the right first question: did the script ever run, or fail before that? The error said “failed to download files,” so the script never executed — not a script bug. The engineer SSH’d into one VM (it was Running; only the extension failed) and ran sudo grep -iE "download|403|denied" /var/log/waagent.log, which showed an HTTP 403 fetching bootstrap.sh. The script was fine; the VM couldn’t authorise the download. The cause: that morning, stbootstrap had been moved behind selected networks with public access tightened, and a Terraform module refactor had dropped the user-assigned managed identity the CSE relied on to read the blob. The managedIdentity reference resolved to no identity, the blob refused anonymous access, and the download 403’d before the script ran. Every VM failed identically — the tell that it was infrastructure, not code.

A second, quieter problem lurked. Even on VMs where the download was manually fixed, the Azure Monitor Agent showed Provisioning succeeded but dashboards were blank — the team had assumed “agent installed = telemetry flowing.” az monitor data-collection rule association list --resource <vmId> returned zero rows: the refactor had also dropped the DCR association. The agent was a radio with no station tuned in.

The fix landed in two parts. Immediately: restore the managed identity and grant it Storage Blob Data Reader on stbootstrap, clearing the 403 — terraform apply went green. Within the day: re-add the dataCollectionRuleAssociation so every VM associates the standard dcr-linux-perf-syslog rule on creation, and Heartbeat rows appeared in minutes. They also hardened the script with set -euo pipefail and idempotent installs. The next scale-out rolled five VMs in eight minutes with telemetry flowing. The lesson on the wall: “‘Provisioning failed’ and ‘provisioning succeeded’ are both lies until you read the layer underneath — the agent log for the failure, the Heartbeat query for the success.”

The incident as a timeline, because the order of moves is the lesson:

Time Symptom Action taken Effect What it should have been
T+0 CSE “failed to download files” on 5 VMs (apply fails) Ask: did the script run, or fail before it?
T+5 Same failure Re-run terraform apply Identical failure Don’t re-run a deterministic failure
T+20 Still failing Re-create one VM by hand Same 403 Stop; read the agent log
T+40 Root cause SSH in; grep 403 waagent.log 403 on blob download This was the breakthrough
T+50 Cause confirmed Check az vm identity show (empty) Missing managed identity
T+60 CSE fixed Restore identity + Storage Blob Data Reader Download works, apply green Correct fix
+1 day AMA “no data” found dcr association list (0 rows) No DCR associated Re-add DCR association in IaC

Advantages and disadvantages

The agent-driven extension model both causes this class of problem and makes it diagnosable. Weigh it honestly:

Advantages (why this model helps you) Disadvantages (why it bites)
Configure a VM as code in the same deployment that creates it — one terraform apply installs, configures, and monitors The status ARM shows is a summary of a summary; the real cause hides in on-box agent/handler logs
The guest agent captures stdout, stderr and exit codes automatically — you rarely lack data You must SSH/RDP (or run-command) into the box to read it; the portal alone often isn’t enough
Idempotent, declarative configuration (DSC) can enforce state over time, not just set it once DSC is heavy: long applies, reboots, WMF/module version mazes, and reboot loops
Managed identity makes secret-free, auditable downloads possible (no SAS in config) A missing/mis-scoped identity fails the download with an opaque 403 that looks like a script bug
AMA + DCR cleanly separate what to collect from the agent — reusable across a fleet “Provisioning succeeded” for AMA means “installed,” not “flowing” — a silent zero-data failure with no alert
A failed extension is usually deterministic — fix the cause and re-run, not a flaky retry game A failed extension can block the whole VM deploy and stall dependent resources in your pipeline
run-command gives you a break-glass way to run diagnostics through the agent without SSH If the agent itself is Not Ready, even run-command and every extension stall together

The model is right when you want VMs that arrive configured and monitored without hand-touching them. It bites hardest on private-storage bootstraps (download/identity), DSC-heavy estates (apply/reboot complexity), and AMA migrations (the silent no-data trap). The disadvantages are all manageable — but only if you know the agent log is the spine and that “succeeded” needs a second proof for data-shipping extensions.

Hands-on lab

Reproduce a Custom Script Extension failure on purpose, read it the way you would in an incident, then fix it — all on the cheapest VM (delete at the end). Run in Cloud Shell (Bash).

Step 1 — Variables and resource group.

RG=rg-ext-lab
LOC=centralindia
VM=vm-ext-lab
az group create -n $RG -l $LOC -o table

Step 2 — Create a small Linux VM.

az vm create -n $VM -g $RG --image Ubuntu2204 --size Standard_B1s \
  --admin-username azureuser --generate-ssh-keys -o table

Expected: a JSON/table row with provisioningState: Succeeded and a public IP.

Step 3 — Run a Custom Script Extension that deliberately fails (exit 1). This mirrors the most common CSE failure — a script that returns non-zero:

az vm extension set -n CustomScript --publisher Microsoft.Azure.Extensions \
  --vm-name $VM -g $RG \
  --protected-settings '{"commandToExecute":"echo starting; ls /no/such/path; exit 1"}'

Expected: the command fails with “VM has reported a failure when processing extension ‘CustomScript’.”

Step 4 — Read it the way you would in an incident. First what ARM saw:

az vm get-instance-view -n $VM -g $RG \
  --query "instanceView.extensions[?contains(name,'CustomScript')].substatuses[].message" -o tsv

The substatus carries your script’s stderr (the ls error) and the exit code. Then read the real thing on the box via run-command (no SSH needed):

az vm run-command invoke -n $VM -g $RG --command-id RunShellScript \
  --scripts "tail -n 50 /var/log/waagent.log; echo '--- stdout ---'; cat /var/lib/waagent/custom-script/download/0/stdout 2>/dev/null; echo '--- stderr ---'; cat /var/lib/waagent/custom-script/download/0/stderr 2>/dev/null"

You’ll see the script’s starting echo, the ls error in stderr, and exit code 1 — proof the script ran and failed (this is a payload bug, not a download/agent bug).

Step 5 — Fix it by replacing the command with one that succeeds. Because the config changed, CSE re-runs:

az vm extension set -n CustomScript --publisher Microsoft.Azure.Extensions \
  --vm-name $VM -g $RG \
  --protected-settings '{"commandToExecute":"echo bootstrap-ok; uname -a; exit 0"}'

Expected: the command succeeds; provisioningState for the extension is now Succeeded.

Step 6 — Confirm the green state and prove the difference between “ran” and “succeeded.”

az vm get-instance-view -n $VM -g $RG \
  --query "instanceView.extensions[].{name:name, state:statuses[0].displayStatus}" -o table

Expected: CustomScriptProvisioning succeeded.

Validation checklist. You reproduced a CSE failure from a non-zero exit, read both the ARM substatus and the on-box stdout/stderr via run-command, and fixed it by changing the command (which is what re-triggers CSE). The steps map to what each proves:

Step What you did What it proves Real-world analogue
3 CSE with exit 1 Non-zero exit = extension failure The everyday script bug
4 instanceView + run-command log read The truth is on the box, reachable without SSH The 5-minute diagnosis
5 New command (config change) CSE only re-runs when config changes Forcing a re-run after a fix
6 Green instanceView “Provisioning succeeded” is the terminal state Confirming the fix landed

Cleanup (avoid lingering charges).

az group delete -n $RG --yes --no-wait

Cost note. A Standard_B1s VM is a few rupees per hour; an hour of this lab is well under ₹50, and deleting the resource group stops everything.

Common mistakes & troubleshooting

This is the playbook — the part you bookmark. First as a scannable table you can read mid-incident, then the same entries with the full confirm-command detail underneath. It spans the basic failures (a script exits non-zero, a re-run that won’t trigger) and the advanced ones (a dead guest agent, a DSC reboot loop, an AMA with no DCR).

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 “Enable failed” with an exit code and your stderr Your script exited non-zero az vm get-instance-view ... substatuses[].message; on-box CSE stdout/stderr Fix the script; set -euo pipefail; return honest exit codes
2 “failed to download files” — script never ran Blob auth (403) or no egress to storage `grep -iE "download 403
3 CSE 403 on a private blob specifically VM has no/wrong managed identity for the download az vm identity show (empty); az role assignment list --assignee <principalId> Assign identity; grant Storage Blob Data Reader; reference it in protectedSettings.managedIdentity
4 Extension stuck at Creating/Transitioning 20+ min Guest agent Not Ready or no route to 168.63.129.16 az vm get-instance-view ... vmAgent.statuses; Test-NetConnection 168.63.129.16 -Port 80 Restart/repair the agent; restore fabric egress
5 Every extension on the VM fails/stalls at once Guest agent unhealthy (not just one handler) vmAgent.statuses = Not Ready; systemctl status walinuxagent Restart agent; check time/clock skew; re-image agent if missing
6 Fixed the script in storage but CSE won’t re-run CSE only re-executes when its config changes az vm extension show settings unchanged Bump settings.timestamp (or change any setting) to force re-run
7 Windows script from a Windows editor fails oddly on Linux CRLF line endings break the shell script Script has \r; errors like command not found on valid lines Save LF; or set skipDos2Unix:false so CSE converts
8 DSC “configuration failed” early Config didn’t compile / module missing DSC handler log; compile error; Get-DscConfigurationStatus Compile locally; package + pin modules in the config zip
9 DSC applies, reboots, applies, reboots — forever Reboot loop: config never converges Get-DscConfigurationStatus -All repeated RebootRequested=True Make config converge; gate RebootNodeIfNeeded; fix the drifting resource
10 AMA “Provisioning succeeded” but dashboards blank No DCR associated → agent collects nothing az monitor data-collection rule association list --resource <vmId> (0 rows); Heartbeat KQL empty Associate a DCR to the VM
11 AMA installed, DCR set, still no data No managed identity, or blocked egress to ingestion/DCE az vm identity show; AMA log connection errors Assign identity; open egress / wire DCE + Private Link
12 “Another operation in progress” / conflict Concurrent VM/extension operation Activity log; overlapping deploys Serialise — one extension op at a time; wait for the prior to finish
13 Exit code 51 from CSE Handler refused: unsupported OS/distro or skipped op Handler log; OS vs supported matrix Use a supported image and handler version
14 Provisioning “succeeded” but the app is broken Script swallowed an error and exit 0’d Your own script log; no error surfaced anywhere Remove `
15 Extension fine on create, fails on later re-deploy Script not idempotent (assumes a clean box) Re-run hits “already exists”/half-state Make every step idempotent (check-then-act)

The expanded form, with the full reasoning for the entries that bite hardest:

1. “Enable failed” with an exit code and your script’s stderr. Root cause: Your script ran and returned a non-zero exit code; the handler faithfully reported it as the extension failure. Confirm: az vm get-instance-view -n <vm> -g <rg> --query "instanceView.extensions[?contains(name,'CustomScript')].substatuses[].message"; on Linux read /var/lib/waagent/custom-script/download/0/stdout and stderr. Fix: Fix the failing line. Add set -euo pipefail (bash) so a mid-script failure actually fails the script; log loudly; return an honest exit code.

2. “failed to download files” — the script never executed. Root cause: CSE couldn’t fetch the file from storage — either a 403 (private blob, missing identity, expired SAS) or no outbound route to storage. Confirm: sudo grep -iE "download|403|denied|timed out" /var/log/waagent.log. A 403 = auth; a timeout/refused = egress. Fix: For auth, give the VM a managed identity + Storage Blob Data Reader and reference it (protectedSettings.managedIdentity), or use a valid, narrowly-scoped SAS. For egress, fix the NSG/UDR/firewall — see Diagnosing Connectivity with Network Watcher: Connection Monitor, Connection Troubleshoot and Next Hop.

3. CSE 403 specifically on a private blob, identity-related. Root cause: The VM has no managed identity, the wrong one, or the identity lacks the data-plane role (Reader on the resource is not enough — blob data needs Storage Blob Data Reader). Confirm: az vm identity show -n <vm> -g <rg> (empty = none); az role assignment list --assignee <principalId> --scope <storageId>. Fix: Assign the identity, grant Storage Blob Data Reader at the account/container scope, and reference it in the extension’s protected settings. Background: Managed Identities Demystified: System vs User-Assigned and When to Use Each.

4. Extension stuck at Creating/Transitioning for 20+ minutes. Root cause: A layer below your payload never finished — the guest agent is Not Ready or can’t reach the fabric host 168.63.129.16 to fetch goal state/handler. Confirm: az vm get-instance-view ... --query "vmAgent.statuses[].displayStatus" (Not Ready), and from the box Test-NetConnection 168.63.129.16 -Port 80 / curl -m 5 http://168.63.129.16/. Fix: Restart the agent (systemctl restart walinuxagent / Restart-Service WindowsAzureGuestAgent); restore outbound to 168.63.129.16 and the agent CDN; check for clock skew (a badly wrong clock breaks the agent’s TLS to the fabric).

5. Every extension on the VM fails or stalls together. Root cause: It’s the guest agent, not any one handler — a single shared dependency failing all of them. Confirm: vmAgent.statuses shows Not Ready; systemctl status walinuxagent (or the Windows services) shows it stopped/crashed; the image may simply lack the agent. Fix: Restart or reinstall the agent; verify the image includes it; check time sync. Once the agent is Ready, re-trigger the extensions.

The remaining entries are tighter — symptom, the one-line root cause, and the move:

Best practices

Security notes

The security knobs that also prevent these incidents — secure and reliable pull in the same direction:

Control Setting / mechanism Secures against Also prevents
Managed identity + data-plane role protectedSettings.managedIdentity + Storage Blob Data Reader Secrets in config; over-broad access The opaque 403 download failure
Protected settings protectedSettings block Plaintext secrets in the resource Settings that “work” but leak
Storage network lockdown Selected networks / Private Endpoint Public exposure of bootstrap files Public-blob “fixes” that bypass auth
RBAC on extension write .../extensions/write permission Unauthorised remote-root via CSE Rogue/accidental extension changes
Handler version pinning typeHandlerVersion + autoUpgradeMinorVersion:false Unexpected handler behaviour change Surprise breakage on an unchanged config
SAS scope + expiry (if used) Read-only, single-blob, short TTL Standing credential leak Expired-SAS 403s caught early

Cost & sizing

Extensions themselves are free — you don’t pay for CustomScript, DSC, or the agent extension as a line item. The cost shows up indirectly, in time and the resources the extension touches:

A rough picture for a small fleet: extensions add ₹0 directly; AMA-driven Log Analytics ingestion is the variable cost (often ₹1,000s/month by DCR breadth and VM count); compute is briefly held during long applies. The cost drivers and what each buys:

Cost driver What you pay for Rough magnitude What it buys Watch-out
The extension itself Nothing ₹0 The mechanism
VM time during the extension Compute while it runs Minutes of the VM’s hourly rate Configuration-as-code Long DSC/CSE holds the deploy
AMA → Log Analytics ingestion GB ingested + retention Variable; can exceed VM cost Telemetry you can query A broad DCR quietly overspends
Storage for bootstrap files Blob storage + egress Negligible per script Versioned, private artifacts Large pulls × many VMs add up
Custom image (alternative) Image build + storage One-time build + small storage Boots ready; no slow per-VM CSE Image maintenance overhead

Interview & exam questions

1. A VM extension is stuck at “Creating” for 30 minutes with no error. What’s the most likely layer, and how do you confirm? A layer below the payload — most likely the guest agent is Not Ready or can’t reach the Azure fabric at 168.63.129.16 to fetch goal state/the handler. Confirm with az vm get-instance-viewvmAgent.statuses (Not Ready) and, on the box, Test-NetConnection 168.63.129.16 -Port 80. A stuck (non-terminal) state is an agent/egress problem; a Failed state is a payload/handler problem.

2. Custom Script Extension fails with “failed to download files.” Did your script run? What do you check? No — the failure is before execution; the handler couldn’t fetch the file. Check waagent.log for the download line and HTTP code: a 403 means auth (private blob, missing managed identity, expired SAS), a timeout means no egress to storage. Fix the identity/role or the NSG/UDR accordingly.

3. What’s the difference between an extension’s provisioning state and its handler status? The provisioning state (Succeeded/Failed) says the handler ran and returned; the handler status/substatus (and your own log) says whether the work was correct. A script can exit 0 on a swallowed error → provisioning “Succeeded” while the app is broken. Always read the substatus, not just the green tick.

4. The Azure Monitor Agent shows “Provisioning succeeded” but no data appears in Log Analytics. Most common cause? No Data Collection Rule is associated with the VM. AMA is config-driven and has no implicit default — without a DCR it collects nothing despite being installed and healthy. Confirm with az monitor data-collection rule association list --resource <vmId> (0 rows) and an empty Heartbeat query; fix by associating a DCR.

5. AMA has a DCR associated and the VM has an identity, but still no data. What’s left? Network egress to the ingestion endpoint (or the Data Collection Endpoint / AMPLS in a Private Link design) is blocked by an NSG/UDR/firewall. The AMA log shows connection/timeout errors to the ingestion host. Fix the egress or wire a DCE + Private Link.

6. Why does AMA need a managed identity when the old Log Analytics agent used a workspace key? AMA authenticates to the ingestion endpoint with the VM’s managed identity (Entra ID), not a shared workspace key — more secure and centrally revocable. No identity means no authenticated channel and therefore no data, even though the agent is installed.

7. You fixed the script in blob storage, but re-running the deployment doesn’t re-run the Custom Script Extension. Why? CSE re-executes only when its configuration changes, not when the referenced file changes. Bump the settings.timestamp integer (or change any setting) to force a fresh run. This catches many teams who update the blob and expect the extension to notice.

8. A DSC extension applies, reboots, and re-applies endlessly. What’s happening and how do you break it? A reboot loop: the configuration requests a reboot but never converges (a resource always reports “not in desired state”). Confirm with Get-DscConfigurationStatus -All showing repeated RebootRequested=True. Break it by fixing the always-drifting resource, and temporarily disabling RebootNodeIfNeeded or setting ActionAfterReboot=StopConfiguration to investigate.

9. Where do you read the real output of a Custom Script Extension on a Linux VM? In the handler’s capture of your script’s stdout/stderr (e.g. /var/lib/waagent/custom-script/download/<n>/stdout and stderr) and the status file under the handler’s status/ directory; the agent log (/var/log/waagent.log) covers download/agent issues. You can reach all of these via az vm run-command without SSH.

10. What does exit code 0 vs non-zero mean to a Custom Script Extension, and why does it matter for reliability? 0 = success, non-zero = failure, and that code is what bubbles to ARM as the extension result. It matters because a script that does real work but returns 0 on error makes the extension “succeed” with a broken VM — the worst failure mode since nothing alerts. Honest exit codes (set -euo pipefail, no blanket || true) are the foundation of reliable bootstrapping.

11. Why can adding a Custom Script Extension be a security-sensitive action? Because the extension runs through the guest agent as root/SYSTEM — anyone with Microsoft.Compute/virtualMachines/extensions/write on a VM effectively has remote code execution on it. The permission should be tightly RBAC-scoped and audited; it’s as powerful as shell access.

12. An entire VM’s extensions all fail at once. Is it more likely your script or the platform? The platform/agent — when every extension fails or stalls together, suspect the shared dependency: the guest agent is Not Ready (stopped, crashed, missing from the image, or clock-skewed) or there’s no route to the fabric. A single script bug fails one extension while the agent stays Ready.

These map to AZ-104 (Administrator)deploy and manage Azure compute resources, VM extensions, and monitor resources with the Azure Monitor Agent and DCRs — and AZ-204 (Developer) where bootstrap/config-as-code and managed identities appear. The networking-egress angle (fabric, ingestion endpoints, Private Link) touches AZ-700, and the identity/RBAC angle touches AZ-500. A compact cert-mapping for revision:

Question theme Primary cert Exam objective area
Extensions, guest agent, instanceView AZ-104 Deploy & manage VMs
Azure Monitor Agent + DCR associations AZ-104 Monitor Azure resources
Managed identity for downloads AZ-204 / AZ-500 Implement secure access; manage identities
CSE/DSC bootstrap as code AZ-204 Provision & configure compute
Egress to fabric/ingestion endpoints AZ-700 Design & implement network connectivity
RBAC on extension write (remote-root) AZ-500 Manage security operations

Quick check

  1. An extension sits at “Creating” for 25 minutes with no error message. Is this more likely a script bug or a guest-agent/egress problem, and what’s the one command you run first?
  2. Custom Script Extension fails with “failed to download files.” Did your script execute, and what’s the first log line you grep for?
  3. True or false: changing the bootstrap.sh file in blob storage will cause the Custom Script Extension to re-run on the next deployment.
  4. The Azure Monitor Agent reports “Provisioning succeeded” but Log Analytics shows no data. Name the single most common cause and the command that confirms it.
  5. A bootstrap script does its work and returns exit 0, but the application is broken. What failure class is this, and why is it the most dangerous?

Answers

  1. A guest-agent/egress problem — a stuck (non-terminal) state means a layer below your payload never finished. Run az vm get-instance-view -n <vm> -g <rg> --query "vmAgent.statuses[].displayStatus"; if it’s Not Ready, the agent (or its route to 168.63.129.16) is the issue, not your script.
  2. No — the download failed before the script ran. Grep /var/log/waagent.log for download/403/denied: a 403 is auth (managed identity / SAS), a timeout is egress.
  3. False. CSE re-runs only when its configuration changes, not when the referenced file changes. Bump settings.timestamp (or change any setting) to force a re-run.
  4. No Data Collection Rule is associated with the VM (AMA has no implicit default, so it collects nothing). Confirm with az monitor data-collection rule association list --resource <vmId> returning 0 rows (and an empty Heartbeat query).
  5. A silent success-on-failure (the script swallowed an error and exit 0’d). It’s the most dangerous because the provisioning state is green and nothing alerts, so a broken VM looks healthy. Fix with set -euo pipefail, no blanket || true, and honest exit codes.

Glossary

Next steps

You can now localise any extension failure to the agent, the handler, or your payload and fix it. Build outward:

AzureVirtual MachinesTroubleshootingVM ExtensionsCustom Script ExtensionDSCAzure Monitor AgentGuest Agent
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading