Azure Troubleshooting

Troubleshooting Azure Deployment Failures: Reading ARM Error Codes and Activity Logs

The pipeline goes red on the last step: Deployment failed. Correlation ID: 6f2a…. One line. No stack trace, no obvious file, just a GUID and a vague verb. You re-run it — same failure. You open the portal, find a long list of green resources and one red one, click in, and get a JSON blob whose top-level message says At least one resource deployment operation failed. — which tells you nothing new. This is the most common friction point in infrastructure-as-code on Azure, frustrating for one specific reason: Azure Resource Manager (ARM) — the control plane every az, Bicep, Terraform, portal and SDK call goes through — reports failures as a nested tree of operations, and the message at the top is almost never the one that matters. The real error is buried two or three levels down, attached to the one resource provider that rejected your request.

This is the diagnostic playbook for reading that tree. We treat a failed deployment not as one error but as a layered failure: ARM validates the template shape first (a syntax/schema layer), then evaluates it against the live environment (preflight), then submits each resource to its resource provider (the Microsoft.Compute, Microsoft.Network, Microsoft.Storage services that actually create things) — and any layer can fail with a completely different class of error. You will learn to localise a failure to one layer and one resource, using the tools that tell the truth: az deployment group show, the deployment operations list, the Activity Log with its correlation ID, az deployment ... what-if, and the provider status messages. Every diagnosis comes with the exact command or portal path to confirm it and the precise fix, with az CLI and Bicep where it applies. Because this is a mid-incident reference, the playbook, error codes, modes and limits are all laid out as scannable tables.

By the end you will stop re-running and praying. When a deployment fails you will know within two minutes whether you face a template that never parsed, a policy that denied the request, a colliding name, a quota you blew through, an out-of-order dependency, or a provider that simply rejected your payload — and the one command that proves which.

What problem this solves

ARM gives you declarative deployments: describe desired state in a template, submit it, and Azure makes reality match. That abstraction is a gift until it breaks — then the declarative model works against you, reporting what failed but rarely why in the same place, because one template can touch a dozen resource providers, each with its own error vocabulary. The top-level DeploymentFailed message is deliberately generic; the actionable detail lives in a child operation you have to go find.

What breaks without this skill: an engineer re-runs the pipeline (sometimes it “passes” because a transient throttle cleared, teaching the wrong lesson), comments out the failing resource to get green, or escalates with a screenshot of the unhelpful top message. Meanwhile the actual cause — a RequestDisallowedByPolicy, a globally-taken storage name, a QuotaExceeded on a VM family, or a ResourceNotFound from out-of-order deployment — sits in the operation tree, perfectly diagnosable, unread.

Who hits this: everyone who deploys IaC on Azure — Bicep and ARM JSON authors directly, and Terraform users too, because azurerm calls the same control plane and surfaces the same provider errors. It bites hardest on first deployments to a new subscription (policy and quota surprises), templates with cross-resource references (ordering races), anything with globally-unique names (storage, Key Vault, ACR), and CI/CD where the failure is one log line. The fix is almost never “re-run” — it’s “open the tree, find the red leaf, read its code and message, act on that.”

Every failure layer this article covers, the question it forces, and where to look first:

Failure layer What ARM is telling you First question to ask First place to look Most common single cause
Template validation “Your template isn’t valid JSON/Bicep” Did it even parse and compile? az bicep build / CLI parse error Bad expression, missing param, bad reference
Preflight (validation) “Valid shape, but it won’t work here” Would this succeed against the live env? az deployment ... validate / what-if Quota, name taken, policy, bad SKU/region
Resource provider “A provider rejected one resource” Which resource, and what did its provider say? az deployment group show → operations Provider-specific: quota, conflict, bad value
Dependency / ordering “A resource needed something not ready” Did things deploy in the right order? Operation timestamps + resourceNotFound Missing dependsOn / implicit reference
Authorization “You/the SP can’t do this” Does the identity have the role at this scope? Activity Log → AuthorizationFailed Missing RBAC on the deploying principal

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should understand ARM basics: a deployment is a request to create/update resources from a template, scoped to a resource group, subscription, management group or tenant; a template (ARM JSON or Bicep, which compiles to ARM JSON) declares the resources; a resource provider (like Microsoft.Compute) fulfils each. You should be comfortable running az in Cloud Shell, reading JSON, and authoring a basic Bicep file. New to Bicep? Deploy your first Bicep file from scratch is the starting point, and Bicep vs ARM vs Terraform: choosing IaC on Azure frames where each tool sits.

This sits in the Observability & Troubleshooting track for IaC. It pairs tightly with Bicep what-if and preflight validation as a CI gate — preflight is the most effective way to turn a deployment failure into a PR comment. Many errors here are governance errors, so Azure Policy effects explained: Deny, Audit, Modify, DeployIfNotExists is the layer that emits RequestDisallowedByPolicy, and authorization failures map onto Azure RBAC fundamentals.

Who owns each layer during a failed deployment, so you call the right person fast:

Layer What lives here Who usually owns it Failure classes it causes
Template (Bicep/JSON) Syntax, schema, expressions, params App / IaC author InvalidTemplate, BCPxxxx compile errors
Deployment engine (ARM) Ordering, mode, preflight, the tree Platform / ARM (Microsoft) DeploymentFailed, ResourceNotFound, ordering
Resource provider Actual create/update of one resource Microsoft (per service) QuotaExceeded, Conflict, SkuNotAvailable
Governance (Policy) Deny/Modify rules on the scope Cloud governance team RequestDisallowedByPolicy
Identity / RBAC Permissions of the deploying principal Identity / platform AuthorizationFailed, LinkedAuthorizationFailed
Quota / capacity Per-region, per-family limits Platform + Microsoft support QuotaExceeded, SkuNotAvailable, AllocationFailed

Core concepts

Five mental models make every later diagnosis obvious.

A deployment is a tree, and the message at the top is the summary, not the cause. Submit a template and ARM creates one deployment object whose status rolls up many child deployment operations — one per resource (and nested deployment). If any leaf fails, the root reports Failed with a generic At least one resource deployment operation failed. The real error — a code and message from one resource provider — is attached to the failing leaf. Reading the tree, not the root, is the entire skill: az deployment group show gives the root; az deployment operation group list gives the leaves.

