Warmup Guide — Azure Landing Zones & Management-Group Scale

Zero-to-principal primer for Phase 05. Phase 04 gave you the evaluator for a single assignment. This phase is about scale: when you have hundreds of subscriptions and thousands of engineers, governance is no longer a question you answer per-resource — it is a tree you walk. We start from "what even is a management group" and end with the arithmetic of effective inheritance, subscription vending, and a compliance score you can defend in an audit. Every later phase deploys into the landing zone you reason about here.

Table of Contents


Chapter 1: The Problem — Governance at Estate Scale

From zero. A single subscription is easy to govern: you assign a few policies and roles, and you are done. Now make it real. A bank has 300 subscriptions across dev/test/prod, multiple business units, two regulators, and 4,000 engineers who all want to ship. Three things must be true at once:

  1. Every subscription enforces the non-negotiables (allowed regions, required tags, no public IPs on internal workloads, mandatory diagnostic logging).
  2. A new subscription is governed the moment it exists — not after a manual checklist a human forgets to run.
  3. You can prove to an auditor, with a number, what fraction of the estate is compliant and exactly which control is failing where.

You cannot do this by hand-assigning policy to 300 subscriptions. The math alone defeats you: one change to a baseline policy is 300 edits, and you will miss some. What you need is a way to assign governance once, high up, and have it flow down to everything beneath — and a structure that puts "everything beneath" into sensible, regulator-shaped buckets.

That structure is the management-group hierarchy, and the discipline that designs it well is the Cloud Adoption Framework. This phase is where "I can configure a policy" becomes "I can govern an estate."

Chapter 2: The Cloud Adoption Framework and the Azure Landing Zone

What CAF is. The Cloud Adoption Framework (CAF) is Microsoft's published, opinionated methodology for adopting Azure in a governed way. It is not a product; it is a set of recommendations, reference architectures, and tooling organised into methodologies — Strategy, Plan, Ready, Adopt, Govern, Manage, Secure. Two of them matter most here:

  • Readyprepare the environment. This is where landing zones live.
  • Governkeep it compliant as it grows. This is where policy-as-code and compliance scoring live.

What a landing zone is. An Azure Landing Zone (ALZ) is the reference implementation of "Ready": a pre-built, governed environment a workload can be dropped into and immediately inherit identity, networking, policy, RBAC, and logging. Concretely it is six things wired together:

PillarWhat it providesTrack phase
Management-group hierarchythe inheritance tree (this phase)P05
Policy-as-code baselinethe guardrails every subscription inheritsP04/P05
RBAC modelwho can do what, assigned at the right scopeP04
Hub networkingshared firewall, DNS, ExpressRoute/VPNP06
Centralized loggingone Log Analytics workspace for the estateP13
Subscription vendingthe automated "new subscription" pipelineP05

Why it exists. Without it, every team builds its own snowflake: its own networking, its own (missing) logging, its own (absent) policy. The landing zone is the org saying "here is the paved road — build on it and security/compliance/networking are handled; leave it and you own all of that yourself." The whole point is self-service with guardrails — the only answer that scales to thousands of engineers.

