Warmup Guide — Azure Resource Manager & the Deployment Engine
Zero-to-principal primer for Phase 01. We start from "what even is ARM" and end with you able to draw the control-plane request pipeline, predict a deployment's order from implicit references alone, explain the Complete-mode foot-gun, and reason about the async-operation poll — the things interviewers probe and incidents turn on. Every mechanism is opened under the hood: the pipeline, the graph algorithm, the idempotent PUT, the diff.
Table of Contents
- Chapter 1: What ARM Is — The One API in Front of Everything
- Chapter 2: The Control-Plane Request Pipeline
- Chapter 3: ARM Template Anatomy
- Chapter 4: The Dependency Graph — Explicit and Implicit Edges
- Chapter 5: Topological Ordering and Cycle Detection
- Chapter 6: Idempotency — The PUT That Converges
- Chapter 7: Deployment Modes — Incremental vs Complete
- Chapter 8: What-If — The Dry-Run Diff
- Chapter 9: Asynchronous Operations and provisioningState
- Chapter 10: Bicep — The Typed Transpiler
- Chapter 11: Deployment Stacks and Deny-Settings
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: What ARM Is — The One API in Front of Everything
From zero. When you create anything in Azure — through the portal, the az CLI,
PowerShell, a Terraform run, a GitHub Action — you are not talking to "the storage
service" or "the VM service" directly. You are talking to one front door:
Azure Resource Manager (ARM). It is a single, global REST API and deployment engine
that sits in front of every Azure resource.
What it is. ARM is the control plane: the system that decides what resources exist, what their configuration is, and who may manage them. Every management operation — create, read, update, delete, tag, lock, assign a role — is an HTTP request to an ARM endpoint whose URL is the resource's canonical ID:
PUT https://management.azure.com
/subscriptions/{subId}
/resourceGroups/{rg}
/providers/{namespace}/{type}/{name}
?api-version=2023-01-01
That hierarchy — subscription → resource group → provider/type/name — is the resource ID, and it's the unit of RBAC scope, Policy scope, tagging, and locks (Phase 00 drilled this). Memorize the shape; you'll read it in error messages for the rest of your career.
Why it exists. Before ARM (the "classic"/ASM era), every Azure service had its own
API, its own auth, its own deployment story. There was no single place to apply
governance, no atomic "deploy these ten things together," no uniform RBAC. ARM unifies all
of it: one auth model (Entra), one authorization model (RBAC + Policy), one
deployment grammar (templates), and one idempotent verb (PUT desired state). Every
hard problem in this curriculum — identity, governance, networking, secrets — plugs into
this pipeline.
The control-plane / data-plane split (recall from P00). ARM is the control plane: it
creates the storage account, it does not read your blob. Reading the blob is the data
plane — a different endpoint ({account}.blob.core.windows.net), different auth (a SAS
token or a data-plane RBAC role), different throttling, different SLA. A PUT to create
Key Vault goes through ARM; getting a secret out of it does not. Conflating the two is
the single most common Azure misconception; keep them in separate boxes in your head.
Common misconception. "The portal does its own thing." No — the portal is just a
JavaScript client of the same ARM REST API you could call with curl. There is no
privileged back channel. Everything the portal can do, ARM can do, and so can you with a
token.
Chapter 2: The Control-Plane Request Pipeline
From zero. A request doesn't go straight from your az command to the disk that
stores your VM. It passes through a pipeline of gates, in a fixed order. This is the
single most valuable diagram in the phase:
┌─────────────────────────────────────────────┐
az / portal / TF ───▶ │ Azure Resource Manager │
(a signed request) │ │
│ 1. AUTHENTICATE validate the Entra JWT │
│ (signature, iss, aud, exp) │
│ 2. AUTHORIZE RBAC: Actions − NotActions │
│ then DENY assignments win │
│ 3. VALIDATE Azure Policy effects │
│ (deny / modify / append / audit) │
│ 4. ROUTE to the resource provider │
└──────────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Resource Provider (Microsoft.Storage, …) │
│ does the idempotent PUT, returns │
│ provisioningState (async) │
└─────────────────────────────────────────────┘
Walk each gate under the hood:
- Authenticate. Your token is a JWT issued by Microsoft Entra ID. ARM verifies the
signature against Entra's published keys (JWKS, selected by the
kidin the header), and checksiss(issuer = your tenant),aud(audience =https://management.azure.com), and the validity window (nbf ≤ now ≤ exp). No valid token →401. You build this validator in Phase 03; here, just know it's gate one. - Authorize. ARM computes your effective permissions: the union of
Actionsacross all your role assignments at this scope and every ancestor scope, minusNotActions. Then — deny assignments win — any explicit deny overrides an allow. No matching allow, or a matching deny →403. You build this in Phase 04. - Validate. Azure Policy evaluates the request against assigned rules. A
denyeffect blocks the write (RequestDisallowedByPolicy);modify/appendmutate the request (e.g. add a required tag) before it proceeds;auditjust records non-compliance. Policy runs at create/update time, here, on the control-plane path. You build this in Phase 04 too. - Route. Only now does ARM hand the request to the resource provider — the service
(
Microsoft.Storage,Microsoft.Network,Microsoft.Compute) that owns this resource type and implements its actual CRUD. The RP must be registered on the subscription, or you getMissingSubscriptionRegistration.
Why this order. It is fail-closed and cheapest-check-first: reject the unauthorized caller before spending a cycle on policy, reject the policy-violating request before bothering the resource provider. Identity is the perimeter; it's gate one for a reason.
Production significance. Almost every "I can't deploy" ticket is one of these four
gates: a 401 (bad/expired token), a 403 (RBAC or a deny assignment — and "I'm Owner"
doesn't help if a deny assignment from a deployment stack or managed app is blocking you),
a RequestDisallowedByPolicy, or a MissingSubscriptionRegistration. Knowing the pipeline
is the triage tree.
Common misconception. "RBAC and Policy are the same thing." No. RBAC answers who may
perform this action; Policy answers is this resource configuration allowed. You can have
permission to create a storage account (RBAC says yes) and still be blocked because Policy
requires HTTPS-only and your request didn't set it (deny). Two different gates, both must
pass.
Chapter 3: ARM Template Anatomy
From zero. A template is a JSON document describing desired state — the resources you want to exist, declaratively, not the steps to create them. You hand ARM the "what"; ARM figures out the "how" and the "in what order." The top-level shape:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": { "location": { "type": "string", "defaultValue": "eastus" } },
"variables": { "saName": "[concat('stg', uniqueString(resourceGroup().id))]" },
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2023-01-01",
"name": "[variables('saName')]",
"location": "[parameters('location')]",
"sku": { "name": "Standard_LRS" },
"kind": "StorageV2"
}
],
"outputs": { "saId": { "type": "string", "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('saName'))]" } }
}
Each section, under the hood:
$schema— the template-language version; lets the engine and tooling validate the document shape.parameters— the typed inputs callers supply at deploy time (with optional defaults, allowed values, constraints). The seam between the template and the environment.variables— computed, reusable values; pure functions of parameters and other variables, resolved at the start of the deployment.resources— the heart: the array of desired resources, each with atype,apiVersion,name, and properties.outputs— values returned after deploy (a connection string, a resource ID) for the caller or a parent/nested deployment to consume.
The [...] expressions and template functions. Anything in [ ] is an expression ARM
evaluates at deploy time. The functions that matter for this phase:
resourceId('type', 'name')— builds a resource's full ID. Crucially, when you use it to point one resource at another, ARM infers an implicit dependency (Chapter 4).reference('name')— reads runtime properties of another resource (its endpoints, its generated keys). Because it must read a resource that exists, it also creates an implicit dependency.dependsOn— the explicit ordering you write by hand: "deploy these first."parameters(),variables(),concat(),uniqueString()— value plumbing.
Why declarative. Because declarative + idempotent (Chapter 6) is what makes the file re-runnable. An imperative script ("create X, then create Y") run twice creates two X's; a declarative desired-state template run twice converges to the same single X. That property is the whole reason IaC exists.
Common misconception. "Order in the resources array is the deploy order." It is
not. ARM ignores array position and deploys in dependency order (Chapter 5).
Resources with no dependency relationship deploy in parallel.
Chapter 4: The Dependency Graph — Explicit and Implicit Edges
This is the chapter the lab is built on, and the one most engineers get wrong.
From zero. ARM cannot deploy a network interface before the subnet it lives in, or a VM before its NIC. It learns these ordering constraints by building a directed graph: a node per resource, an edge from a resource to each resource it depends on. Edges come from two sources:
1. Explicit edges — dependsOn. You literally list the names:
{ "type": "…/virtualMachines", "name": "vm1", "dependsOn": ["nic1"] }
2. Implicit edges — the ones you didn't type. When a resource's property references
another resource via reference() or resourceId(), ARM infers the dependency
automatically:
{
"type": "…/networkInterfaces", "name": "nic1",
"properties": {
"ipConfigurations": [{
"properties": { "subnet": { "id": "[resourceId('…/subnets', 'vnet1', 'subnet1')]" } }
}]
}
}
You never wrote "dependsOn": ["vnet1"] — but ARM adds that edge, because resourceId
names vnet1, and you can't get a subnet's ID until the VNet exists. This is why a
correct template often has almost no dependsOn: the references already encode the
order.
Under the hood — what the lab does. The engine scans every string value in a
resource's properties (recursing into nested dicts and lists, because real properties
nest deeply) for the pattern reference('NAME') or resourceId('NAME'), pulls out
NAME, and unions those with the explicit dependsOn list:
deps(resource) = set(resource.dependsOn) ∪ { NAME : reference('NAME') or resourceId('NAME') ∈ properties }
A resource never depends on itself (ARM drops a self-edge; a self-edge would be a trivial
cycle). And every referenced name must resolve to a resource in the template — a
reference to a name that isn't there is ARM's deploy-time InvalidTemplate: the resource '…' is not defined in the template error, which the lab raises as a ValueError.
Production significance. When you over-specify dependsOn (adding edges the references
already imply), you don't break correctness — but you can serialize a deployment that
could have run in parallel, making it slower. When you under-specify (a hidden ordering
the references don't capture, e.g. an eventual-consistency timing dependency), you get
flaky deploys. The principal reads the graph, not the dependsOn list.
Common misconception. "I need dependsOn everywhere." Usually the opposite: prefer
references (Bicep makes this automatic and symbolic). Reach for explicit dependsOn only
for an ordering ARM can't infer from a reference.
Chapter 5: Topological Ordering and Cycle Detection
From zero. Given the dependency graph, the engine must produce an order in which every
resource comes after all of its dependencies. That ordering is a topological sort,
and it exists iff the graph is a DAG (directed acyclic graph). A cycle —
A dependsOn B, B dependsOn A — has no valid order and is un-deployable.
Under the hood — Kahn's algorithm. The lab implements the classic queue-based topo sort:
1. in_degree[n] = number of n's dependencies # how many edges point OUT to deps
2. ready = every node with in_degree 0 # nothing left to wait for
3. while ready is non-empty:
n = pop the SMALLEST-named node from ready # deterministic tie-break
emit n
for each m that depends on n:
in_degree[m] -= 1
if in_degree[m] == 0: add m to ready
4. if we emitted fewer nodes than the graph has → a CYCLE remains
Two design choices in that pseudocode are worth their own paragraph:
Determinism via tie-break. At any step, several nodes may be "ready" at once (all their deps are done). Real ARM deploys all of them in parallel — order among them is undefined. But an undefined order is untestable, so the lab always pops the alphabetically smallest ready node. That gives one canonical, reproducible order: the same template always yields the same plan. Internalize that the determinism is a lab affordance for testability — production ARM parallelises ties. (The Extensions section shows how to emit parallel "waves" instead.)
Cycle detection for free. If the graph has a cycle, the nodes in the cycle never reach
in_degree 0 (each is waiting on the next), so they're never emitted. After the loop, if
len(emitted) < len(graph), the leftover nodes are the cycle — the lab raises
ValueError("cycle: ...") naming them. This is the same mechanism ARM uses to reject a
circular dependsOn.
A worked trace. Graph: vnet1 → ∅, subnet1 → {vnet1}, nic1 → {subnet1},
vm1 → {nic1, storage1}, storage1 → ∅.
| Step | ready (sorted) | pop | emitted so far |
|---|---|---|---|
| 0 | storage1, vnet1 | storage1 | storage1 |
| 1 | vnet1 | vnet1 | storage1, vnet1 |
| 2 | subnet1 | subnet1 | …subnet1 |
| 3 | nic1 | nic1 | …nic1 |
| 4 | vm1 | vm1 | …vm1 |
Order: storage1 → vnet1 → subnet1 → nic1 → vm1. Every resource lands after its deps.
Complexity. \( O(V + E) \) — linear in resources plus edges. ARM deployments are small (tens to low hundreds of resources), so this is instant; the value is correctness, not speed.
Common misconception. "Topo sort gives one answer." A DAG generally has many valid topological orders; the tie-break picks one. What's invariant is the partial order (dependencies before dependents), not the total order.
Chapter 6: Idempotency — The PUT That Converges
From zero. An operation is idempotent if doing it twice has the same effect as
doing it once. PUT (set this resource to this desired state) is idempotent; POST
(create a new thing) generally is not. ARM resources are deployed with PUT, and that
single fact is what makes Infrastructure-as-Code work.
Under the hood — the three outcomes. For each resource, the resource provider compares the desired state in the template against the actual state of the resource and converges:
| Situation | Action | Effect |
|---|---|---|
| resource does not exist | Create | provision it to desired state |
| exists, desired ≠ actual | Modify | reconcile it to desired state |
| exists, desired = actual | NoChange | nothing happens (a safe no-op) |
The lab's deploy does exactly this against a state dict (name → properties): absent →
Create, present-but-different → Modify, identical → NoChange.
Why it matters — the convergence property. Because a PUT of already-matching state is
a NoChange, you can re-run the same template as many times as you want and it stays
correct. This is the property your CI/CD depends on (Phase 08): every commit can re-apply
the full template; nothing duplicates; drift gets reconciled back to desired state. Run the
lab's idempotency test and watch a second deploy of the same template return all
NoChange — that is IaC's foundational guarantee, in code.
$$ \text{deploy}(T, \text{deploy}(T, S).\text{state}) ;\Rightarrow; \text{every action} = \texttt{NoChange} $$
Where idempotency quietly breaks (principal territory). It is not magic:
- A resource with a generated/random name (
uniqueString(newGuid())done wrong) makes every deploy Create a new one — non-idempotent by construction. - A
reference()to a resource someone deleted out-of-band fails the whole deploy. - Some properties are immutable — changing them isn't a Modify, it forces a
delete-and-recreate (this is Terraform's
ForceNew; you'll build it in Phase 02). - A
PATCH-style partial update can behave differently from a fullPUTof desired state.
Common misconception. "Idempotent means it does nothing the second time." No — it means
the end state is the same. The second run still checks every resource; it just finds
them already correct and reports NoChange. "Same result," not "no work."
Chapter 7: Deployment Modes — Incremental vs Complete
From zero. When you deploy a template to a resource group, you pick a mode that decides what happens to resources in the RG that aren't in your template.
Incremental (the default). ARM adds the resources in the template and updates the ones that exist — and leaves everything else alone. Your template is a superset instruction: "make sure these exist and look like this." Resources in the RG not mentioned in the template are untouched.
Complete. ARM makes the resource group match the template exactly — which means it deletes every resource in the RG that is not in the template. Your template becomes the whole truth of the RG.
RG before: [ storage1, vnet1, rogue_db ]
Template: [ storage1, vnet1 ]
Incremental → RG after: [ storage1, vnet1, rogue_db ] (rogue_db untouched)
Complete → RG after: [ storage1, vnet1 ] (rogue_db DELETED)
The lab models this precisely: in Complete mode, any name in state but not in the
template is given a Delete action and removed from the final state; in Incremental
mode it's never touched.
Production significance — the foot-gun. Complete mode is how production resources get
deleted by a "routine" deploy. The classic incident: someone added a resource by hand (or
another team's template owns it), you deploy your template — which doesn't mention it —
in Complete mode, and ARM dutifully deletes it. This is not a bug; it is the defined
contract. The defenses: (1) Incremental is the default — stay there unless you have a
reason; (2) always run what-if first (Chapter 8) and read the Deletes; (3) use
resource locks (CanNotDelete) on anything precious; (4) prefer deployment stacks
(Chapter 11) for managed, intentional deletion.
When Complete is the right tool. When you want the RG to be exactly the template — e.g. an ephemeral environment you own end-to-end, or cleaning up resources a previous template version created and the new one dropped. Intentional, scoped, what-if'd.
Common misconception. "Complete redeploys everything from scratch." No — it doesn't re-create the resources that match; those are still NoChange/Modify. It only adds the delete behavior for un-templated extras. Complete = Incremental + delete-the-rest.
Chapter 8: What-If — The Dry-Run Diff
From zero. Before you let ARM change anything, you want to see what it would do. What-if is ARM's dry run: it computes the diff between your template (desired) and the current state (actual) and reports, per resource, a change type — without deploying anything.
The change classification mirrors the lab's Change objects:
| Change type | Meaning |
|---|---|
| Create | resource will be added |
| Modify | resource exists; listed properties will change |
| Delete | resource will be removed (only in Complete mode / stacks) |
| NoChange | identical; nothing happens |
| Ignore | exists but outside the deployment's scope; left alone |
Under the hood. What-if runs the same classification logic as deploy — build the
graph, order it, compare desired vs actual per resource — but writes the results to a list
instead of to state. The non-negotiable property: it must not mutate anything. The lab
asserts this explicitly: what_if simulates against a throwaway copy so the real state
is byte-identical before and after. A "dry run" that mutates state is a bug, and a
dangerous one.
Production significance. What-if is your PR-time safety net and the thing that
catches the Complete-mode Delete before it ships. The principal workflow (Phase 08):
what-if on the pull request (the diff is the review — reviewers read the Creates,
Modifies, and especially the Deletes), then deploy on merge. A what-if that surfaces an
unexpected Delete has paid for itself a hundred times over.
Common misconception. "What-if is exact." It's a strong predictor, not a contract —
some changes only manifest when the resource provider actually evaluates the request
(api-version quirks, computed defaults). Read it as "here's what ARM intends," and treat
surprising Deletes as a stop sign.
Chapter 9: Asynchronous Operations and provisioningState
From zero. Most Azure resources don't provision instantly — a VM, an AKS cluster, a
SQL database take minutes. So ARM operations are asynchronous: the PUT returns
quickly with "I've accepted this, it's in progress," and you poll until it's done.
Under the hood — the poll loop. The PUT returns 201 Created or 202 Accepted with:
- a
provisioningStateon the resource:Accepted → Creating/Running → Succeeded(orFailed/Canceled), and - an
Azure-AsyncOperation(orLocation) header — a URL the clientGETs repeatedly, honoring theRetry-Afterinterval, until the state is terminal.
client: PUT resource … → 202 Accepted, Azure-AsyncOperation: <url>
client: GET <url> (wait Retry-After) → { "status": "InProgress" }
client: GET <url> (wait Retry-After) → { "status": "InProgress" }
client: GET <url> (wait Retry-After) → { "status": "Succeeded" } ← terminal
az and Terraform hide this loop behind their progress spinner, but it's what they're
doing. "The deploy finished" means the poll reached a terminal state — not that the
PUT returned.
Why it matters — ordering and debugging. Because the engine deploys dependencies before
dependents (Chapter 5), it must wait for a dependency's poll to reach Succeeded before
starting its dependents — otherwise the NIC would try to attach to a subnet that's still
Creating. And when a deploy is "stuck," the principal looks at which resource's
provisioningState is non-terminal and why (a quota error, a policy modify, a
dependency that Failed), rather than staring at the spinner. The lab abstracts the poll
away (resources "complete" instantly) so the graph logic is naked; the Extensions add a
provisioningState state machine so you can model the real loop.
Common misconception. "If the CLI command returned 0, the resource is ready." Only if
the CLI waited for the poll (most do by default; --no-wait does not). With --no-wait
you've only submitted the operation — the resource may still be Creating.
Chapter 10: Bicep — The Typed Transpiler
From zero. ARM JSON is verbose, expression-heavy, and painful to write by hand.
Bicep is a domain-specific language that compiles ("transpiles") down to ARM JSON.
It is not a different engine — bicep build emits the same template the ARM deployment
engine consumes. Same pipeline, same graph, same modes; nicer front-end.
The same storage account, in Bicep:
param location string = 'eastus'
resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'stg${uniqueString(resourceGroup().id)}'
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}
output saId string = sa.id
What Bicep buys you, under the hood:
- Symbolic references and automatic dependencies. You write
sa.id, notresourceId(...). Becausesais a symbol, Bicep infers the dependency graph from the symbol references — you almost never writedependsOn. (This is Chapter 4's implicit edges, made first-class.) - Types and IntelliSense. The compiler knows each resource type's schema for the chosen
api-versionand catches typos and invalid properties before deploy. - Modules, loops, conditionals. First-class
module,for, andifinstead of copy-paste andcopyblocks.
Production significance. Bicep is Microsoft's recommended authoring experience for
native Azure IaC, and the what-if/modes/stacks story is identical (it's the same engine).
But — and this is the principal point — it's still ARM underneath. When a Bicep deploy
fails, you debug the ARM error, the ARM graph, the ARM provisioningState. Bicep is
ergonomics; the engine you built in the lab is the truth.
Common misconception. "Bicep is a competitor to ARM." It compiles to ARM. "Bicep vs Terraform" is a real comparison (different engines, different state models — Phase 02); "Bicep vs ARM" is not (same engine).
Chapter 11: Deployment Stacks and Deny-Settings
From zero. A normal deployment is fire-and-forget: ARM applies it and forgets which resources came from which template. A deployment stack is a resource that remembers the set of resources it manages and governs their lifecycle as a unit.
Two principal features, under the hood:
- Managed deletion. Drop a resource from the stack's template and re-deploy → the stack
deletes that resource (you choose
detachResourcesto leave it, ordeleteResourcesto tear it down). This is a safer, scoped version of Complete mode: the blast radius is "resources this stack manages," not "everything in the RG." - Deny-settings (drift protection). A stack can apply a deny assignment to its
managed resources —
denySettingsMode: denyDeleteordenyWriteAndDelete— that blocks out-of-band changes at the control plane. Someone tries to delete or edit a stack-managed resource in the portal → ARM denies it (deny assignments win, Chapter 2). The desired state is protected by ARM itself, not by hope.
Production significance. This closes the IaC loop: not just "deploy desired state," but "keep the real world equal to desired state and reject drift." It's the control-plane analog of Terraform's drift detection (Phase 02) — except enforced by ARM rather than detected after the fact. For a landing zone (Phase 05) where hundreds of engineers share an estate, deny-settings are how you stop a well-meaning portal click from deleting shared infrastructure.
Common misconception. "A deployment stack is just a deployment." A deployment is an event (it happened, then it's history). A stack is a resource (it persists, tracks its managed set, enforces deny-settings, and governs deletion on the next update).
Lab Walkthrough Guidance
Lab 01 — ARM Deployment Engine, suggested order (each step has tests waiting):
_implicit_refs_in_value+ the regex (Ch. 4). Write the regex that capturesNAMEfromreference('NAME')andresourceId('NAME'). Recurse into dicts and lists —test_extract_implicit_resourceid_and_nestedproves nested properties count.extract_dependencies(Ch. 4). UniondependsOnwith the implicit refs; raise on a missingname; drop the self-edge. Tests: explicit+implicit union, self-reference dropped, missing-name raises.build_dependency_graph(Ch. 4). Map name → deps; raise on a duplicate name and on any reference to a name not in the template. Tests cover both unknowndependsOnand unknown implicit references.topological_order(Ch. 5). Kahn's algorithm, tie-break by name for determinism, raiseValueError("cycle: ...")if not every node is emitted. Tests: order respects deps, deterministic tie-break, cycle + self-cycle raise.deploy(Ch. 6–7). Validate the mode; topo-order; classify Create/Modify/NoChange against a copy ofstate(don't mutate the caller's dict); in Complete mode, Delete un-templated extras (sorted). Tests: all-Create from empty, idempotent re-deploy = all-NoChange, Modify on changed props, Incremental leaves extras, Complete deletes them, bad mode raises, state not mutated.what_if(Ch. 8). Same classification, returned asChangeobjects, mutating nothing. Tests assert the input state is byte-identical after the call.
Run pytest test_lab.py -v after each step; commit when a section goes green. Then
python solution.py to see the worked plan: graph, deploy order, all-Create, idempotent
re-deploy, a Modify via what-if, and a Complete-mode Delete.
Success Criteria
You are ready for Phase 02 when you can, from memory:
- Draw the ARM request pipeline: authenticate (JWT) → authorize (RBAC, deny wins) → validate (Policy) → route (resource provider) — and name the HTTP error each gate throws.
- Given a template, list its implicit (
reference/resourceId) and explicit (dependsOn) edges and write a valid deploy order. - Run Kahn's algorithm on paper and explain how the leftover-nodes test detects a cycle.
- State why an ARM
PUTis idempotent and trace a re-deploy to all NoChange. - Explain Incremental vs Complete and the exact blast radius of each — and why
what-ifis the defense. - Describe the provisioningState async-poll loop and why dependents wait for a
dependency's
Succeeded. - Explain that Bicep transpiles to ARM and that deployment stacks + deny-settings protect against drift.
Interview Q&A
Q: Walk me through exactly what happens when I run az deployment group create.
The CLI acquires an Entra token and sends a deployment PUT to ARM. ARM authenticates the
JWT (signature, iss, aud, exp), authorizes via RBAC — union of Actions minus
NotActions across my assignments and inherited scopes, with deny assignments overriding —
and validates against Azure Policy (which may deny or modify the request). Then ARM
parses the template, builds a dependency graph from explicit dependsOn and implicit
reference()/resourceId() edges, topologically orders it, and deploys independent
resources in parallel — each routed to its resource provider as an idempotent PUT. Most
are async, so ARM returns 202 and polls provisioningState to Succeeded before
starting dependents. If I used the default Incremental mode, un-templated resources are
left alone; in Complete mode they'd be deleted.
Q: I never wrote a dependsOn, but my NIC deployed after my subnet. How?
Implicit dependencies. The NIC's subnet.id property uses resourceId(...) (or in Bicep,
a symbolic subnet.id reference) naming the subnet. ARM infers an edge from that reference
— you can't compute a subnet's ID until the VNet/subnet exists — so it orders the subnet
first automatically. Most correct templates have very few hand-written dependsOn precisely
because references already encode the order; explicit dependsOn is for orderings ARM
can't infer from a reference.
Q: Incremental vs Complete — which is the default, and when would Complete bite you?
Incremental is the default: it adds and updates the template's resources and leaves
everything else in the RG untouched. Complete makes the RG match the template exactly —
it deletes any resource not in the template. It bites you when something exists that
your template doesn't mention — a hand-created resource, another team's resource — and you
deploy in Complete mode: ARM deletes it, by design. Defenses: stay on Incremental, always
run what-if and read the Deletes, lock precious resources, and prefer deployment stacks
for intentional, scoped deletion.
Q: Why is an ARM deployment idempotent, and why do you care?
Because resources are deployed with PUT of desired state, not imperative commands. The
resource provider compares desired to actual and converges: Create if absent, Modify if
drifted, NoChange if identical. So re-running the same template is safe — it doesn't
duplicate anything; matching resources are NoChange. I care because it's the foundation of
CI/CD: every commit can re-apply the full template, drift reconciles back to desired state,
and a retry after a transient failure just resumes convergence. The classic break is a
resource with a randomly generated name — that Creates a new one every run and isn't
idempotent.
Q: A deployment is stuck. How do you debug it?
I look at the dependency graph and the provisioningState of each resource. A "stuck"
deploy is usually one resource whose async operation hasn't reached a terminal state —
because of a quota limit, a policy modify/deny, a resource-provider error, or a
dependency that Failed and is blocking its dependents (they correctly wait for the
dependency's Succeeded). I pull the deployment operations (az deployment operation group list) to find the specific resource and its error code, rather than watching the spinner.
Q: What's the difference between ARM, Bicep, and Terraform here?
ARM is the engine and the JSON template language — the control-plane API every Azure write
goes through. Bicep is a typed DSL that transpiles to ARM JSON: same engine, same graph,
same modes, nicer syntax with symbolic references and automatic dependencies. Terraform is a
different engine (Phase 02) with its own state file, plan/apply model, and ForceNew
replace semantics — it maps HCL to ARM calls via the azurerm provider but doesn't use
ARM's template engine. So "Bicep vs ARM" is front-end vs engine; "Terraform vs ARM/Bicep"
is two genuinely different deployment engines.
Q: How does what-if work, and why trust it?
It runs the same desired-vs-actual classification as a real deploy — build the graph,
compare each resource — but writes the result to a diff instead of to Azure, mutating
nothing. It classifies each resource as Create/Modify/Delete/NoChange/Ignore. I trust it as
a strong predictor and a safety net — especially for catching an unintended Delete before a
Complete-mode deploy — but not as a hard contract, since some changes only surface when the
resource provider actually evaluates the request.
References
- Microsoft Learn — Azure Resource Manager overview
- Microsoft Learn — Understand the structure and syntax of ARM templates
- Microsoft Learn — Define the order for deploying resources (dependsOn + implicit dependencies)
- Microsoft Learn — Deployment modes (Incremental and Complete)
- Microsoft Learn — ARM deployment what-if operation
- Microsoft Learn — Track asynchronous Azure operations
- Microsoft Learn — What is Bicep?
- Microsoft Learn — Azure deployment stacks
- Microsoft Learn — Azure Resource Manager resource provider registration
- Arthur B. Kahn, Topological sorting of large networks (1962) — the algorithm in Chapter 5
- The track's own CHEATSHEET.md and GLOSSARY.md