There are two failure regimes: shape and reality. Before ARM submits anything, it checks the template’s shape — valid JSON/Bicep, matching schemas, evaluable expressions. Failures here are InvalidTemplate (or a Bicep BCPxxxx) and happen before any resource is touched, so nothing is half-created. Then ARM checks against realitypreflight — and submits each resource; failures here (QuotaExceeded, Conflict, RequestDisallowedByPolicy) can leave a partial deployment, because some resources may have succeeded before one failed. The regime tells you whether you have a mess to clean up.

The resource provider is the one that actually says no. ARM is a router. Conflict, QuotaExceeded, SkuNotAvailable don’t come from ARM — they come from Microsoft.Network or Microsoft.Compute, wrapped and bubbled up. That’s why the detail lives in a nested statusMessage.error, and why the same top-level DeploymentFailed can mean a dozen things. Read a leaf and you read the provider’s words.

Order is inferred, and inference is fallible. ARM deploys in parallel where it can and serialises where one resource references another (implicit) or you declared dependsOn (explicit). Reference B from A by a hard-coded string and ARM doesn’t know A needs B — it runs them in parallel, and A fails with ResourceNotFound because B isn’t there yet, intermittently. Correct references make the ordering deterministic.

The correlation ID is the thread that ties it together. Every deployment — and every operation it spawns, including the provider-side calls a pipeline never logs — shares one correlation ID. When the pipeline gives you nothing but Correlation ID: 6f2a…, that GUID is your key into the Activity Log: filter by it and you see every event for the deployment, with the provider error attached. It’s the difference between “the pipeline failed” and “the Microsoft.Network/virtualNetworks write at 14:32 was denied by policy deny-public-ip.”

The vocabulary in one table

Before the deep sections, pin down every moving part — the mental model side by side (the glossary repeats these for lookup):

Concept One-line definition Where it lives Why it matters to a failure
Deployment One request to apply a template Resource group / sub / MG / tenant The root that reports Failed
Deployment operation The create/update of one resource in a deployment Under the deployment The leaf that carries the real error
Resource provider The service that fulfils a resource type Microsoft.* namespace Emits the provider-specific error
Template ARM JSON / Bicep describing desired state Repo / inline A bad shape → InvalidTemplate
Preflight (validation) Pre-submit check against the live env ARM, before create Catches quota/name/policy early
Deployment mode Incremental vs Complete On the deployment Complete can delete unlisted resources
Correlation ID GUID shared by all ops of one deployment Activity Log Ties pipeline failure to provider error
dependsOn Explicit ordering between resources Template Wrong/missing → ResourceNotFound race
Activity Log Control-plane audit of all operations Subscription / RG Where the buried error is visible
what-if Predicted change set before deploy az deployment ... what-if Surfaces preflight failures pre-apply
Rollback on error Re-deploy a last-known-good on failure Optional deployment flag Turns a failed deploy into a restored one

The ARM error-code reference

The lookup table you scan first: every error code you realistically see from a failed ARM deployment, what it means, the likely cause, how to confirm it, the first fix. The non-obvious ones are the difference between InvalidTemplate (shape, no resources touched) and ResourceDeploymentFailure (a provider rejected a real create), and the family of authorization codes.

Code Meaning Likely cause How to confirm First fix
InvalidTemplate Template shape/schema/expression is invalid Bad function, undeclared param, type mismatch, bad reference() CLI error text names the line/expression; az bicep build Fix the expression/param; recompile
DeploymentFailed One or more child operations failed (generic root) Anything below — read the leaves az deployment operation group list Open the failing operation’s error
ResourceDeploymentFailure A specific resource’s provider call failed Provider rejected the payload Same — the leaf’s statusMessage.error Act on the provider’s message
Conflict Resource state conflicts (exists / in-use / busy) Name taken, immutable prop changed, concurrent op Leaf message; az resource show on the target Use a new name; don’t change immutable props
RequestDisallowedByPolicy An Azure Policy Deny blocked the request A governance Deny rule matched the resource Activity Log error names the policyAssignment Comply, or get an exemption
QuotaExceeded A subscription/region quota would be exceeded Cores/IPs/SKU count over the limit Error names the quota; az vm list-usage Request a quota increase or pick another region/size
SkuNotAvailable The requested SKU isn’t offered here/now SKU not in that region/zone, or capacity-restricted az vm list-skus --location ... --zone Choose an available SKU/region/zone
AllocationFailed Region/cluster couldn’t allocate the capacity Transient capacity shortage for that size/zone Retry; check az vm list-skus restrictions Retry, change size/zone, or use a flexible set
AuthorizationFailed The principal lacks the required action Deploying SP/user missing RBAC at the scope Error names the action + scope Grant the role at the right scope
LinkedAuthorizationFailed Lacks permission on a referenced resource Cross-resource link needs rights on the other one Error names the linked resource + action Grant access to the referenced resource
ResourceNotFound A referenced resource doesn’t exist (yet) Missing dependency, wrong name/ID, ordering race Leaf names the missing resource ID Fix the reference / add dependsOn
InvalidResourceReference A reference()/resourceId points at nothing valid Typo’d name, wrong API version, wrong scope Inspect the expression; az resource show Use a symbolic reference / correct ID
ParentResourceNotFound The parent of a child resource is missing Child deployed before/without its parent Check the parent’s deployment status Deploy/declare the parent; nest correctly
ReservedResourceName The name uses a forbidden word/pattern e.g. reserved words, bad characters/length Naming rules for that resource type Rename per the type’s rules
MissingRegistrationForLocation / NoRegisteredProviderFound The resource provider isn’t registered New subscription, provider never registered az provider show -n Microsoft.X az provider register -n Microsoft.X
DeploymentQuotaExceeded Too many deployments in the RG history (800) Long-lived RG with many deployments az deployment group list count Prune old deployment history
InvalidContentLink / template link 404 A linked/nested template URL can’t be fetched Bad/expired SAS, private URL, wrong path Try the URL; check the SAS expiry Re-issue the link/SAS; make it reachable

