🛸 Hitchhiker's Guide — Phase 01: ARM & the Deployment Engine
Read this if: you've run
az deploymentorterraform applya 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')orresourceId('x')→ ARM adds an edge toxautomatically. You never wrotedependsOn, 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
| Mode | Un-templated resources in the RG | Use |
|---|---|---|
| Incremental (default) | left alone | almost always |
| Complete | DELETED (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
| Thing | Value |
|---|---|
| Default deployment mode | Incremental |
| Resource ID shape | /subscriptions/{s}/resourceGroups/{rg}/providers/{ns}/{type}/{name} |
| Every write is | one idempotent PUT + api-version |
| Implicit-dependency functions | reference(), resourceId() |
| provisioningState path | Accepted → Running → Succeeded | Failed | Canceled |
| Async signals | 202 Accepted + Azure-AsyncOperation header to poll |
| MG tree max depth | 6 levels under root (P05, but same control plane) |
| what-if change types | Create / 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
provisioningStateis non-terminal (quota, aFaileddependency 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-dependsOnserialized 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 adependsOn. - 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
- Thinking array position in
resourcesis the deploy order (it's the graph). - Adding
dependsOneverywhere instead of letting references imply edges. - Not knowing Incremental is the default — or what Complete deletes.
- Saying "the deploy is done" when the PUT returned but
provisioningStateisn't terminal. - Calling a dry run "what-if" while mutating state — a what-if never mutates.
- Treating idempotency as magic and getting bitten by a random resource name.
- 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
ForceNewand 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.