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

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:

  1. Authenticate. Your token is a JWT issued by Microsoft Entra ID. ARM verifies the signature against Entra's published keys (JWKS, selected by the kid in the header), and checks iss (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.
  2. Authorize. ARM computes your effective permissions: the union of Actions across all your role assignments at this scope and every ancestor scope, minus NotActions. Then — deny assignments win — any explicit deny overrides an allow. No matching allow, or a matching deny → 403. You build this in Phase 04.
  3. Validate. Azure Policy evaluates the request against assigned rules. A deny effect blocks the write (RequestDisallowedByPolicy); modify/append mutate the request (e.g. add a required tag) before it proceeds; audit just records non-compliance. Policy runs at create/update time, here, on the control-plane path. You build this in Phase 04 too.
  4. 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 get MissingSubscriptionRegistration.

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 a type, 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 → ∅.

Stepready (sorted)popemitted so far
0storage1, vnet1storage1storage1
1vnet1vnet1storage1, vnet1
2subnet1subnet1subnet1
3nic1nic1nic1
4vm1vm1vm1

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:

SituationActionEffect
resource does not existCreateprovision it to desired state
exists, desired ≠ actualModifyreconcile it to desired state
exists, desired = actualNoChangenothing 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 full PUT of 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 typeMeaning
Createresource will be added
Modifyresource exists; listed properties will change
Deleteresource will be removed (only in Complete mode / stacks)
NoChangeidentical; nothing happens
Ignoreexists 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 provisioningState on the resource: Accepted → Creating/Running → Succeeded (or Failed/Canceled), and
  • an Azure-AsyncOperation (or Location) header — a URL the client GETs repeatedly, honoring the Retry-After interval, 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, not resourceId(...). Because sa is a symbol, Bicep infers the dependency graph from the symbol references — you almost never write dependsOn. (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-version and catches typos and invalid properties before deploy.
  • Modules, loops, conditionals. First-class module, for, and if instead of copy-paste and copy blocks.

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 detachResources to leave it, or deleteResources to 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: denyDelete or denyWriteAndDelete — 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):

  1. _implicit_refs_in_value + the regex (Ch. 4). Write the regex that captures NAME from reference('NAME') and resourceId('NAME'). Recurse into dicts and lists — test_extract_implicit_resourceid_and_nested proves nested properties count.
  2. extract_dependencies (Ch. 4). Union dependsOn with the implicit refs; raise on a missing name; drop the self-edge. Tests: explicit+implicit union, self-reference dropped, missing-name raises.
  3. 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 unknown dependsOn and unknown implicit references.
  4. topological_order (Ch. 5). Kahn's algorithm, tie-break by name for determinism, raise ValueError("cycle: ...") if not every node is emitted. Tests: order respects deps, deterministic tie-break, cycle + self-cycle raise.
  5. deploy (Ch. 6–7). Validate the mode; topo-order; classify Create/Modify/NoChange against a copy of state (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.
  6. what_if (Ch. 8). Same classification, returned as Change objects, 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:

  1. Draw the ARM request pipeline: authenticate (JWT) → authorize (RBAC, deny wins) → validate (Policy) → route (resource provider) — and name the HTTP error each gate throws.
  2. Given a template, list its implicit (reference/resourceId) and explicit (dependsOn) edges and write a valid deploy order.
  3. Run Kahn's algorithm on paper and explain how the leftover-nodes test detects a cycle.
  4. State why an ARM PUT is idempotent and trace a re-deploy to all NoChange.
  5. Explain Incremental vs Complete and the exact blast radius of each — and why what-if is the defense.
  6. Describe the provisioningState async-poll loop and why dependents wait for a dependency's Succeeded.
  7. 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