Three reading notes that save the most time:

Distinction The trap How to tell them apart
InvalidTemplate vs ResourceDeploymentFailure Only one leaves a mess InvalidTemplate is before any resource is created (nothing to clean up); ResourceDeploymentFailure means some resources may already exist
Root DeploymentFailed vs the real error Hours wasted on the generic root message The root is a summary; the real code/message is on a failing operation — always list operations
Policy Deny vs RBAC denial Both look like “you can’t do this” RequestDisallowedByPolicy names a policyAssignment; AuthorizationFailed names an RBAC action the principal lacks

Anatomy of a failed deployment — reading the tree

The single most important move is to stop reading the root and list the operations — the same five steps every time.

Step 1 — get the deployment’s root status. Confirms it failed and gives you the timestamp and (sometimes) a hint:

# Root status of the latest deployment in a resource group
az deployment group show \
  --resource-group rg-app-prod --name main \
  --query "{state:properties.provisioningState, ts:properties.timestamp, error:properties.error}" -o json

Step 2 — list the operations and find the failing leaf. Where the real error lives — filter to the ones that failed:

# Every operation in the deployment; the failing one carries the provider error
az deployment operation group list \
  --resource-group rg-app-prod --name main \
  --query "[?properties.provisioningState=='Failed'].{resource:properties.targetResource.resourceName, type:properties.targetResource.resourceType, code:properties.statusMessage.error.code, msg:properties.statusMessage.error.message}" \
  -o jsonc

That statusMessage.error.code and .message are the words the resource provider actually said. Everything after this is acting on that, not on the root.

Step 3 — follow the correlation ID into the Activity Log when the leaf is terse. Some errors (policy denials especially) carry their detail in the Activity Log rather than the operation message:

# Pull the correlation ID from the deployment, then list every activity-log event for it
CID=$(az deployment group show -g rg-app-prod -n main --query properties.correlationId -o tsv)
az monitor activity-log list --correlation-id "$CID" \
  --query "[].{time:eventTimestamp, op:operationName.localizedValue, status:status.value, sub:subStatus.value, msg:properties.statusMessage}" \
  -o jsonc

Step 4 — read the provider’s full status message. Activity-log entries often embed a JSON statusMessage whose nested error.details[] is the most specific text Azure gives you — where a policy assignment name or a quota number lives.

Step 5 — confirm against the live resource for a conflict/exists case. When the code is Conflict, check what’s actually there:

az resource show --ids "/subscriptions/<sub>/resourceGroups/rg-app-prod/providers/Microsoft.Storage/storageAccounts/stappprod" \
  --query "{name:name, state:properties.provisioningState}" -o json

In short: step 1 confirms it failed, step 2 almost always hands you the real error, step 3 (the correlation ID) is for when the leaf is too terse, step 4 (error.details[]) is where a policy name or quota number lives, step 5 (az resource show) confirms the live state for a Conflict. In the portal, Resource group → Deployments → Operation details expands the failing row to the provider error, and Monitor → Activity Log filtered by correlation ID gives the same thread.

Anatomy of the template-shape failures (InvalidTemplate / Bicep compile)

These fail before any resource is created — the cheapest failures to have, because nothing is half-built. They split into Bicep compile errors (caught locally with az bicep build) and ARM InvalidTemplate errors (caught at submit): the five classes are a BCPxxxx compile error, an undeclared/unbound parameter, a bad expression/function, a type/schema mismatch, and a circular dependency.

Cause 1 — a Bicep compile error you never built locally

Bicep compiles to ARM JSON; a BCPxxxx error means it never produced valid JSON. Compile before you deploy so the error shows up in your editor, not the pipeline:

# Compile only — surfaces BCPxxxx errors without touching Azure
az bicep build --file main.bicep

Common ones: BCP028 (duplicate symbol), BCP057 (name not in scope), BCP062 (referenced declaration not found). There’s no Azure-side fix — these are pure authoring errors caught for free if you build in CI.

Cause 3 — a bad template expression evaluated at submit

If the shape is valid Bicep but an expression can’t resolve (a malformed resourceId(), a reference() with wrong arguments, a bad interpolation), ARM rejects it as InvalidTemplate naming the expression. Correct it — and prefer Bicep symbolic references (storageAccount.id) over hand-built resourceId() strings, which removes a class of typos:

// Wrong: hand-built resourceId is easy to typo and breaks ordering inference
// properties: { storageId: resourceId('Microsoft.Storage/storageAccounts', 'stappprod') }

// Right: symbolic reference — compiler validates it AND infers the dependency
resource sa 'Microsoft.Storage/storageAccounts@2023-05-01' = { name: 'stappprod', /* ... */ }
resource fn 'Microsoft.Web/sites@2023-12-01' = {
  name: 'fn-app'
  properties: { /* ... */ storageId: sa.id }   // sa.id is validated and creates the dependency
}

The discipline that kills the whole shape-failure class: build Bicep in CI (catches BCPxxxx and circular dependencies), review a params file (catches undeclared parameters), and prefer symbolic references (catches bad expressions at compile time). A validate/what-if step catches the rest — schema and API-version mismatches — at preflight.

Anatomy of the deployment-time failures (a provider said no)

These can leave a partial deployment, because they happen during create/update after some resources already succeeded — and they are the bulk of real incidents. The six classes, each with its confirm-command below: Policy Deny, name conflict, quota, SKU/capacity, authorization, unregistered provider.

Cause 1 — Azure Policy denied the request (RequestDisallowedByPolicy)

The most surprising failure on a governed subscription: your template is perfect, but a Policy Deny rejects the resource — a banned public IP, a disallowed region, a missing tag, a storage account without supportsHttpsTrafficOnly. The code is RequestDisallowedByPolicy and it names the policy assignment that blocked you, in the Activity Log. Pull the correlation ID and read it:

CID=$(az deployment group show -g rg-app-prod -n main --query properties.correlationId -o tsv)
az monitor activity-log list --correlation-id "$CID" \
  --query "[?contains(to_string(properties), 'Policy')].properties.statusMessage" -o tsv