The two flavors you will hear named. ALZ Bicep (Microsoft's accelerator deployed with Bicep/ARM) and ALZ Terraform (the Azure/caf-enterprise-scale / AVM modules). Both deploy the same conceptual hierarchy — the thing this lab models. Pick the IaC you already use; the governance model is identical.

Chapter 3: Management Groups — The Tree Above Subscriptions

From zero. You already know the scope ladder from Phase 04:

management group → subscription → resource group → resource

A management group (MG) is the container above a subscription. MGs nest into a tree with exactly one root — the Tenant Root Group, created automatically per Entra tenant, whose id is the tenant id. Every subscription hangs under exactly one MG. So the full address space is a forest collapsed into one tree:

Tenant Root Group  (the one node with no parent)
├── Platform
│   ├── Identity
│   ├── Management
│   └── Connectivity
├── Landing Zones
│   ├── Corp ──────────── sub-payments, sub-hr, …   (subscriptions hang here)
│   └── Online ────────── sub-shop, sub-api, …
├── Sandbox
└── Decommissioned

The hard limits (memorise — interviewers probe the off-by-one):

LimitValueWhy it matters
Management groups per tenant10,000you will never hit it
Hierarchy depth6 levels of MGs under the rootthe cap your build_tree enforces
Levels including root + subscriptionup to 8 totalroot (1) + 6 MGs + the subscription (1)
MGs a subscription can belong toexactly 1vending is placement, not membership
Direct children of a nodeno fixed small capbreadth is fine; depth is the constraint

Why depth is capped at 6. Inheritance is computed by walking from a scope up to the root, unioning assignments at every hop. A bounded depth keeps that walk cheap and keeps humans able to reason about it — six "why is this policy here?" hops is already a lot. Your build_tree rejects depth 7 for the same reason ARM does: it is an invariant, not a suggestion.

Under the hood — the walk. When ARM authorizes a request or computes effective policy at a resource, it does not look only at the resource. It assembles the scope chain:

resource → resource group → subscription → MG → parent MG → … → Tenant Root Group

and unions every assignment found at every link. The lab's ancestors(scope, parent_map) is this walk, returned leaf-first so index 0 is the most-local scope:

ancestors("sub-payments", tree)
# ['sub-payments', 'corp', 'landing-zones', 'root']

This is also the order you debug in: start at the broken resource, walk up until you find the assignment that explains the behavior. You can never answer "who can touch this / what applies here?" from the leaf alone.

Chapter 4: Effective Inheritance — The One Formula

This is the chapter the whole phase exists for. Write it on the whiteboard until it is reflex:

$$ \text{effective}(s) ;=; \underbrace{A(s)}{\text{local}} ;\cup; \bigcup{a ,\in, \text{ancestors}(s)} A(a) ;-; \text{notScopes} ;-; \text{exemptions} $$

In words: the assignments in effect at a scope are the union of everything assigned at that scope and at every ancestor up to the root — minus the scopes an assignment explicitly carves out (notScopes), minus any assignment a documented exemption suppresses.

Why union (not override). Assignments are additive. There is no "override a parent policy from a child" — a child can only add assignments, never silently delete an inherited one. (The one way to "remove" governance from below is a notScopes carve-out or an exemption, both of which are explicit and auditable — that is the design intent.) For RBAC the union is over Actions; for Policy it is over the set of assigned definitions. Phase 04 owns evaluating each assignment; Phase 05 owns which assignments are in the union.

notScopes vs exemptions (the distinction interviewers love).

notScopesExemption
Lives onthe assignment (an array of excluded scopes)a separate Microsoft.Authorization/policyExemptions resource at a scope
Excludesa scope from this one assignmentan assignment (or specific policies in it) at this scope
Typical use"this allowed-locations policy applies to all of root except Sandbox""this subscription is temporarily exempt from require-encryption while it migrates"
Lifecyclestructural, long-livedusually time-boxed, with an expiry and a justification

In the lab, Assignment.not_scopes is the first; the exemptions set passed to effective_assignments is the second. Both remove from the union, but they answer different governance questions — and a principal who conflates them gets caught in a design review.

Determinism — why ordering is part of the contract. The resolver sorts results by ancestor depth (most-local first), then assignment name:

effective_assignments("sub-payments", assignments, tree)
# [on-sub (depth 0), corp-baseline (1), allowed-locations (2, name<), require-tags (2)]

Most-local-first is the natural precedence order (it matches how you read a chain and how deny/precedence rules tend to surface the nearest assignment first), and a stable tiebreak by name makes the output byte-identical across runs — which is what lets you diff two estates, snapshot a baseline, and write a property test. Non-deterministic governance output is un-auditable; determinism is a feature, not a nicety.

Worked example. A Corp subscription sub-payments under corp:

root          : require-tags, allowed-locations(notScopes=[sandbox])
landing-zones : deny-public-ip
corp          : corp-no-internet
online        : online-waf-required      ← NOT on sub-payments' chain
platform      : platform-admins (rbac)   ← NOT on the chain

effective("sub-payments") walks [sub-payments, corp, landing-zones, root], so it picks up corp-no-internet, deny-public-ip, allowed-locations, require-tags — and not online-waf-required or platform-admins, because online and platform are siblings up the tree, not ancestors. That last point is where people slip: inheritance is down, never sideways.

Chapter 5: Subscription Vending — Governance by Construction

From zero. A team needs a new subscription. The bad way: file a ticket, a platform human manually creates a subscription, then runs a 30-step checklist (place it in the MG, assign RBAC, set a budget, peer the VNet, wire DNS, turn on logging) — and forgets step 17 on Tuesdays. The result is snowflake subscriptions and audit findings.

Subscription vending is the automated version: a pipeline takes a request (team, cost center, environment, Corp-vs-Online, budget) and produces a fully governed subscription. The load-bearing step is placement — hanging the new subscription under the correct management group:

parent_map = place_subscription("sub-shop", target_mg="online", parent_map=tree)
# sub-shop now inherits the entire 'online → landing-zones → root' baseline, automatically

The instant the subscription is a child of online, it inherits Online's policy, RBAC, and networking baseline by construction — no checklist, no forgotten step. That is the whole promise of a landing zone: governance is a property of where you are in the tree, not a sequence of manual actions.

What real vending wires up (beyond placement):

StepMechanismPhase
Place under the right MGthis lab's place_subscriptionP05
Inherit policy/RBAC baselineeffective inheritance (Ch. 4)P04/P05
Set a budget + cost alertsCost Management budgetP14/FinOps
Peer to the hub VNetVNet peering to ConnectivityP06
Wire DNS / firewall routesPrivate DNS, UDRs to the firewallP06
Bootstrap RBAC for the teamrole assignment at the subscriptionP04

The lab models the placement — the one step that makes all the others inherit. Note the invariant the lab enforces: a subscription belongs to exactly one MG, so vending the same subscription twice is an error (place_subscription raises if it is already placed).

Chapter 6: Policy-as-Code, Initiatives, and Compliance Scoring

Initiatives (policy sets). Assigning fifty individual policies to root is unmanageable. An initiative (Azure: policy set definition) bundles related policy definitions into one unit you assign once — e.g. the "Azure Security Benchmark" initiative is ~200 policies behind a single assignment. In the lab, initiative is just a list[dict] of policy definitions and compliance_score runs them all.

Guardrails: deny vs audit. Every policy declares an effect. Two dominate landing zones:

  • denyblock the non-compliant write at the control plane. A hard guardrail: you literally cannot create a public IP on a Corp subscription.
  • auditallow the write but flag it non-compliant. A soft guardrail: it surfaces in the compliance score without breaking deployments.

The principal pattern is "audit first, then deny." You roll a new control out as audit, watch the compliance score to size the blast radius (how much would deny have broken?), fix the violations, then flip to deny. Shipping a deny straight to prod is how you take down every team's pipeline at 9 a.m. In the lab, both deny and audit count a resource as non-compliant — because audit failing still means the resource violates the control, even though it was not blocked. That is faithful to Azure's compliance dashboard.

FAILING_EFFECTS = frozenset({"deny", "audit"})   # both mean "this resource failed the policy"

Compliance scoring — the number on the slide. Run every policy against every resource; a resource is non-compliant if any policy flags it. The score:

$$ \text{score_pct} ;=; 100 \times \frac{\text{compliant resources}}{\text{total resources}} $$

and a by_policy breakdown counts how many resources each policy flagged — so you do not just learn "we're 71%," you learn "the https-only policy is flagging 40 of the 80 violations, all in Online — fix that first." An empty estate scores 100% by convention (nothing failed). This is exactly what the Azure Policy Compliance blade computes, rolled up per management group.

Policy-as-code, the discipline. All of the above lives in Git: definitions, initiatives, and assignments as JSON/Bicep/Terraform, changed via pull request, reviewed by humans, deployed by a pipeline. Benefits that matter in an audit: every change has an author, a reviewer, and a timestamp; a bad rollout is a git revert; and you can run a what-if / compliance-diff in CI before a deny reaches prod. Governance becomes software, with software's safety nets — which is the entire thesis of this curriculum applied to the control plane.

Chapter 7: The Canonical ALZ Hierarchy, Node by Node

You must be able to draw this from memory and say why each node exists. It is a near-certain interview question and the spine of the lab's canonical_alz_tree().

Tenant Root Group
├── Platform          — shared services the whole org depends on; locked-down, central team
│   ├── Identity      — domain controllers, Entra Connect; the identity fabric
│   ├── Management    — Log Analytics, Automation, backup; the management plane
│   └── Connectivity  — hub VNet, Azure Firewall, ExpressRoute/VPN, Private DNS
├── Landing Zones     — where actual application workloads live
│   ├── Corp          — private workloads, NO direct internet (reach via the hub)
│   └── Online        — internet-facing workloads (public endpoints, WAF, Front Door)
├── Sandbox           — experimentation; loose guardrails, isolated from prod networking
└── Decommissioned    — subscriptions being retired; deny-most baseline, no new deployments

Why split Platform from Landing Zones? Different audiences and different rates of change. Platform is owned by a small central team, changes rarely, and is heavily locked down — a mistake there has org-wide blast radius. Landing Zones are owned by application teams, change constantly, and need self-service within guardrails. Putting them under different MGs lets you assign different policy baselines to each with a single assignment per branch.

Why Corp vs Online? They have opposite networking policies. Corp denies public IPs and forces traffic through the hub firewall; Online requires a WAF on its public endpoints. One assignment at corp and one at online — instead of per-subscription rules — is the entire payoff of having the sub-tree.

Why a Decommissioned MG? Retiring a subscription is dangerous (something always still depends on it). Moving it under a deny-most MG first freezes new deployments and shrinks the blast radius while you verify nothing breaks — governance as a safety procedure.

Sandbox is the pressure valve: give engineers a place to experiment without fighting the prod guardrails, but isolate it so a sandbox mistake cannot reach production networks or data. Note that allowed-locations in the lab's worked example carries notScopes=[sandbox] — a real pattern, loosening one control for the sandbox while keeping it everywhere else.

Chapter 8: Failure Modes and the 2 a.m. Diagnoses

The failures this layer produces, and the one-line diagnosis each maps to:

  • "My deployment is denied and I can't see why." A deny policy is inherited from an ancestor MG, not assigned at your subscription. Walk the chain (ancestors) and check every scope — the assignment is almost always two hops up. The resource never tells you; the tree does.
  • "I added a policy to the subscription but a child resource group ignores it." It does not ignore it — RGs inherit from the subscription. If it really is not applying, check for a notScopes carve-out or an exemption at that RG suppressing it.
  • "We moved a subscription and governance didn't change for ten minutes." Management-group moves and assignment changes are eventually consistent (cache up to a few minutes). Not a bug — wait, or check the Activity Log for the move's completion.
  • "Compliance dropped to 60% overnight." Someone flipped an audit initiative to deny, or added a new policy — and the existing estate was always non-compliant; the score just started counting it. Read the by_policy breakdown to find which policy, then which scope.
  • "A team can't create anything in their new subscription." It got vended under the wrong MG (Decommissioned or an over-locked Platform child). Re-place it under the right Landing-Zones node. Placement is the bug surface of vending.

The unifying skill: every one of these is answered by walking the scope chain and applying the one formula. That is why the lab is the lab.

Lab Walkthrough Guidance

Lab 01 — Landing-Zone Resolver, suggested order (matches the file top-to-bottom):

  1. build_tree(parent_map) — validate one root, no dangling parents, no cycles, and depth ≤ 6 levels under root. Test the boundary explicitly: depth 6 allowed, depth 7 rejected (Ch. 3). The cycle check is a bounded walk-to-root; the dangling-parent check is "every referenced parent is a key."
  2. ancestors(mg_id, parent_map) — walk to the root, leaf-first. ancestors("corp", …) is ["corp", "landing-zones", "root"]; ancestors("root", …) is ["root"] (Ch. 3).
  3. effective_assignments(scope, assignments, hierarchy, exemptions) — select assignments whose scope is on the ancestor chain, drop those excluded by not_scopes for this scope or by an exemption, and sort by (depth, name) for determinism (Ch. 4). Test: union over ancestors, off-chain assignments excluded, notScopes exclusion, exemption exclusion, and identical output across two runs.
  4. place_subscription(sub_id, target_mg, parent_map) — return a new map; validate the MG exists and the subscription is not already placed (Ch. 5).
  5. compliance_score(resources, initiative, evaluator) — any-fail per resource, a by_policy count, and score_pct (Ch. 6). Test 100%, a partial case with the by_policy breakdown, and the empty-estate = 100% convention.

Run it red first (pytest test_lab.py -v against lab.py), then green. Then run LAB_MODULE=solution pytest test_lab.py -v and python solution.py to see the worked example print the canonical tree, the effective inheritance for a Corp subscription, a vend, and a compliance score.

Success Criteria

You are ready for Phase 06 when you can, from memory:

  1. Draw the canonical ALZ management-group hierarchy and say what each node is for.
  2. State the management-group limits — depth ≤ 6 under root, exactly one MG per subscription.
  3. Write the effective-inheritance formula and explain why it is a union (additive), not an override.
  4. Distinguish a notScopes carve-out from an exemption, with an example of each.
  5. Explain subscription vending and why placement is the step that makes the baseline inherit.
  6. Read a compliance score with a by_policy breakdown and name the policy + scope to fix first — and explain "audit first, then deny."
  7. Diagnose "my deployment is denied and I can't see why" by walking the scope chain.

Interview Q&A

Q: What is a management group, and how does it differ from a resource group? A resource group is below a subscription — a container for resources that share a lifecycle, and the smallest scope you typically assign at the bottom. A management group is above a subscription — a container for subscriptions (and other MGs), forming a tree under the Tenant Root Group. The key behavioral difference is direction of inheritance: an assignment at an MG flows down through every subscription, RG, and resource beneath it, which is exactly what lets you govern hundreds of subscriptions with one assignment. RGs do not nest; MGs do (up to 6 levels). A subscription has exactly one parent MG and exactly one position in the tree.

Q: A resource is being denied a deployment and the developer swears there's no deny policy on their subscription. Walk me through the diagnosis. The deny is almost certainly inherited from an ancestor, not assigned at the subscription — that is the single most common landing-zone confusion. I assemble the scope chain: resource → RG → subscription → MG → parent MG → … → Tenant Root Group, and I check for an assignment at every link, because the resource itself tells you nothing about inherited governance. In an ALZ, a Corp subscription's "no public IP" deny typically lives two hops up, at the corp or landing-zones MG. If I find it and there's a legitimate reason to allow this one case, the fix is not to delete the policy (that breaks everyone) — it is a notScopes carve-out for this scope or a time-boxed exemption with a justification. That distinction — inherited governance, surgical exclusion — is the whole skill.

Q: Difference between notScopes and a policy exemption? notScopes lives on the assignment and excludes a scope from that one assignment — "this allowed-locations policy applies to all of root except the Sandbox MG." It is structural and long-lived. An exemption is a separate resource at a scope that suppresses an assignment (or specific policies within an initiative) for that scope — "this subscription is exempt from require-encryption until 2025-09-01 while it migrates." It is typically time-boxed, carries a justification and a category (Waiver vs Mitigated), and is the auditable way to say "we know, and here's why, and here's when it ends." Both remove from the effective set; they answer different governance questions and an auditor cares which you used.

Q: Why is the management-group hierarchy depth capped, and at what? At 6 levels of MGs under the Tenant Root Group (so up to 8 scopes total including root and the subscription). It is capped because effective access and effective policy are computed by walking the chain to the root and unioning at every hop — a bounded depth keeps that walk cheap and, more importantly, keeps humans able to reason about it. Six "why is this here?" hops is already near the limit of what's debuggable. It is a hard invariant: trying to create a 7th level fails. In the lab, build_tree enforces exactly this, and the boundary test (depth 6 allowed, 7 rejected) is the off-by-one a principal carries.

Q: Walk me through subscription vending and why it's better than a ticket. Vending is the automated, policy-compliant allocation of a new subscription. A team submits a request (cost center, environment, Corp-vs-Online, budget); a pipeline creates the subscription and — the load-bearing step — places it under the correct management group. The instant it's a child of, say, online, it inherits Online's entire policy/RBAC/networking baseline by construction. The pipeline then wires the budget, peers the hub VNet, bootstraps team RBAC, and turns on logging. The win over a ticket is twofold: speed (minutes, not a two-week queue) and correctness (no human forgets step 17 — governance is a property of where you are in the tree, not a checklist). The failure surface is placement: vend under the wrong MG and the team either can't deploy (over-locked) or is dangerously under-governed.

Q: How do you roll out a new mandatory control to a 300-subscription estate without breaking everyone? Audit first, then deny. I assign the policy at the right MG (so it inherits everywhere) but with effect audit, not deny. The compliance score and by_policy breakdown immediately tell me the blast radius: how many resources would a deny have blocked, and in which scopes. I work that list down — remediate, grant time-boxed exemptions where justified, give teams a deadline — and only when the violation count is near zero do I flip the assignment to deny. Shipping deny straight to prod is how you take down every pipeline at 9 a.m. The whole thing is policy-as-code in Git, so the change is reviewed in a PR, the rollout is a pipeline, and a regression is a git revert.

Q: Your estate's compliance score is 71%. What do you do? First, I don't panic at the single number — I read the by_policy breakdown and the per-MG rollup, because "71%" is meaningless until I know which policy and which scope. Say it's the https-only policy flagging 90% of the gap, concentrated in the online MG. Now it's a specific, answerable problem: is https-only a deny or audit (am I blocking or just flagging)? Are these legitimate exceptions (then time-boxed exemptions) or real misconfig (then a remediation — a deployIfNotExists policy can auto-fix many)? I fix the highest-count policy first because it moves the number most, document each exemption, and re-score. The compliance dashboard rolls up the MG tree, so I can show the score improving per scope — which is exactly what an auditor and an exec want to see.

References

  • Microsoft Learn — Cloud Adoption Framework: What is an Azure landing zone? (learn.microsoft.com/azure/cloud-adoption-framework/ready/landing-zone/)
  • Microsoft Learn — Azure landing zone conceptual architecture and the management-group hierarchy (.../ready/landing-zone/design-area/resource-org-management-groups)
  • Microsoft Learn — Organize your resources with management groups (learn.microsoft.com/azure/governance/management-groups/overview) — limits, depth, root
  • Microsoft Learn — Azure Policy: Overview, Initiative (policy set) definitions, Exemption structure, and Get policy compliance data
  • Microsoft Learn — Subscription vending (.../ready/landing-zone/design-area/subscription-vending) and the bicep-lz-vending / Terraform lz-vending modules
  • Microsoft — Enterprise-Scale / ALZ reference implementations: Azure/ALZ-Bicep and Azure/caf-enterprise-scale (Terraform) on GitHub
  • The track's own CHEATSHEET.md (Management-group inheritance section) and GLOSSARY.md (Governance at Scale — Landing Zones)
  • Phase 04 — RBAC & Azure Policy Evaluation (the single-assignment evaluator this phase composes across the tree)