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
- Chapter 2: The Cloud Adoption Framework and the Azure Landing Zone
- Chapter 3: Management Groups — The Tree Above Subscriptions
- Chapter 4: Effective Inheritance — The One Formula
- Chapter 5: Subscription Vending — Governance by Construction
- Chapter 6: Policy-as-Code, Initiatives, and Compliance Scoring
- Chapter 7: The Canonical ALZ Hierarchy, Node by Node
- Chapter 8: Failure Modes and the 2 a.m. Diagnoses
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
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:
- Every subscription enforces the non-negotiables (allowed regions, required tags, no public IPs on internal workloads, mandatory diagnostic logging).
- A new subscription is governed the moment it exists — not after a manual checklist a human forgets to run.
- 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:
- Ready — prepare the environment. This is where landing zones live.
- Govern — keep 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:
| Pillar | What it provides | Track phase |
|---|---|---|
| Management-group hierarchy | the inheritance tree (this phase) | P05 |
| Policy-as-code baseline | the guardrails every subscription inherits | P04/P05 |
| RBAC model | who can do what, assigned at the right scope | P04 |
| Hub networking | shared firewall, DNS, ExpressRoute/VPN | P06 |
| Centralized logging | one Log Analytics workspace for the estate | P13 |
| Subscription vending | the automated "new subscription" pipeline | P05 |
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):
| Limit | Value | Why it matters |
|---|---|---|
| Management groups per tenant | 10,000 | you will never hit it |
| Hierarchy depth | 6 levels of MGs under the root | the cap your build_tree enforces |
| Levels including root + subscription | up to 8 total | root (1) + 6 MGs + the subscription (1) |
| MGs a subscription can belong to | exactly 1 | vending is placement, not membership |
| Direct children of a node | no fixed small cap | breadth 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).
notScopes | Exemption | |
|---|---|---|
| Lives on | the assignment (an array of excluded scopes) | a separate Microsoft.Authorization/policyExemptions resource at a scope |
| Excludes | a scope from this one assignment | an 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" |
| Lifecycle | structural, long-lived | usually 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):
| Step | Mechanism | Phase |
|---|---|---|
| Place under the right MG | this lab's place_subscription | P05 |
| Inherit policy/RBAC baseline | effective inheritance (Ch. 4) | P04/P05 |
| Set a budget + cost alerts | Cost Management budget | P14/FinOps |
| Peer to the hub VNet | VNet peering to Connectivity | P06 |
| Wire DNS / firewall routes | Private DNS, UDRs to the firewall | P06 |
| Bootstrap RBAC for the team | role assignment at the subscription | P04 |
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:
deny— block the non-compliant write at the control plane. A hard guardrail: you literally cannot create a public IP on a Corp subscription.audit— allow 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
denypolicy 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
notScopescarve-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
auditinitiative todeny, or added a new policy — and the existing estate was always non-compliant; the score just started counting it. Read theby_policybreakdown 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):
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."ancestors(mg_id, parent_map)— walk to the root, leaf-first.ancestors("corp", …)is["corp", "landing-zones", "root"];ancestors("root", …)is["root"](Ch. 3).effective_assignments(scope, assignments, hierarchy, exemptions)— select assignments whosescopeis on the ancestor chain, drop those excluded bynot_scopesfor this scope or by an exemption, and sort by(depth, name)for determinism (Ch. 4). Test: union over ancestors, off-chain assignments excluded,notScopesexclusion, exemption exclusion, and identical output across two runs.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).compliance_score(resources, initiative, evaluator)— any-fail per resource, aby_policycount, andscore_pct(Ch. 6). Test 100%, a partial case with theby_policybreakdown, 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:
- Draw the canonical ALZ management-group hierarchy and say what each node is for.
- State the management-group limits — depth ≤ 6 under root, exactly one MG per subscription.
- Write the effective-inheritance formula and explain why it is a union (additive), not an override.
- Distinguish a
notScopescarve-out from an exemption, with an example of each. - Explain subscription vending and why placement is the step that makes the baseline inherit.
- Read a compliance score with a
by_policybreakdown and name the policy + scope to fix first — and explain "audit first, then deny." - 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 thebicep-lz-vending/ Terraformlz-vendingmodules - Microsoft — Enterprise-Scale / ALZ reference implementations:
Azure/ALZ-BicepandAzure/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)