# The JSON names policyAssignmentName, policyDefinitionId and the disallowed reason

Fix. Comply (add the tag, switch region, enable HTTPS-only) — almost always right, because the policy exists for a reason. If the resource genuinely needs an exception, request a scoped policy exemption rather than weakening the policy. See Azure Policy effects explained: Deny, Audit, Modify, DeployIfNotExists for which effects block (only Deny and a failed DeployIfNotExists/Modify; Audit never blocks).

The policy effects and whether each fails your deployment:

Effect Blocks the deployment? What you see What to do
Deny Yes RequestDisallowedByPolicy Comply or get an exemption
DeployIfNotExists Only if its remediation deploy fails Nested deployment error Fix the remediation template / its identity’s RBAC
Modify Rarely (if it can’t apply) Modify failure on the resource Ensure the Modify identity has rights
Audit / AuditIfNotExists No A non-compliant mark only Fix later; it won’t fail the deploy
Append No (mutates the request) Silent property addition Be aware your resource gains properties
Disabled No Nothing

Cause 2 — a name conflict or an immutable-property change (Conflict)

Conflict has two flavours people confuse. Flavour A: the name is already taken — for globally-unique types (Storage account, Key Vault, ACR, App Service hostname, Cosmos DB) “taken” can mean by another tenant, which is why stappprod fails even though you’ve never used it. Flavour B: you tried to change an immutable property on an existing resource (a subnet’s address space while in use, a VM’s OS disk) — the provider refuses because it can’t change in place.

Confirm. For a name clash, test availability before assuming (for an immutable-property conflict, the leaf names the property and az resource show shows the value you’re overwriting):

# Storage account global name availability (true = free to use)
az storage account check-name --name stappprod --query "{available:nameAvailable, reason:reason}" -o json

Fix. For name clashes, make the name deterministically unique — a uniqueString() suffix is stable per resource group but globally unlikely to collide. For immutable-property conflicts, don’t mutate in place — leave it or replace it:

param location string = resourceGroup().location
// Stable per-RG, globally unique — avoids the "name taken" Conflict
resource sa 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: 'st${uniqueString(resourceGroup().id)}'
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: { supportsHttpsTrafficOnly: true, minimumTlsVersion: 'TLS1_2' }
}

The globally-unique types you must name carefully:

Resource type Name scope Length / charset gotcha Make it unique with
Storage account Global 3–24, lowercase + digits only, no hyphens st${uniqueString(resourceGroup().id)} (trim to 24)
Key Vault Global 3–24, alphanumeric + hyphens, start letter kv-${uniqueString(...)}
Container Registry (ACR) Global 5–50, alphanumeric only acr${uniqueString(...)}
App Service (default host) Global (*.azurewebsites.net) DNS-safe include an env/suffix token
Cosmos DB account Global 3–44, lowercase + digits + hyphens cosmos-${uniqueString(...)}
Public IP DNS label Per-region DNS-safe, region-unique suffix with the RG hash

Cause 3 — quota exceeded (QuotaExceeded)

Subscriptions have quotas per region per family: total regional vCPUs, per-family vCPUs (Standard_Dsv5), public IPs, NAT gateways. Deploy past the limit and the provider returns QuotaExceeded naming the quota and the requested-vs-allowed numbers — the classic “works in dev, fails in the new prod subscription” failure. Read usage against the region’s limit:

# Compute (vCPU) usage vs limit in a region — look for currentValue near limit
az vm list-usage --location centralindia \
  --query "[?currentValue >= limit].{name:localName, used:currentValue, limit:limit}" -o table

# Network quotas (public IPs, etc.)
az network list-usages --location centralindia \
  --query "[?currentValue >= limit].{name:name.localizedValue, used:currentValue, limit:limit}" -o table

Fix. Reduce the footprint or request a quota increase — full mechanics in Azure subscription quotas and limits: increase requests. The common quotas that fail deployments:

Quota Typical default scope Symptom How to confirm
Total regional vCPUs Per region QuotaExceeded on VM/VMSS create az vm list-usage --location
Per-family vCPUs (e.g. Dsv5) Per family per region Fails only for that family az vm list-usage (per-family rows)
Public IP addresses (Standard) Per region Fails the public IP resource az network list-usages --location
Storage accounts per subscription Per region (250) Fails new storage account az storage account list count
NAT gateways / load balancers Per region Fails the network resource az network list-usages
Deployments per RG (history) 800 per RG DeploymentQuotaExceeded az deployment group list count

Cause 4 — SKU not available or capacity allocation failed

SkuNotAvailable means the SKU isn’t offered in that region/zone (or is restricted for your subscription). AllocationFailed is different — the SKU is offered, but the cluster couldn’t allocate it right now (a transient shortage, common for large/GPU SKUs). Confirm what’s available, with zones and restrictions:

# What VM sizes are available in a region, and any restrictions/zones
az vm list-skus --location centralindia --resource-type virtualMachines \
  --query "[?name=='Standard_D4s_v5'].{name:name, zones:locationInfo[0].zones, restrictions:restrictions}" -o jsonc

A non-empty restrictions array means the SKU is blocked (often NotAvailableForSubscription). Fixes by code: SkuNotAvailable → choose an available SKU/region/zone; AllocationFailed → retry or change size/zone (capacity fluctuates), or use a flexible VMSS; OverconstrainedAllocationRequest (too many constraints — zone + size + proximity) → relax one constraint or split the request.

Cause 5 — authorization failed (AuthorizationFailed)

The deploying principal — your user, but more often a CI service principal or managed identity — lacks the RBAC action the operation needs at that scope. The error names the action (e.g. Microsoft.Network/virtualNetworks/write) and scope. A subtlety is LinkedAuthorizationFailed: you can create A, but A references B and you lack rights on B. Confirm the assignments, then grant the least-privilege role at the right scope:

# What roles does the deploying SP/MI have at the resource-group scope?
az role assignment list --assignee <appId-or-objectId> \
  --scope /subscriptions/<sub>/resourceGroups/rg-app-prod -o table

For a pipeline using OIDC, GitHub Actions to Azure with OIDC federated credentials wires the identity; the RBAC model is in Azure RBAC fundamentals. The codes, distinguished:

