🛸 Hitchhiker's Guide — Phase 01: ARM & the Deployment Engine

Read this if: you've run az deployment or terraform apply a hundred times but couldn't draw what ARM actually does between "create" and "the resource exists." This is the compressed tour the WARMUP derives slowly. Skim it, then read the WARMUP, then build the engine.


0. The 30-second mental model

Every write in Azure is one idempotent PUT to a resource ID, through ARM. A deployment is ARM building a dependency graph from your template, topologically ordering it, and PUTting desired state to each resource provider — Create if absent, Modify if drifted, NoChange if identical. The whole engine is: graph → topo sort → idempotent PUT → poll till Succeeded. Build that and Bicep, Terraform, Pulumi, and CloudFormation are all the same algorithm wearing different syntax.

One sentence to tattoo: ARM doesn't run your steps — it reconciles your desired state, in dependency order, idempotently.

1. The request pipeline, one breath

client → ARM:  AUTHENTICATE (Entra JWT)  →  AUTHORIZE (RBAC: Actions−NotActions, deny wins)
             →  VALIDATE (Azure Policy: deny/modify/append/audit)  →  ROUTE (resource provider)
             →  RP does idempotent PUT, returns provisioningState (async)

Fail-closed, cheapest-check-first. Almost every "can't deploy" ticket is one of these four gates: 401 (token), 403 (RBAC / deny assignment), RequestDisallowedByPolicy, MissingSubscriptionRegistration.

2. The dependency graph (the part everyone gets wrong)

Edges come from two places:

  • Explicit: "dependsOn": ["nic1"] — you typed it.
  • Implicit: a property uses reference('x') or resourceId('x') → ARM adds an edge to x automatically. You never wrote dependsOn, but the order is enforced anyway.

A correct template often has almost no dependsOn because references already encode the order. Bicep makes this fully automatic via symbolic references (vnet.id). Reach for explicit dependsOn only for an ordering ARM can't infer.

3. Topo sort + cycles, the algorithm

Kahn's: emit any node whose deps are all emitted; repeat. A cycle = nodes that never reach in-degree 0 → un-deployable → ARM rejects it (the lab raises cycle: ...). Independent resources deploy in parallel in real ARM; the lab serializes ties by name purely so the plan is deterministic and testable. Complexity \(O(V+E)\).

4. Idempotency, the load-bearing property

PUT desired state →  absent: Create | drifted: Modify | identical: NoChange

Re-run the same template → all NoChange. That's why CI can re-apply on every commit and why drift reconciles. It breaks when a resource has a random name (Creates a new one every run) or when a property is immutable (Modify becomes destroy-and-recreate — that's Terraform's ForceNew, Phase 02).

5. Modes — the foot-gun

ModeUn-templated resources in the RGUse
Incremental (default)left alonealmost always
CompleteDELETED (RG = template exactly)ephemeral envs you own end-to-end

Complete = Incremental + delete-the-rest. The production-deletion story: deploy a template that's missing a resource, in Complete mode → ARM deletes the resource. By design, not a bug. Defense: stay Incremental, always what-if first, lock precious resources, use deployment stacks.

6. The numbers & names to tattoo on your arm

ThingValue
Default deployment modeIncremental
Resource ID shape/subscriptions/{s}/resourceGroups/{rg}/providers/{ns}/{type}/{name}
Every write isone idempotent PUT + api-version
Implicit-dependency functionsreference(), resourceId()
provisioningState pathAccepted → Running → Succeeded | Failed | Canceled
Async signals202 Accepted + Azure-AsyncOperation header to poll
MG tree max depth6 levels under root (P05, but same control plane)
what-if change typesCreate / Modify / Delete / NoChange / Ignore
Topo sort complexity\(O(V + E)\)

7. az one-liners you'll actually type

# Dry-run first — read the Deletes before you ship anything
az deployment group what-if -g rg -f main.bicep -p @params.json

# Deploy (Incremental is default; be explicit when it matters)
az deployment group create -g rg -f main.bicep --mode Incremental

# Complete mode — only with a what-if you've read and a reason
az deployment group create -g rg -f main.bicep --mode Complete

# Find the resource that's blocking a stuck/failed deploy
az deployment operation group list -g rg -n <deploymentName> \
  --query "[?properties.provisioningState!='Succeeded'].{res:properties.targetResource.id, state:properties.provisioningState, err:properties.statusMessage}"

# Bicep is just a transpiler — see the ARM JSON it emits
az bicep build -f main.bicep            # → main.json

# Deployment stack with drift protection (deny out-of-band delete/edit)
az stack group create -g rg --name app -f main.bicep \
  --deny-settings-mode denyWriteAndDelete --action-on-unmanage deleteResources

# Register a resource provider before first use of its types
az provider register --namespace Microsoft.ContainerService

8. War story shapes you'll relive

  • "Complete mode deleted prod." → a template missing a hand-created DB, deployed Complete. ARM did exactly what it was told. Fix: Incremental + what-if + locks; never Complete blind.
  • "Deploy is stuck forever." → one resource's provisioningState is non-terminal (quota, a Failed dependency blocking dependents). Pull deployment operations; find that resource.
  • "It deployed in the wrong order / flaky." → a real ordering not captured by a reference; add the missing dependsOn. Or the reverse: over-dependsOn serialized a parallel deploy and made it slow.
  • "It worked locally, 403 in CI." → the pipeline's identity lacks the RBAC Action, or a deny assignment from a stack/managed-app is overriding the allow. Deny wins.
  • "Re-running created duplicates." → a uniqueString(newGuid())-style random name broke idempotency. Desired state must be stable to converge.

9. Vocabulary that signals you've held the pager

  • Idempotent PUT — desired state, not a command; re-run converges to NoChange.
  • Implicit dependency — the edge reference()/resourceId() creates without a dependsOn.
  • provisioningState — the async truth; "PUT returned" ≠ "resource ready."
  • Incremental vs Complete — additive vs "RG = template exactly (deletes the rest)."
  • what-if — the read-only diff; the PR review and the Delete-catcher.
  • Resource provider (RP) — the service that owns a type; must be registered.
  • Deployment stack / deny-settings — managed deletion + control-plane drift protection.
  • Transpile — Bicep → ARM JSON; same engine underneath.

10. Beginner mistakes that mark you in interviews

  1. Thinking array position in resources is the deploy order (it's the graph).
  2. Adding dependsOn everywhere instead of letting references imply edges.
  3. Not knowing Incremental is the default — or what Complete deletes.
  4. Saying "the deploy is done" when the PUT returned but provisioningState isn't terminal.
  5. Calling a dry run "what-if" while mutating state — a what-if never mutates.
  6. Treating idempotency as magic and getting bitten by a random resource name.
  7. Confusing "Bicep vs ARM" (front-end vs engine — same thing) with "Terraform vs ARM" (genuinely different engines).

11. How this phase pays off later

  • The request pipeline is the spine of P03 (the JWT/auth gate), P04 (RBAC + Policy gates), and P05 (inheritance down the MG tree).
  • The dependency graph + topo sort + idempotency is exactly Terraform's plan/apply engine (P02) — you'll add ForceNew and a state file to the same algorithm.
  • Idempotent re-apply is what makes the CI/CD pipeline (P08) safe to run on every commit.
  • Modes, what-if, and deny-settings are how you protect a shared landing zone (P05) from drift and accidental deletion.

Now read the WARMUP slowly, then build the engine. After this, no IaC tool is a black box.