Lab 01 — ARM Deployment Engine
Phase: 01 — Azure Resource Manager & the Deployment Engine | Difficulty: ⭐⭐⭐☆☆ | Time: 4–6 hours
Every single write in Azure — a storage account, a subnet, a role assignment — is one idempotent
PUTthat ARM routes through a pipeline: authenticate, authorize, validate, then resolve a dependency graph and deploy resources in topological order. This lab builds that engine: the implicit/explicit dependency resolver, a deterministic topological sort with cycle detection, the idempotent Create/Modify/NoChange decision, theComplete-mode delete foot-gun, and thewhat-ifdry run. Strip away the network, the JWT, and the resource providers, and what's left is a graph algorithm — the one ARM, Bicep, Terraform, and CloudFormation all share.
What you build
extract_dependencies(resource)— the union of explicitdependsOnand the implicit dependencies parsed out ofreference('NAME')/resourceId('NAME')calls hiding in (possibly nested) property strings. This is the step everyone gets wrong by hand.build_dependency_graph(resources)—name → {dependencies}, raising on a dependency to a resource that isn't in the template (ARM'sInvalidTemplate) and on duplicate names.topological_order(graph)— deterministic Kahn's algorithm, tie-breaking ready nodes by name so the same template always produces the same plan; raisesValueError("cycle: ...")on a circular dependency.deploy(resources, state, mode)— process in topo order; idempotentPUT→Create/Modify/NoChange;Completemode alsoDeletes un-templated resources. Returns the ordered actions and the converged state, without mutating the caller's state.what_if(resources, state, mode)— the side-effect-free dry run: the same classification asdeploy, returned as a list ofChangeobjects, mutating nothing.
Key concepts
| Concept | What to understand |
|---|---|
| Implicit vs explicit deps | reference()/resourceId() create edges you didn't type; ARM finds them, so must you |
| Topological order | dependencies deploy before dependents; independents run in parallel (we serialise by name for determinism) |
| Cycle detection | a dependsOn loop is un-deployable; Kahn's leftover-nodes test finds it |
Idempotent PUT | desired state, not a command → re-running converges (all NoChange), which is why IaC is safe to re-apply |
| Incremental vs Complete | Incremental leaves extras alone; Complete deletes anything not in the template — the classic 2 a.m. mistake |
what-if is read-only | the diff you read in the PR; mutating state in a "dry run" is a bug |
Files
| File | Purpose |
|---|---|
lab.py | skeleton with # TODO markers |
solution.py | complete reference; python solution.py prints a worked deployment plan |
test_lab.py | the proof — run it red, make it green |
requirements.txt | pytest only (pure stdlib otherwise) |
Run
pip install -r requirements.txt
pytest test_lab.py -v # against your lab.py
LAB_MODULE=solution pytest test_lab.py -v # against the reference
python solution.py # worked example
Success criteria
- All 25 tests pass against your implementation.
- You can explain why
test_topo_order_is_deterministic_tie_break_by_namematters — real ARM parallelises independent resources; we serialise ties by name only so the plan is reproducible and testable. The determinism is a lab affordance, not how ARM schedules. - You can explain why
test_redeploy_is_idempotent_all_no_changeis the whole reason IaC works: aPUTof desired state that already matches is a safe no-op. - You can explain, in one breath, why
test_complete_mode_deletes_extrasis the most dangerous line in the suite — and when you'd ever wantCompletemode. - Given a 5-resource template on paper, you can draw its dependency graph and write the deploy order in under two minutes.
How this maps to real Azure
| The lab | Real ARM |
|---|---|
extract_dependencies | ARM's template expression engine evaluating reference()/resourceId() and building implicit edges, plus your literal dependsOn |
build_dependency_graph | the deployment's internal DAG; an unknown reference is the deploy-time InvalidTemplate error |
topological_order | the deployment scheduler — except ARM deploys all currently-ready resources in parallel (bounded fan-out), not one-at-a-time |
deploy Create/Modify/NoChange | the resource provider's idempotent PUT: it compares desired vs actual and converges; provisioningState goes Accepted → Running → Succeeded and the client polls an async operation |
Complete mode delete | az deployment group create --mode Complete — deletes every resource in the RG not in the template |
what_if | az deployment group what-if / New-AzResourceGroupDeployment -WhatIf — the dry-run diff |
What the miniature leaves out (and why it's fine): real ARM also authenticates the
Entra JWT, authorizes via RBAC + deny assignments, runs Azure Policy (which can deny
or modify the request), registers resource providers, calls a different REST endpoint
per api-version, and handles long-running async operations by returning 202 Accepted with an Azure-AsyncOperation URL the client polls. None of that changes the
graph algorithm — which is exactly the part interviewers probe and incidents turn on.
Extensions (build these in your own subscription)
- Async operations: give each resource a
provisioningStatethat walksAccepted → Running → Succeeded|Failedover N "ticks," and makedeploypoll until terminal — now you've modeled the202/Azure-AsyncOperationloop. - Parallel waves: instead of a flat topo list, emit waves (all nodes whose deps are satisfied deploy together) — the real ARM scheduler. Assert that independent resources land in the same wave.
- Bicep front-end: parse a tiny Bicep-like syntax (
resource sa 'type' = { ... }) with symbolic references (sa.id) into your resource-dict format, proving Bicep is just a transpiler to the same graph. - Deployment stack / deny-settings: track which resources a "stack" manages; on the
next deploy,
Deletethe ones dropped from the stack (managed deletion) and reject an out-of-bandModifyto adenySettings: denyDeleteresource (drift protection). - Wire it to a real RG:
az deployment group what-if -g rg -f main.bicepand diff your predicted plan against Azure's — they should classify the same resources the same way.
Interview / resume
- Talking points: "Walk me through what happens when I run
az deployment group create." / "How does ARM know to deploy the VNet before the NIC?" / "What's the difference between Incremental and Complete mode, and which is the default?" / "Why is an ARMPUTidempotent and why does that matter for CI?" - Resume bullet: Built a dependency-graph deployment engine (implicit/explicit
reference resolution, deterministic topological scheduling with cycle detection,
idempotent
PUTconvergence, and a non-mutatingwhat-ifdiff) modeling the Azure Resource Manager control plane.