Code Means Where the gap is Fix
AuthorizationFailed Principal lacks the action at the scope On the resource being created Grant a role covering the action
LinkedAuthorizationFailed Lacks rights on a referenced resource On the other (linked) resource Grant access to the linked resource
InvalidAuthenticationToken Token expired/invalid The login session/SP secret Re-authenticate; rotate the SP credential
RoleAssignmentExists The role assignment already exists Re-creating an existing assignment Use a deterministic guid() name; make idempotent

Dependency ordering and the ResourceNotFound race

The most intermittent class of failure. ARM parallelises independent resources and serialises dependent ones — but only knows about a dependency if you express it. Express it wrong and you get a race: sometimes the order is right, sometimes it fails with ResourceNotFound or ParentResourceNotFound. Flaky deployments that “pass on retry” are almost always this. The failing leaf names a resource ID that “doesn’t exist”, and timestamps show the dependent resource started before its dependency:

az deployment operation group list -g rg-app-prod -n main \
  --query "[].{resource:properties.targetResource.resourceName, state:properties.provisioningState, start:properties.timestamp}" \
  -o table

Fix. Use symbolic references wherever one resource needs another — vnet.id, subnet.id — because the compiler turns a reference into an implicit dependsOn, making the order deterministic. Reach for an explicit dependsOn only for a runtime dependency the compiler can’t see (e.g. a role assignment that must exist before a resource uses it but isn’t referenced):

resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
  name: 'vnet-app'
  location: location
  properties: { addressSpace: { addressPrefixes: ['10.20.0.0/16'] } }
}
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
  parent: vnet                      // parent → deterministic ordering
  name: 'snet-app'
  properties: { addressPrefix: '10.20.1.0/24' }
}
resource nic 'Microsoft.Network/networkInterfaces@2023-11-01' = {
  name: 'nic-app'
  location: location
  properties: {
    ipConfigurations: [{
      name: 'ipconfig1'
      properties: { subnet: { id: subnet.id } }   // reference → implicit dependsOn
    }]
  }
  // dependsOn: [ roleAssignment ]   // only for a non-referenced runtime dependency
}

The ordering failures and the right tool for each:

Symptom Code Cause Fix
Child fails, parent not ready ParentResourceNotFound Child declared without parent/nesting Use parent: or proper nesting
References a resource that “doesn’t exist” ResourceNotFound Hand-built ID instead of symbolic ref Use the symbolic .id
Passes sometimes, fails sometimes ResourceNotFound (intermittent) True race; no expressed dependency Add the reference or dependsOn
Two resources deadlock CircularDependency (build-time) A references B references A Break the cycle; extract a module
Runtime need not visible to compiler ResourceNotFound at use-time E.g. RBAC must exist before use Explicit dependsOn on the role assignment

Deployment modes, scope and rollback

Three mechanics that change what a failure means. Get them wrong and a “failed” deployment does more damage than the bug.

Deployment mode — Incremental vs Complete. Incremental (default) adds/updates your template’s resources and leaves everything else alone. Complete makes the group match the template exactly — so it deletes resources in the group that aren’t in your template. A Complete deployment missing a resource you forgot to include will delete it: the single most dangerous footgun in ARM. Always know which mode you’re in:

# Incremental (safe default) vs Complete (deletes unlisted resources in the RG)
az deployment group create -g rg-app-prod -n main -f main.bicep --mode Incremental
# az deployment group create -g rg-app-prod -n main -f main.bicep --mode Complete  # DANGER: prunes

Deployment scope. Resource-group deployments are the common case, but you can also deploy at subscription, management group and tenant scope. The error tree is identical; only the verb changes:

Scope Command Deploys things like Common failure here
Resource group az deployment group create VMs, networks, apps Most provider errors
Subscription az deployment sub create RGs, sub-level policy, budgets Scope/permission, RG-name conflict
Management group az deployment mg create Policy, role assignments, sub placement AuthorizationFailed at MG scope
Tenant az deployment tenant create MGs, tenant-wide assignments Rarely used; high-privilege only

Rollback on error. By default a failed deployment leaves whatever succeeded in place (a partial deployment). Opt into rollback-on-error and on failure ARM re-deploys the last successful deployment (or a --rollback-on-error <name> you pin) to restore known-good state — use it when a partial state is worse than rolling back:

# On failure, redeploy the last successful deployment instead of leaving a partial state
az deployment group create -g rg-app-prod -n main -f main.bicep \
  --rollback-on-error

Architecture at a glance

The diagram traces a deployment request as it flows through Azure Resource Manager, mapping each failure class onto the stage where it bites. Read it left to right. A client — your az/Bicep CLI, a Terraform azurerm apply, the portal, or a CI runner — submits a template to the ARM control plane. ARM first runs validation and preflight: it parses and schema-checks the template (a bad shape fails here as InvalidTemplate, badge 1, before any resource exists) and checks it against the live environment. Before or during that check, Azure Policy can reject the request outright with RequestDisallowedByPolicy (badge 2) — a governance Deny, not a code bug.

Once admitted, ARM fans each resource out to its resource providerMicrosoft.Compute, Microsoft.Network, Microsoft.Storage — in dependency order. This is where most real failures land: a provider returns QuotaExceeded or Conflict (badge 3) when a limit or name collides, and a mis-expressed dependency produces a ResourceNotFound race (badge 4) when a resource is created before the thing it references. Every one is written to the Activity Log under a single correlation ID (badge 5), the thread you pull when the pipeline gives you nothing but a GUID. The whole method is on the page: localise the failure to a stage, read the provider’s words there, confirm via the tree or the correlation ID, fix the layer that said no.

Azure Resource Manager deployment path from a client (az/Bicep, Terraform, portal, CI) into the ARM control plane, where template validation and preflight catch InvalidTemplate shape errors and Azure Policy can reject the request with RequestDisallowedByPolicy, then ARM fans each resource out in dependency order to resource providers Microsoft.Compute, Microsoft.Network and Microsoft.Storage where QuotaExceeded, Conflict and ResourceNotFound race failures occur, with every operation recorded in the Activity Log under one correlation ID, annotated with five numbered failure badges mapping each error class to the stage where it bites

Real-world scenario

Northwind Logistics runs a nightly Bicep deployment that stands up a per-customer environment — resource group, VNet, a Standard_D4s_v5 VM scale set, a storage account and a Key Vault — via a GitHub Actions workflow using an OIDC-federated service principal with Contributor on the subscription. It had run clean for months. Then, onboarding their biggest customer into a brand-new prod subscription in Central India, the workflow went red at 02:10: Deployment failed. Correlation ID: 6f2a8c1e….

The on-call engineer re-ran it — same failure. The GitHub log showed only At least one resource deployment operation failed., so they went to the tree. az deployment operation group list showed three failing leaves, not one — the breakthrough, because the three errors were unrelated and at three different layers. The VM scale set read QuotaExceeded (Standard_Dsv5 vCPUs: requested 16, limit 10 in the new subscription); the storage account read Conflict (stnorthwindprod taken as a global name); the Key Vault read RequestDisallowedByPolicy.

They worked each leaf against its own confirm-command. az vm list-usage --location centralindia showed the Dsv5 family capped at 10 vCPUs in the fresh subscription (defaults are lower than a seasoned subscription’s raised limits). az storage account check-name --name stnorthwindprod returned available: false — a global collision, not a permissions issue. And pulling the correlation ID into the Activity Log surfaced the policy detail: assignment deny-keyvault-public-network blocked the Key Vault because the template left publicNetworkAccess at its default Enabled, under a stricter management-group policy than dev.

Each fix was small and on the right layer. Quota: a quota-increase request for Dsv5 to 32 vCPUs (approved within the hour), with the already-parameterised SKU shipped as Standard_D2s_v5 to land the environment that night. Name: switch to st${uniqueString(resourceGroup().id)}. Policy: publicNetworkAccess: 'Disabled' plus a private endpoint — complying rather than fighting. The rerun went green. The lasting change was a CI gate: what-if plus az deployment group validate on every PR, against a subscription carrying the same policy assignments, so quota, naming and policy failures now surface as a PR comment instead of a 02:10 page. The lesson on the wall: “A failed deployment is never one error until you’ve listed the operations. Read the leaves.”

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

Time Symptom Action taken Effect What it should have been
02:10 DeploymentFailed + correlation ID (alert fires) List the operations immediately
02:12 Same on re-run Re-run the workflow No change Don’t re-run blind
02:18 Generic message in CI log az deployment operation group list Found three failing leaves The breakthrough
02:25 Three distinct codes Confirm each: quota / name / policy Three layers identified
02:40 Root causes known Param SKU down, uniqueString name, publicNetworkAccess: Disabled Deploy goes green Correct fixes, on the right layer
+1 day Hardening Add what-if + validate PR gate Failures now caught at PR time The durable fix is the gate

Advantages and disadvantages

The declarative, tree-structured ARM model both causes this class of friction and makes it diagnosable. Weigh it honestly:

Advantages (why this model helps you) Disadvantages (why it bites)
Every operation is recorded with a code, message and correlation ID — you rarely lack data The top-level message is generic; the real error is two levels down
Preflight (validate/what-if) catches quota/naming/policy/SKU failures before anything is created Skip preflight and you find those only mid-deploy, leaving a partial state
Declarative templates are repeatable and reviewable as code One template fans out to many providers — one failure, many error languages
Symbolic references make order deterministic and catch typos at compile time Hand-built resourceId() strings hide dependencies → intermittent ResourceNotFound
Rollback-on-error can restore known-good state automatically The default leaves a partial deployment; Complete mode can delete resources
Policy enforces governance at deploy time, before bad infra exists Policy denials surprise authors whose template is otherwise perfect
One control plane backs az, Bicep, Terraform, portal and SDK Terraform/portal wrap the ARM error in another layer of abstraction

Every disadvantage is manageable — preflight gates, symbolic references, deterministic names, Incremental mode, reading the tree — but only if you know they exist, which is the point of this article.

Hands-on lab

Reproduce a classic deployment failure, read it in the operation tree, and fix it — free-tier-friendly (an empty storage account and what-if cost nothing). Run in Cloud Shell (Bash).

Step 1 — Variables and resource group.

RG=rg-arm-lab
LOC=centralindia
az group create -n $RG -l $LOC -o table

Step 2 — Author a template with a deliberate name clash. Create lab.bicep:

param location string = resourceGroup().location

// BUG A: a hard-coded global storage name almost certainly taken
resource sa 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: 'storageaccount'                 // 12 chars, valid shape, but globally taken
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: { supportsHttpsTrafficOnly: true, minimumTlsVersion: 'TLS1_2' }
}

Step 3 — Preflight with what-if and validate BEFORE deploying — catch what you can without touching reality:

az deployment group what-if -g $RG -n lab -f lab.bicep
az deployment group validate -g $RG -n lab -f lab.bicep \
  --query "{state:properties.provisioningState, error:error}" -o jsonc

validate may already flag the name; if not, the deploy will.

Step 4 — Deploy and watch it fail, then read the tree.

az deployment group create -g $RG -n lab -f lab.bicep -o none || true
# Read the LEAF, not the root:
az deployment operation group list -g $RG -n lab \
  --query "[?properties.provisioningState=='Failed'].{resource:properties.targetResource.resourceName, code:properties.statusMessage.error.code, msg:properties.statusMessage.error.message}" \
  -o jsonc

Expected: a Conflict on the storage account. Confirm the global clash:

az storage account check-name --name storageaccount --query "{available:nameAvailable, reason:reason}" -o json

Step 5 — Fix the name to be deterministically unique and redeploy. Edit lab.bicep:

resource sa 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: 'st${uniqueString(resourceGroup().id)}'   // globally unique, stable per RG
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: { supportsHttpsTrafficOnly: true, minimumTlsVersion: 'TLS1_2' }
}
az deployment group create -g $RG -n lab -f lab.bicep \
  --query "{state:properties.provisioningState}" -o json

Expected: provisioningState: Succeeded.

Step 6 — Trace it via the correlation ID (the pipeline-debug muscle).

CID=$(az deployment group show -g $RG -n lab --query properties.correlationId -o tsv)
az monitor activity-log list --correlation-id "$CID" \
  --query "[].{time:eventTimestamp, op:operationName.localizedValue, status:status.value}" -o table

Validation checklist. You ran preflight, reproduced a real Conflict, read it from the operation tree (not the root), confirmed the global clash, fixed it with uniqueString(), and traced every operation by correlation ID — the exact muscle you use on a red pipeline. The lab steps mapped to what each proves:

Step What you did What it proves Real-world analogue
3 what-if + validate first Preflight catches failures pre-apply The CI gate that stops 02:10 pages
4 Read the failing leaf The real error is in the operations, not the root The 2-minute diagnosis
4 check-name A Conflict can be a global clash Storage/KV/ACR naming reality
5 uniqueString() name The deterministic-unique fix Every production naming scheme
6 Correlation-ID trace One GUID unlocks every event Debugging a pipeline that logs one line

Cleanup (avoid lingering charges).

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

Cost note. An empty Standard_LRS storage account plus a few what-if/validate calls is effectively free — the whole lab is well under ₹10, and deleting the resource group removes everything.

Common mistakes & troubleshooting

This is the playbook — the part you bookmark. A scannable table for 02:14, spanning basic failures (a name taken, a typo’d expression) and advanced ones (a policy Deny, an ordering race, a Complete-mode pruning), with the deepest lessons called out below.

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 At least one resource deployment operation failed. and nothing else Reading the root, not the leaves az deployment operation group list -g <rg> -n <dep> --query "[?properties.provisioningState=='Failed']" Read the failing leaf’s code/message; act on that
2 Pipeline shows only Correlation ID: <guid> The provider/policy error isn’t in the pipeline log az monitor activity-log list --correlation-id <guid> Read the activity-log statusMessage.error.details[]
3 InvalidTemplate referencing an expression Bad function/resourceId()/reference() CLI error text; az bicep build -f main.bicep Fix the expression; prefer symbolic references
4 BCP028/BCP057/BCP062 etc. Bicep authoring error, never compiled locally az bicep build -f main.bicep Fix the flagged line; build in CI
5 Conflict on a storage/KV/ACR name Name is globally taken (maybe another tenant) az storage account check-name --name <n> uniqueString()-suffixed name
6 Conflict changing an existing resource Tried to mutate an immutable property Leaf message names the property; az resource show Don’t mutate; replace deliberately
7 RequestDisallowedByPolicy An Azure Policy Deny matched the resource Activity Log error names the policyAssignment Comply with the policy, or get an exemption
8 QuotaExceeded (cores/IPs/count) Subscription/region quota hit az vm list-usage --location <r> / az network list-usages Quota-increase request or smaller footprint
9 SkuNotAvailable SKU not offered/restricted here az vm list-skus --location <r> --query "[?name=='<sku>'].restrictions" Different SKU/region/zone
10 AllocationFailed Transient capacity shortage for that size/zone Retry; check az vm list-skus restrictions Retry; change size/zone; flexible VMSS
11 AuthorizationFailed Deploying principal lacks the RBAC action Error names action+scope; az role assignment list --assignee <id> Grant the role at the right scope
12 LinkedAuthorizationFailed Lacks rights on a referenced resource Error names the linked resource Grant access to the linked resource
13 ResourceNotFound, intermittent (“passes on retry”) Dependency race — order not expressed Operation timestamps; the named missing ID Symbolic reference or explicit dependsOn
14 ParentResourceNotFound Child resource declared without its parent Check the parent’s deployment status Use parent:/proper nesting
15 NoRegisteredProviderFound / location not registered Resource provider not registered in the sub az provider show -n Microsoft.X --query registrationState az provider register -n Microsoft.X
16 RoleAssignmentExists on redeploy Non-idempotent role-assignment name The leaf names the assignment Name it with a deterministic guid()
17 DeploymentQuotaExceeded >800 deployments in the RG history az deployment group list -g <rg> --query "length(@)" Prune old deployment history
18 Resources deleted after a deploy Complete mode pruned anything not in the template Check --mode; Activity Log delete events Use Incremental; never Complete unless intended
19 InvalidContentLink / nested template 404 Bad/expired SAS, wrong path, or unreachable template URL Open the templateLink.uri; check SAS expiry Re-issue the link/SAS or use a Bicep module

One lesson sits above the rest. Entry 1 is the meta-mistake behind half of all “I’m stuck” reports — the root error.message is a summary incapable of telling you the cause; internalise “list the operations, read the leaf” and most other entries become routine. The runner-up for damage is entry 18: a partial template run in --mode Complete deletes resources not in the template, so stay on Incremental unless the template is the reviewed source of truth for the group.

Best practices

Security notes

The deploying identity is the crown jewel of an IaC pipeline — most authorization failures are least privilege working correctly, so fix them by granting the narrow role, not Owner. Use a service principal with OIDC federation (no stored secret) or a managed identity for CI — see GitHub Actions to Azure with OIDC federated credentials and Managed identity: system-assigned vs user-assigned patterns — and scope the role to the resource group or subscription you deploy to, not the tenant.

RequestDisallowedByPolicy failures are a security feature working — a Deny policy stopped non-compliant infrastructure before it existed. Comply (HTTPS-only, disable public network access, add the tag); if a genuine exception is needed, use a scoped, time-boxed policy exemption with an owner. Never disable a Deny policy to unblock a deployment.

Treat the Activity Log as the audit trail it is — who deployed what, when, with which identity. Finally, never embed secrets in template parameters or outputs: deployment outputs are stored in the history in plaintext, so use Key Vault references in parameters so the secret value never lands in ARM’s history.

Cost & sizing

The deployment operations themselves — validate, what-if, create, reading the tree and the Activity Log — are free; ARM is a control plane and you pay only for the resources it creates. The cost story is entirely about the failures you avoid and the resources you accidentally create or delete.

Cost driver Why it costs How to control it
Partial deployment left after a failure Half-created resources still bill Read the tree, finish or roll back; no orphans
Complete-mode accidental deletion Re-creating deleted resources (and lost data) Incremental default; what-if shows deletions
Over-large SKU to dodge AllocationFailed A bigger size than needed, just to allocate Right-size; retry or change zone instead
Quota-increase lead time A blocked deploy delays delivery Request increases ahead of need; parameterise SKUs
Deployment history bloat Causes DeploymentQuotaExceeded friction Prune history in long-lived RGs

There’s no free-tier limit for ARM itself — the lab here costs under ₹10. The expensive failures are the ones that create infrastructure (a partial VM deployment) or destroy it (Complete mode), which is why reading the tree and previewing with what-if pays for itself the first time it catches a deletion.

Interview & exam questions

Q1. A deployment fails with At least one resource deployment operation failed. What is your very first command, and why? List the deployment operations filtered to the failed ones (az deployment operation group list ... --query "[?properties.provisioningState=='Failed']"). The root message is only a summary; the real provider error — its code and message — is on the failing child operation, so you read the leaf, not the root.

Q2. What’s the difference between InvalidTemplate and ResourceDeploymentFailure? InvalidTemplate is a shape failure (bad JSON/Bicep, schema or expression) caught before any resource is created, so nothing is half-built. ResourceDeploymentFailure means a specific provider rejected a real create/update, which can leave a partial deployment to reconcile.

Q3. You see RequestDisallowedByPolicy. Is your template wrong? How do you find the cause and fix it? Your template is probably valid; an Azure Policy Deny rejected the request. Pull the correlation ID and read the Activity Log, which names the policyAssignment and the disallowed reason. Fix by complying (enable HTTPS-only, switch region, add the tag) or requesting a scoped exemption — not by weakening the policy.

Q4. A storage account deployment fails with Conflict even though you’ve never created that account. Why? Storage account names are globally unique across all of Azure, so it’s taken by another subscription or tenant. Confirm with az storage account check-name; fix by making the name deterministically unique, e.g. st${uniqueString(resourceGroup().id)}.

Q5. What is the correlation ID and when is it indispensable? A GUID shared by a deployment and every operation it spawns, including provider- and policy-side calls. It’s indispensable when a pipeline logs nothing but the GUID — filtering the Activity Log by it surfaces every event for the deployment, including the buried error the pipeline never printed.

Q6. Explain Incremental vs Complete deployment mode and the risk of Complete. Incremental (default) adds/updates the template’s resources and leaves the rest of the group alone. Complete makes the group equal the template, so any resource in the group but missing from the template is deleted. The risk is running a partial template in Complete mode and deleting resources you forgot to include.

Q7. A deployment passes on retry sometimes and fails with ResourceNotFound other times. Diagnose it. A dependency race: ARM deployed a resource before the thing it references, because the dependency wasn’t expressed — usually you referenced a resource by a hand-built string instead of a symbolic reference. Fix with the symbolic .id (Bicep infers the order) or an explicit dependsOn for a non-referenced runtime dependency.

Q8. How do what-if and validate differ, and why gate CI with them? validate checks the shape and runs preflight (catching many quota/policy/naming/SKU failures) without creating resources; what-if additionally shows the predicted change set, including deletions, in readable form. Gating CI with both turns deployment-time failures into PR comments and reveals Complete-mode deletions before apply.

Q9. You get QuotaExceeded deploying a VM scale set in a new subscription. What are your options? Confirm with az vm list-usage --location <region> to see which quota (regional or per-family vCPUs) is hit. Then request a quota increase for that family/region, or reduce the footprint immediately with a smaller SKU or fewer instances (which is why SKUs should be parameterised).

Q10. SkuNotAvailable vs AllocationFailed — what’s the distinction and the fix for each? SkuNotAvailable means the SKU isn’t offered or is restricted in that region/zone — choose an available SKU/region/zone (az vm list-skus). AllocationFailed means it is offered but there’s no capacity right now — retry, change zone/size, or use a flexible orchestration that lets Azure pick capacity.

Q11. A redeploy of a working template now fails with RoleAssignmentExists. How do you make it idempotent? The assignment’s name isn’t deterministic, so the redeploy tries to create a “new” one that already exists. Name it with a deterministic guid() (e.g. guid(resourceId, principalId, roleDefinitionId)) so the same logical assignment yields the same name and the operation becomes idempotent.

Q12. (Cert context) Which exams touch this and what’s the takeaway? AZ-104 (Administrator) and AZ-400 (DevOps) cover ARM/Bicep deployments and troubleshooting; AZ-305 touches IaC governance. The takeaway they reward: deployments fail in layers (template shape, preflight, provider, policy, RBAC), and the discipline is to read the operation tree and Activity Log rather than the generic top-level message.

Quick check

  1. A deployment’s root message says At least one resource deployment operation failed. — what do you run next?
  2. A storage account create fails with Conflict and you’ve never used that name. What’s the cause and the fix?
  3. Which deployment mode can delete resources, and what triggers the deletion?
  4. You have only a Correlation ID from a red pipeline. How do you find the real error?
  5. A deploy fails intermittently with ResourceNotFound. What’s the root cause class and the fix?

Answers

  1. List the deployment operations and read the failing leafaz deployment operation group list -g <rg> -n <dep> --query "[?properties.provisioningState=='Failed']". The root is only a summary; the provider’s real code/message is on the child operation.
  2. The name is globally taken (storage names are unique across all of Azure, possibly another tenant). Confirm with az storage account check-name; fix with a deterministically-unique name like st${uniqueString(resourceGroup().id)}.
  3. Complete mode. It makes the resource group equal the template, so any resource in the group but not in the template is deleted. (Incremental, the default, never deletes unlisted resources.)
  4. Filter the Activity Log by the correlation IDaz monitor activity-log list --correlation-id <guid> — which surfaces every operation for that deployment, including the provider/policy error the pipeline didn’t print.
  5. A dependency race — ARM deployed a resource before the thing it referenced because the dependency wasn’t expressed (usually a hand-built resourceId() string). Fix with a symbolic reference (.id) or an explicit dependsOn.

Glossary

Next steps

AzureARMBicepDeploymentTroubleshootingActivity LogError CodesIaC
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