Architecture Azure

Designing the Enterprise-Scale Landing Zone Management Group Hierarchy and Policy Layering

The management group hierarchy is the load-bearing wall of an enterprise-scale landing zone. Get it right and governance scales for free: every new subscription inherits the correct guardrails the moment it lands in the tree, and onboarding the two-hundredth workload costs the platform team exactly as much effort as the second. Get it wrong and you spend the next two years untangling policy assignments, exemptions, and one-off RBAC grants that nobody dares to touch, because nobody can predict what breaks when they do. This is the part of the platform you change least and reason about most, so it deserves to be designed deliberately, not grown organically.

Azure Landing Zones (ALZ) — the architecture pattern the Cloud Adoption Framework (CAF) calls enterprise-scale — answers this with an opinionated, battle-tested shape: a single intermediate root under the Tenant Root Group, a Platform branch split into Identity, Management and Connectivity, a Landing Zones branch split into Corp and Online archetypes, plus Sandbox and Decommissioned at the edges. Onto that skeleton it layers roughly forty default Azure Policy assignments — a curated mix of Deny guardrails and DeployIfNotExists auto-remediation — an RBAC model built on a handful of personas, and a subscription vending pipeline that turns “we need a new environment” into a twenty-minute automated operation instead of a three-week ticket queue.

This article is the full design treatment. You will work through the hierarchy node by node, the exact policy assignments the ALZ library places at each scope and why, the mechanics of DINE remediation identities, the RBAC and Privileged Identity Management (PIM) model, subscription vending with the lz-vending Terraform module and the subscription alias API, the hub-spoke versus Virtual WAN decision inside the connectivity subscription, the identity subscription that still runs your domain controllers, the accelerator/Bicep/Terraform implementation options and precisely what to customize versus keep, brownfield migration of a live estate, and the anti-patterns I have been paid to dismantle. Real az, real Bicep, real Terraform throughout — this is the reference I wish someone had handed me before my first hundred-subscription tenant.

What problem this solves

Without a designed hierarchy, an Azure tenant converges on the same failure state every time. Subscriptions accumulate directly under the Tenant Root Group, each one governed by whatever ad-hoc policies someone remembered to assign. Security baseline drift becomes unmeasurable because there is no single scope that provably covers everything. Network isolation between internet-facing and internal workloads exists only by convention. Every new subscription request triggers a bespoke setup ritual — assign these roles, create that VNet, remember the diagnostic settings — performed slightly differently each time by whoever picks up the ticket. Two years in, an auditor asks “show me that no production workload can expose a public endpoint” and the honest answer is a shrug.

The costs are concrete and they compound:

Failure without a designed hierarchy How it shows up Typical blast radius
No provable baseline Audit findings; failed ISO 27001 / RBI / PCI evidence requests Whole tenant; weeks of manual evidence gathering
Snowflake subscriptions Each subscription configured by hand, differently Every incident becomes archaeology
Org-chart governance Policy scoped to departments, reorg breaks scoping Re-plumbing assignments after every restructure
Manual subscription setup 2–6 week lead time per environment Teams bypass the platform, shadow IT in personal tenants
Guardrails bolted on late Deny policies flipped on over live workloads Production deployment outages caused by governance
RBAC sprawl Owner grants at subscription scope to unblock tickets Privilege escalation paths nobody can enumerate
No decommissioning path “Dead” subscriptions with live credentials and old VMs Breach entry points; zombie spend

The management group hierarchy solves this by making governance a property of position in the tree. A subscription placed under corp is — by construction, not by checklist — denied public IPs on NICs, wired into central logging, backed up, monitored by Defender for Cloud, and restricted to approved regions. The platform team stops configuring subscriptions and starts placing them. That single shift is what lets a four-person platform team govern 400 subscriptions, and it is why this design is the highest-leverage decision in your entire Azure estate.

Who hits this problem: any organisation past roughly ten subscriptions, any regulated industry that must evidence uniform controls, any platform team fielding subscription requests faster than it can hand-craft them, and — most painfully — any brownfield estate that grew for five years before anyone said the words “landing zone”.

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

This is an Expert-level article. You should already be fluent in Azure management groups fundamentals (what a management group is, inheritance mechanics), subscription types and billing boundaries, Azure Policy effects, and RBAC roles, scopes and assignments. You should have Owner-equivalent rights in a test tenant for the lab — a Microsoft 365 developer tenant or a Visual Studio subscription tenant works.

In the Cloud Adoption Framework this material sits in the Ready phase (see Azure CAF Ready) and feeds directly into Govern (Azure CAF Govern). The ALZ conceptual architecture is organised into eight design areas; this article owns design area C and touches most of the others:

Design area Scope Where this article covers it
A — Billing and Entra tenant EA/MCA structure, tenant strategy Subscription vending billing scopes; see Azure landing zone billing and tenant design
B — Identity and access management RBAC, PIM, identity resources RBAC model section; identity subscription
C — Resource organisation Management groups and subscriptions The entire hierarchy design — this article’s core
D — Network topology and connectivity Hub-spoke vs vWAN Connectivity subscription section; deep dive in Enterprise-scale connectivity subscription hub design
E — Security Defender for Cloud, baselines Policy baseline (MDFC assignments), security notes
F — Management Logging, monitoring, backup Management subscription; see Azure landing zone management
G — Governance Policy-driven guardrails Policy layering sections; see Azure Policy governance at scale
H — Platform automation and DevOps IaC, pipelines, vending Accelerator/Bicep/Terraform and vending sections

Core concepts

The five design principles, and what each one forces

Enterprise-scale is not a diagram; it is five principles that generate the diagram. Every structural argument in this article resolves by appeal to one of them:

# Principle What it means Structural consequence
1 Subscription democratization Subscriptions are the unit of management, scale and isolation — hand them out liberally to workload teams You need vending automation and archetype MGs, because manual per-subscription setup cannot scale
2 Policy-driven governance Guardrails are Azure Policy at management-group scope, not documents or review boards The tree must be shaped by policy difference, not org structure; compliance is queryable, not asserted
3 Single control and management plane One way in: Azure Resource Manager. No wrapper portals that hide ARM IaC and pipelines target ARM directly; the hierarchy is expressible entirely in Bicep/Terraform
4 Application-centric service model Govern by workload archetype (corp/online), not by technology or department Landing Zones children are archetypes; there is no SAP-MG or Finance-MG in the canonical tree
5 Azure-native design and roadmap alignment Prefer platform-native services (Policy, Monitor, Defender) over third-party governance layers The baseline leans on built-in policies and Azure-native tooling that Microsoft keeps evolving

Principle 1 is the one that changes organisational behaviour. Pre-landing-zone, subscriptions are scarce and precious, so teams cram unrelated workloads into shared subscriptions and fight over quotas, RBAC and networking. Democratization inverts this: a subscription costs nothing, carries hard isolation for identity, network, policy, billing and quota — so default to a new subscription per workload per environment. This is only safe because principle 2 means every subscription is born governed. The two principles are a matched pair; adopting democratization without policy-driven governance is how you get four hundred snowflakes instead of forty.

The vocabulary in one table

Concept One-line definition Why it matters here
Tenant Root Group Built-in root management group; ID equals the Entra tenant ID Never assign day-to-day policy here; it governs even future platform MGs
Intermediate root Your single top-level MG (e.g. contoso) under the tenant root The scope for org-wide, non-negotiable guardrails
Management group (MG) A container for subscriptions/MGs that scopes policy and RBAC The only reason an MG should exist is a distinct policy or RBAC need
Archetype An MG plus the policy set and RBAC model attached to it Subscriptions are placed into archetypes, never individually configured
Landing zone A subscription provisioned into an archetype with baseline plumbing The unit workload teams receive
Platform subscription Runs shared services (hub network, logging, identity) Different owners, different policies — hence a separate branch
Policy assignment A policy/initiative applied at a scope, with parameters Inherited by everything below; the layering mechanism
Initiative (policy set) A versioned group of policy definitions assigned as one unit Keeps assignment count manageable against the 200/scope limit
DINE DeployIfNotExists — policy that deploys missing configuration How the platform self-heals instead of raising tickets
Subscription vending Automated creation + placement + baseline of subscriptions Democratization made operational
ALZ library Microsoft’s versioned set of ALZ policy definitions/assignments/archetypes The upstream you consume and overlay, never fork
Canary hierarchy A parallel mini-tree for testing platform changes The only safe way to test policy changes before production scopes

The limits that shape the design

Management groups and Azure Policy have hard service limits. None of them bites a well-designed tree; several of them bite badly-designed ones:

Limit Value Design implication
Management groups per tenant 10,000 Effectively unlimited — depth, not count, is your constraint
Tree depth 6 levels (excluding tenant root and subscriptions) ALZ uses 3–4; if you need 6 you have modelled the org chart
Parents per MG or subscription Exactly 1 No subscription can live in two archetypes — pick one
Policy/initiative assignments per scope 200 Assign initiatives, not loose policies, or corp+inherited blows the budget
Policy definitions per initiative ~1,000 (current documented cap) One “corp baseline” initiative can carry the whole archetype
Custom policy definitions per scope 500 Store definitions once at the intermediate root, not per-MG
Role assignments per subscription 4,000 Vending must not spray direct user assignments; use groups
Role assignments per management group 500 Another reason personas are groups, not individuals
MG ID (name) Immutable after creation, max 90 chars Get naming right on day one; display names can change, IDs cannot

Two mechanics deserve emphasis because everything later builds on them. First, inheritance is a union: a subscription under corp is evaluated against every assignment on corp, landingzones, contoso, and the Tenant Root Group, plus its own direct assignments. There is no override — a lower scope cannot relax a higher Deny; it can only be carved out with an exemption. Second, evaluation is positional and near-immediate for new requests: move a subscription and, within about thirty minutes (usually faster), every new ARM request in it is evaluated against the destination tree’s assignments. Existing-resource compliance data refreshes on the next evaluation cycle (up to 24 hours, or on demand with az policy state trigger-scan). Governance really is a property of position.

Where new subscriptions land, and hierarchy protection

Two tenant-level settings are part of the core model and almost universally forgotten. By default, any subscription created in the tenant lands in the Tenant Root Group — outside your governed tree. And by default, any Entra user can create management groups, which lets a random developer graft ungoverned branches onto your hierarchy. Fix both on day one:

TENANT_ID=$(az account show --query tenantId -o tsv)

# 1. Default new subscriptions into a governed holding MG (here: sandbox),
#    and require MG-write permission to create new management groups.
az account management-group hierarchy-settings create \
  --name "$TENANT_ID" \
  --default-management-group "/providers/Microsoft.Management/managementGroups/contoso-sandbox" \
  --require-authorization true

# Equivalent raw API if your CLI build lacks the command group:
az rest --method put \
  --url "https://management.azure.com/providers/Microsoft.Management/managementGroups/${TENANT_ID}/settings/default?api-version=2023-04-01" \
  --body '{"properties":{"defaultManagementGroup":"/providers/Microsoft.Management/managementGroups/contoso-sandbox","requireAuthorizationForGroupCreation":true}}'

The first setting means even a subscription created outside your vending process is born inside guardrails (sandbox-grade ones, which is the right default for an unknown workload). The second closes the ungoverned-branch hole. Together they make the tree total: nothing exists outside it.

The canonical hierarchy, node by node

The full canonical tree, with every node’s reason to exist:

Node Parent Purpose Typical subscriptions Distinct policy posture Distinct RBAC
Tenant Root Group Built-in anchor; ID = tenant GUID None (keep it empty) None assigned day-to-day Root elevation only, break-glass
contoso (intermediate root) Tenant Root Org-wide non-negotiables None directly MCSB audit, Defender config, activity-log DINE, deny classic/unmanaged-disk Platform owner (PIM-eligible)
platform contoso Shared services umbrella None directly Platform monitoring DINEs Platform team only
identity platform Identity infrastructure 1 identity subscription (per region-pair) Deny public IP, deny mgmt ports, deny subnet w/o NSG, VM backup DINE Identity/AD team
management platform Logging, monitoring, automation 1 management subscription Log Analytics DINE Ops/observability team
connectivity platform Hub network, DNS, edges 1+ connectivity subscriptions DDoS DINE on VNets Network team (NetOps)
landingzones contoso All workload archetypes None directly The big workload baseline (deny + DINE set) Nothing broad — per-sub below
corp landingzones Internal, hub-connected workloads Most workload subscriptions Deny public endpoints/IPs, private DNS DINE Workload teams per subscription
online landingzones Internet-facing workloads Public web/API subscriptions Inherits LZ baseline; public ingress allowed Workload teams per subscription
sandbox contoso Experimentation Per-team/per-person sandboxes Loosened, but no hybrid connectivity Sandbox owners, time-boxed
decommissioned contoso Retirement holding pen Subscriptions being wound down Deny new deployments; deallocate VMs Read-mostly, platform-controlled

The tenant root and the intermediate root

The Tenant Root Group exists whether you like it or not, and every management group and subscription ultimately descends from it. The canonical guidance is to leave it alone: no policy assignments, no broad RBAC. Two reasons. First, anything at tenant root hits everything forever, including the platform itself and any future branch (a canary hierarchy, an acquired company’s estate) that you may need to govern differently — you lose the ability to have any scope in the tenant that differs. Second, operationally it is where subscriptions land by default from outside your process (until you change the default, above), and you want the freedom to treat those as ungoverned quarantine rather than instantly subjecting them to deny policies that might strand a migration.

The intermediate root (contoso here; alz in the reference implementations) is your real root. Everything you consider non-negotiable for the entire organisation — allowed regions, security baseline auditing, central activity logging, Defender for Cloud enablement — is assigned here exactly once. One intermediate root also gives you a clean lift point: tenant migrations and canary testing both become “stand up a second intermediate root”, which is impossible if you built directly under the tenant root.

The platform branch: identity, management, connectivity

The platform / landingzones split is the most important boundary in the design. Platform subscriptions run services that workloads depend on but must never administer: the hub network, the DNS zones, the central Log Analytics workspace, the domain controllers. They are owned by the platform team, carry different guardrails (an identity subscription legitimately runs VMs with domain-controller network profiles that would violate corp policy), and their failure domain is the whole estate. The three children are not decoration — each has a genuinely different policy-and-RBAC contract, which per the design rules is precisely what justifies a management group:

The landing zones branch: corp and online archetypes

landingzones carries the baseline every workload gets. Its children are archetypes, and the canonical two are:

The discipline that keeps this working: archetypes are a small fixed menu, extended only by deliberate platform change. Common legitimate extensions are confidential-corp / confidential-online (regulatory: confidential-computing SKUs, customer-managed keys, tighter residency) and occasionally an arc/edge archetype for hybrid estates. What is never legitimate is a per-team or per-application archetype — that is the org chart sneaking back in.

Design question Add a management group? Instead use
Different policy set needed (e.g. confidential workloads) Yes — new archetype under landingzones
Different environment (dev/test/prod) No Separate subscriptions in the same archetype; tags + parameterised policy where needed
Different business unit No Tags for cost attribution; RBAC at subscription scope
Different region No Allowed-locations policy parameters; region is a resource property
Different app team No One subscription per app per environment, same archetype
Acquired company with different rules (transitional) Yes — temporary sibling branch under the intermediate root Merge into archetypes as remediation completes

Sandbox and decommissioned: the edges of the lifecycle

sandbox gives experimentation a sanctioned home so it stops happening in production subscriptions (or worse, personal tenants). Policy is deliberately loose — audit rather than deny for most of the baseline — with two hard exceptions: no hybrid connectivity (deny VPN/ExpressRoute gateways, deny peering toward the corp estate) so a sandbox mistake cannot reach corp data, and cost controls (budgets with automation). decommissioned is the other end: when a workload retires, its subscription moves here and inherits an initiative that denies new deployments and deallocates running VMs, so a dead subscription cannot quietly resurrect. Both nodes exist because both lifecycle states have — again — a genuinely distinct policy posture.

Naming: IDs are forever

Management group IDs are immutable; only display names can change. The ALZ implementations default to a prefix scheme you should adopt or consciously replace on day one:

Node ALZ-Bicep default ID Display name Note
Intermediate root alz Azure Landing Zones Most orgs substitute their brand: contoso
Platform alz-platform Platform Prefixing prevents collisions — IDs are tenant-unique
Identity alz-platform-identity Identity
Management alz-platform-management Management
Connectivity alz-platform-connectivity Connectivity
Landing zones alz-landingzones Landing Zones
Corp alz-landingzones-corp Corp
Online alz-landingzones-online Online
Sandbox alz-sandbox Sandbox
Decommissioned alz-decommissioned Decommissioned

Provisioning the skeleton by hand is a handful of az calls (ARM PUT semantics — safe to re-run):

# Intermediate root under the tenant root group
az account management-group create --name contoso --display-name "Contoso"

# Platform branch
az account management-group create --name contoso-platform     --display-name "Platform"     --parent contoso
az account management-group create --name contoso-identity     --display-name "Identity"     --parent contoso-platform
az account management-group create --name contoso-management   --display-name "Management"   --parent contoso-platform
az account management-group create --name contoso-connectivity --display-name "Connectivity" --parent contoso-platform

# Landing zones branch
az account management-group create --name contoso-landingzones --display-name "Landing Zones" --parent contoso
az account management-group create --name contoso-corp         --display-name "Corp"          --parent contoso-landingzones
az account management-group create --name contoso-online       --display-name "Online"        --parent contoso-landingzones

# Lifecycle edges
az account management-group create --name contoso-sandbox        --display-name "Sandbox"        --parent contoso
az account management-group create --name contoso-decommissioned --display-name "Decommissioned" --parent contoso

The same skeleton in Bicep, deployed at tenant scope (this is what the ALZ-Bicep managementGroups module does under the hood):

targetScope = 'tenant'

param parPrefix string = 'contoso'

resource mgRoot 'Microsoft.Management/managementGroups@2023-04-01' = {
  name: parPrefix
  properties: {
    displayName: 'Contoso'
  }
}

resource mgPlatform 'Microsoft.Management/managementGroups@2023-04-01' = {
  name: '${parPrefix}-platform'
  properties: {
    displayName: 'Platform'
    details: {
      parent: { id: mgRoot.id }
    }
  }
}

resource mgLandingZones 'Microsoft.Management/managementGroups@2023-04-01' = {
  name: '${parPrefix}-landingzones'
  properties: {
    displayName: 'Landing Zones'
    details: {
      parent: { id: mgRoot.id }
    }
  }
}
// ...identity/management/connectivity/corp/online/sandbox/decommissioned follow the same shape

// Deploy with rights at '/':
//   az deployment tenant create --location centralindia --template-file managementGroups.bicep

One bootstrap gotcha: to create the intermediate root under the tenant root, and to manage hierarchy settings, you need rights at root scope /. A Global Administrator gets them by elevating access (az rest --method post --url "/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"), granting themselves User Access Administrator at / — do it, delegate properly to the platform identity, then remove the elevation. Leaving elevated access lying around is a standing audit finding.

Policy layering: what the ALZ baseline assigns at every scope

Policy in this model is layered: each level adds guardrails, and a subscription is governed by the union of every assignment from the tenant root down. Assigning at the right scope is the entire game — too high and you over-constrain the platform (or worse, break it); too low and you repeat yourself and leave gaps. The layering rules:

Rule Statement Rationale
1 Assign initiatives, not loose policies 200-assignment limit per scope; auditability; parameter management in one place — see initiatives vs definitions
2 Assign at the highest scope where the rule is universally true True for all workloads → landingzones; true for the whole org → intermediate root; corp-only → corp
3 Prefer DINE/Modify over Deny where remediation is possible Deny creates tickets; DINE fixes silently. Reserve Deny for must-never-exist
4 Store custom definitions once, at the intermediate root Definitions are visible to all descendant scopes; per-MG copies drift
5 Never assign day-to-day policy at the Tenant Root Group You lose the ability to have any differing scope, including canary
6 Parameterise, don’t fork One definition, different parameter values per assignment scope
7 Every Deny gets a nonComplianceMessage The engineer who hits it should learn the fix from the error itself

What the ALZ library assigns where

The ALZ policy library ships (order of magnitude) 100+ custom definitions, a dozen initiatives, and ~40 default assignments distributed across the tree. Assignment names below are the library’s own; the library is versioned and evolves — pin a release and review its changelog rather than tracking main. At the intermediate root:

Assignment Effect What it does Parameters you must supply
Deploy-ASC-Monitoring Audit Assigns the Microsoft cloud security benchmark (built-in initiative 1f3afdf9-d0c9-4c3d-847f-89da613e70a8) — the compliance yardstick for the whole estate None (defaults)
Deploy-MDFC-Config DINE Enables and configures Defender for Cloud plans, security contacts, and export to Log Analytics Security contact email; central LA workspace ID
Deploy-AzActivity-Log DINE Routes every subscription’s activity log to the central Log Analytics workspace Central LA workspace resource ID
Deploy-Resource-Diag DINE (initiative) Deploys diagnostic settings for dozens of resource types to central logging Central LA workspace resource ID
Deny-Classic-Resources Deny Blocks classic (ASM) deployment-model resources None
Deny-UnmanagedDisk Deny Blocks VMs/VMSS using unmanaged disks None

Note what is absent: allowed-locations is deliberately not assigned by default (Microsoft cannot guess your regions) — adding it at the intermediate root, parameterised with your approved regions plus the resource-group variant (e56962a6-4747-49cd-b67b-bf8b01975c4c and e765b5de-1225-4ba3-bd56-1ac6695af988), is the first customization almost everyone makes.

Down the platform branch:

Scope Assignment Effect What it does
platform AMA monitoring set (Deploy-VM-Monitoring, Deploy-VMSS-Monitoring, change-tracking DINEs — 2024+ library refresh) DINE Installs Azure Monitor Agent + data collection rules on platform VMs
platform DenyAction-DeleteUAMIAMA (recent releases) DenyAction Blocks deletion of the user-assigned managed identity AMA depends on
identity Deny-Public-IP Deny No public IPs in the identity subscription, full stop
identity Deny-MgmtPorts-Internet Deny No NSG rule may open RDP/SSH/management ports from Internet
identity Deny-Subnet-Without-Nsg Deny Every subnet must carry an NSG — DCs never sit on open subnets
identity Deploy-VM-Backup DINE Auto-enrols identity VMs (your DCs) into Azure Backup
management Deploy-Log-Analytics DINE Deploys the central Log Analytics workspace if absent
connectivity Enable-DDoS-VNET Modify/DINE Associates VNets with the central DDoS Network Protection plan

The landingzones scope carries the heart of the workload baseline — everything true for every workload, corp and online alike:

Assignment Effect What it does
Deny-IP-Forwarding Deny NICs may not enable IP forwarding (no rogue NVAs in workload subs)
Deny-MgmtPorts-Internet Deny No RDP/SSH exposed to Internet in any workload
Deny-Subnet-Without-Nsg Deny Every workload subnet carries an NSG
Deny-Storage-http Deny Storage accounts must enforce HTTPS (404c3081-a854-4457-ae30-26a93ef643f9 built-in underneath)
Enforce-TLS-SSL (initiative) Deny/DINE mix TLS-in-transit enforcement across PaaS (SQL, storage, web)
Deploy-VM-Backup DINE Every workload VM enrolled in backup automatically
Enable-DDoS-VNET Modify/DINE Workload VNets join the DDoS plan
Deploy-AKS-Policy DINE Installs the Azure Policy add-on on every AKS cluster
Deny-Priv-Esc-AKS / Deny-Priv-Containers-AKS Deny (K8s) Blocks privilege escalation and privileged containers in AKS
Enforce-AKS-HTTPS Deny (K8s) AKS ingress must be HTTPS
SQL security set (auditing/threat-detection/TDE DINEs) DINE SQL databases get auditing, Defender, encryption without asking

Then the archetypes diverge. corp adds the private-by-default posture; online adds nothing by default — its identity is the absence of the corp denies; sandbox and decommissioned get their lifecycle initiatives:

Scope Assignment Effect What it does
corp Deny-Public-Endpoints (initiative) Deny PaaS services may not enable public network access
corp Deploy-Private-DNS-Zones (initiative) DINE Private-endpoint DNS zone groups auto-wire into the central private DNS zones in connectivity
corp Deny-Public-IP-On-NIC Deny NICs may not attach public IPs (83a86a26-fd1f-447c-b59d-e51f44264114 built-in)
corp Audit-PeDnsZones (recent releases) Audit Flags private endpoints wired to non-central DNS zones
online — (empty by default) Public ingress permitted; add WAF/DDoS audit policies as your own overlay
sandbox Enforce-ALZ-Sandbox (initiative) Deny No VPN/ER gateways, no peering toward the corp estate — sandboxes stay islands
decommissioned Enforce-ALZ-Decomm (initiative) Deny + DINE Denies new deployments; deallocates running VMs

Read the whole table stack top-to-bottom for any subscription and you get its effective contract. A corp subscription is evaluated against ~6 assignments from the intermediate root, ~11 from landingzones, ~4 from corp, plus anything direct — call it two dozen assignments carrying a few hundred policy definitions. This is why initiative-based assignment is non-negotiable: as loose policies it would breach the 200-assignment limit before you added a single custom rule.

Assigning policy with real tooling

The corp NIC guardrail as a raw CLI assignment:

CORP_ID=$(az account management-group show --name contoso-corp --query id -o tsv)

az policy assignment create \
  --name "Deny-Public-IP-On-NIC" \
  --display-name "Corp: network interfaces must not have public IPs" \
  --scope "$CORP_ID" \
  --policy "83a86a26-fd1f-447c-b59d-e51f44264114" \
  --enforcement-mode Default

The same assignment expressed properly — Bicep at management-group scope, with a non-compliance message so the deny teaches its own fix:

targetScope = 'managementGroup'

resource denyNicPip 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
  name: 'Deny-Public-IP-On-NIC'
  properties: {
    displayName: 'Corp: network interfaces must not have public IPs'
    policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/83a86a26-fd1f-447c-b59d-e51f44264114'
    enforcementMode: 'Default'
    nonComplianceMessages: [
      {
        message: 'Corp workloads are private-by-default. Public ingress belongs in the online archetype, or via the hub. See the platform wiki: /docs/archetypes.'
      }
    ]
  }
}
// az deployment mg create --management-group-id contoso-corp \
//   --location centralindia --template-file corpDeny.bicep

And in Terraform with azurerm:

resource "azurerm_management_group_policy_assignment" "deny_nic_pip" {
  name                 = "Deny-Public-IP-On-NIC"
  display_name         = "Corp: network interfaces must not have public IPs"
  management_group_id  = azurerm_management_group.corp.id
  policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/83a86a26-fd1f-447c-b59d-e51f44264114"
  enforce              = true

  non_compliance_message {
    content = "Corp workloads are private-by-default. Use the online archetype for public ingress."
  }
}

Policy effects in anger: DINE, Modify, Deny, DenyAction and enforcement staging

Every effect in the engine, and where it belongs in a landing zone:

Effect Evaluation moment What it does Landing-zone role Gotcha
Deny Request time (before ARM writes) Rejects the request with RequestDisallowedByPolicy Must-never-exist rules (public IP in corp, classic resources) Blocks pipelines mid-deploy; always pair with a non-compliance message
DenyAction Request time, on DELETE Blocks deletion of matched resources Protecting diagnostic settings, AMA identity, hub plumbing Only delete action supported; not a resource lock replacement
Audit Post-evaluation Marks non-compliant, changes nothing Baselines (MCSB), brownfield staging, sandbox Compliance noise if never acted on
AuditIfNotExists After deployment, checks related resource Non-compliant if the companion resource is missing “VM has no AMA extension” style checks Existence check runs against related resources — scope carefully
DeployIfNotExists ~10–15 min after create/update Deploys an ARM template when the companion is missing Diagnostics, backup, agents, DNS wiring — the self-healing layer Needs a managed identity with roles; existing resources need remediation tasks
Modify Request time (mutates request) Adds/replaces properties or tags in flight Tag stamping, enabling flags (DDoS association) Alias must support modify; conflicts surface as deployment errors
Append Request time Adds fields to the request (legacy Modify) Older NSG/storage rules Prefer Modify; Append can’t remediate
Manual Human attestation Compliance state set by attestation workflow Regulatory controls that automation can’t verify Someone must own the attestation queue
Disabled Never Definition present, effect off Parameterised effects (effect: Disabled per-scope) Useful with Allowed values effect parameters

DINE mechanics: the part everyone gets wrong

A DeployIfNotExists assignment is a small deployment machine, and it fails silently when mis-plumbed. The moving parts:

Step What happens What you must provide Failure mode if missing
1. Assignment Policy assigned with an identity --mi-system-assigned (or user-assigned) + --location Assignment succeeds; nothing ever deploys
2. Authorization Identity needs the definition’s roleDefinitionIds at scope Role assignment(s) at the assignment scope Remediation deployments fail with AuthorizationFailed
3. Trigger New/updated resources evaluated ~10–15 min later Nothing — automatic
4. Existing estate Old resources are not touched automatically A remediation task per assignment Compliance shows non-compliant forever
5. Remediation ARM deployment runs as the identity Optionally --resource-discovery-mode ReEvaluateCompliance Stale compliance data remediates the wrong set

The full, correct sequence for the central-diagnostics DINE at landingzones scope:

LZ_ID=$(az account management-group show --name contoso-landingzones --query id -o tsv)
LAW_ID="/subscriptions/<mgmt-sub>/resourceGroups/rg-logging/providers/Microsoft.OperationalInsights/workspaces/law-platform-prod"

# 1. Assign with a system-assigned managed identity (location is mandatory with an identity)
az policy assignment create \
  --name "Deploy-AzActivity-Log" \
  --display-name "Landing zones: route activity logs to central Log Analytics" \
  --scope "$LZ_ID" \
  --policy "$(az policy definition list --query "[?displayName=='Configure Azure Activity logs to stream to specified Log Analytics workspace'].name | [0]" -o tsv)" \
  --params "{ \"logAnalytics\": { \"value\": \"$LAW_ID\" } }" \
  --mi-system-assigned \
  --location centralindia

# 2. Grant the identity its roles at the assignment scope
#    (the CLI does NOT auto-create these; the portal does)
az policy assignment identity assign \
  --name "Deploy-AzActivity-Log" \
  --scope "$LZ_ID" \
  --system-assigned \
  --role "Log Analytics Contributor" \
  --identity-scope "$LZ_ID"

az policy assignment identity assign \
  --name "Deploy-AzActivity-Log" \
  --scope "$LZ_ID" \
  --system-assigned \
  --role "Monitoring Contributor" \
  --identity-scope "$LZ_ID"

# 3. Remediate the existing estate (new resources self-heal; old ones do not)
az policy remediation create \
  --name "rem-activity-log-initial" \
  --management-group contoso-landingzones \
  --policy-assignment "Deploy-AzActivity-Log" \
  --resource-discovery-mode ReEvaluateCompliance

Grant exactly the roles listed in the policy definition’s details.roleDefinitionIds — no more. A DINE identity with Owner at landingzones is a privilege-escalation machine hiding in plain sight (anyone who can edit the initiative’s template content can make that identity deploy arbitrary role assignments). Remediation-task behaviour at scale is its own discipline — Azure Policy remediation tasks with managed identities covers the queueing, throttling and retry semantics.

Enforcement modes and exemptions: the staging levers

Two levers make policy operable over a live estate rather than a demolition tool:

Lever Values What it changes When to use
Enforcement mode Default / DoNotEnforce DoNotEnforce evaluates and reports compliance but denies nothing and auto-deploys nothing (manual remediation still allowed) Brownfield staging; testing new assignments against real traffic
Exemption — Waiver Scoped object, optional expiry “We accept this risk here” Approved exceptions (an online-style app stuck in corp pending re-platform)
Exemption — Mitigated Scoped object, optional expiry “The control is met another way” NVA provides the control instead of the native feature
notScopes / excluded scopes Assignment property Silently removes scopes from evaluation Almost never — invisible in compliance data; prefer exemptions

Exemptions are first-class auditable objects with categories and expiry dates — the governed alternative to carving holes in assignments (the full comparison lives in exemptions vs exclusions):

# Exempt one resource group from the corp NIC deny, with an expiry and a reason
az policy exemption create \
  --name "exempt-legacy-vdi-pip" \
  --display-name "Legacy VDI: public IP until AVD migration (change CHG0042871)" \
  --exemption-category Waiver \
  --policy-assignment "/providers/Microsoft.Management/managementGroups/contoso-corp/providers/Microsoft.Authorization/policyAssignments/Deny-Public-IP-On-NIC" \
  --scope "/subscriptions/<sub-id>/resourceGroups/rg-legacy-vdi" \
  --expires-on "2026-12-31T23:59:59Z"

Expiring exemptions are the platform team’s forcing function: when the waiver lapses, the deny re-engages automatically. An exemption without an expiry date is just technical debt with a signature.

The RBAC model: personas, custom roles and PIM

Policy answers “what may exist”; RBAC answers “who may act”. The two are deliberately separated in this design — you govern resources with policy at MG scope and grant people the narrowest workable role at the lowest workable scope. The persona model:

Persona Role(s) Scope Standing or PIM-eligible? Notes
Platform owner Owner (or custom equivalent) Intermediate root PIM-eligible only The nuclear key; 2–4 humans, approval + MFA to activate
Platform engineers Contributor platform PIM-eligible Day-to-day platform work via pipelines, not portal
NetOps Network Contributor (+ custom platform-wide read) connectivity PIM for write, standing read Owns hubs, gateways, firewall, DNS
SecOps Security Admin + Security Reader Intermediate root Standing read, PIM for admin Needs estate-wide visibility by definition
Identity ops Contributor identity PIM-eligible DCs and identity plumbing only
Landing-zone (workload) team Contributor or custom “subscription owner” Their subscription(s) only Standing (that’s the democratization) Never at any MG scope
Vending pipeline SPN Owner Target subscription (during vend) + Microsoft.Management/managementGroups/subscriptions/write on archetype MGs Standing, workload-identity federated The only identity that routinely writes MG membership
Policy DINE identities Exactly the definition’s roleDefinitionIds Assignment scope Standing (system-managed) Audit these — they are your largest silent grant surface
Break-glass accounts Owner Tenant root Standing, monitored Excluded from CA policies; alert on any use

Three rules carry most of the security value. First, workload teams get subscription scope, never management-group scope. The entire point of democratization is that a team can be Owner-equivalent inside their blast-radius boundary without any ability to affect the platform. Second, humans get groups, groups get roles. With 500 role assignments per MG and 4,000 per subscription as hard caps, per-user assignment is both a limit risk and an audit nightmare. Third, anything above subscription scope is PIM-eligible, not standing. Activation with MFA, justification, approval for the root scopes, max 8-hour activation, quarterly access reviews — the full treatment is in Entra PIM for roles.

The ALZ reference ships sample custom role definitions for the persona gaps that built-ins miss:

ALZ custom role Built on Key NotActions / restrictions Intended persona
Subscription owner (landing-zone edition) Owner Cannot write route tables, vWAN/VPN/ER resources, or peerings — network plumbing stays with NetOps Workload team “owner” inside their subscription
Network management (NetOps) Custom Platform-wide network read/write, nothing else Central network team
Security operations (SecOps) Custom Estate-wide read + security actions, */read, Defender/Sentinel write Security team
Application owners (DevOps/AppOps) Contributor-ish No RBAC write, no network write App teams in shared subscriptions

A representative custom role — a subscription owner who cannot re-plumb networking (the mechanics of Actions/NotActions are covered in Azure RBAC custom roles):

{
  "Name": "Landing Zone Subscription Owner",
  "Description": "Owner within a landing zone subscription, minus platform networking and RBAC-sprawl actions.",
  "Actions": ["*"],
  "NotActions": [
    "Microsoft.Network/vpnGateways/*",
    "Microsoft.Network/expressRouteCircuits/*",
    "Microsoft.Network/routeTables/write",
    "Microsoft.Network/routeTables/delete",
    "Microsoft.Network/virtualNetworks/peer/action",
    "Microsoft.Network/vpnSites/*"
  ],
  "AssignableScopes": [
    "/providers/Microsoft.Management/managementGroups/contoso-landingzones"
  ]
}
az role definition create --role-definition lz-subscription-owner.json

# Assign to the workload team's group at THEIR subscription only
az role assignment create \
  --assignee-object-id "<entra-group-object-id>" \
  --assignee-principal-type Group \
  --role "Landing Zone Subscription Owner" \
  --scope "/subscriptions/<workload-sub-id>"

Setting AssignableScopes to contoso-landingzones is a quiet but important control: the role physically cannot be assigned inside the platform branch, so a fat-fingered grant cannot give a workload team keys to the hub.

Subscription vending: democratization as a pipeline

Subscription democratization is a promise (“you get your own subscription, quickly”), and vending is the machinery that keeps it. A vend is not just creation — it is creation, placement, and baseline, as one idempotent pipeline run:

Stage What happens Tooling Typical duration
1. Request Team submits workload name, archetype, environment, cost centre, owner group ITSM form / Git PR — see ServiceNow-gated self-service landing zones Minutes (the approval is the slow part)
2. Create Subscription alias API creates the subscription against a billing scope az account alias create / lz-vending module 1–5 min
3. Place Subscription moved into the archetype MG — governance attaches here az account management-group subscription add / module Seconds
4. Baseline Tags, budgets, RP registration, spoke VNet + hub peering, RBAC to the owner group, workload identity for CI/CD lz-vending module 5–15 min
5. Handover Team receives subscription ID, VNet CIDR, pipeline identity — and nothing else to do Automation output

The create step needs a billing scope, and its format depends on your agreement type — this is where vending projects stall for weeks if nobody located the billing owner early:

Agreement Billing scope format Who can create subscriptions Practical note
EA /providers/Microsoft.Billing/billingAccounts/{enrollment}/enrollmentAccounts/{account} Enrollment Account Owner (grantable to an SPN) The classic enterprise path; dev/test offer via --workload DevTest
MCA .../billingAccounts/{account}/billingProfiles/{profile}/invoiceSections/{section} “Azure subscription creator” on the invoice section Grant the vending SPN the role on one dedicated invoice section
MPA (CSP) .../billingAccounts/{account}/customers/{customer} Partner Admin Agents You often can’t self-serve; the partner runs the create step

Raw CLI for a one-off vend:

# Create the subscription against an EA enrollment account
az account alias create \
  --name "sub-corp-payments-prod-01" \
  --display-name "Payments Production" \
  --billing-scope "/providers/Microsoft.Billing/billingAccounts/8611537/enrollmentAccounts/301852" \
  --workload "Production"

# Place it — this is the moment governance attaches
az account management-group subscription add \
  --name contoso-corp \
  --subscription "sub-corp-payments-prod-01"

The mover needs Microsoft.Management/managementGroups/subscriptions/write on the target MG plus owner-level rights on the subscription — scope the vending SPN to exactly the archetype MGs (corp, online, sandbox) so it can never place anything into platform.

In production you run this through the Azure/lz-vending/azurerm Terraform module, which wraps create + place + baseline as one declarative unit per landing zone:

module "lz_payments_prod" {
  source  = "Azure/lz-vending/azurerm"
  version = "~> 4.1"

  location = "centralindia"

  # Stage 2: create
  subscription_alias_enabled = true
  subscription_alias_name    = "sub-corp-payments-prod-01"
  subscription_display_name  = "Payments Production"
  subscription_billing_scope = "/providers/Microsoft.Billing/billingAccounts/8611537/enrollmentAccounts/301852"
  subscription_workload      = "Production"

  # Stage 3: place
  subscription_management_group_association_enabled = true
  subscription_management_group_id                  = "contoso-corp"

  # Stage 4: baseline
  subscription_tags = {
    workload   = "payments"
    env        = "prod"
    costCenter = "CC-4410"
    owner      = "payments-platform@contoso.com"
    archetype  = "corp"
  }

  virtual_network_enabled = true
  virtual_networks = {
    spoke = {
      name                            = "vnet-payments-prod-ci-01"
      address_space                   = ["10.60.24.0/24"]
      resource_group_name             = "rg-payments-network"
      hub_peering_enabled             = true
      hub_network_resource_id         = local.hub_vnet_id
      hub_peering_use_remote_gateways = true
    }
  }

  role_assignments = {
    team_owner = {
      principal_id   = var.payments_team_group_id
      definition     = "Landing Zone Subscription Owner"
      relative_scope = ""            # subscription root
    }
  }

  # CI/CD workload identity, federated for GitHub OIDC — no secrets to rotate
  umi_enabled             = true
  umi_name                = "umi-payments-deploy"
  umi_resource_group_name = "rg-payments-identity"
  umi_federated_credentials_github = {
    main = {
      organization = "contoso"
      repository   = "payments"
      value        = "environment:production"
    }
  }
}

What the module covers versus what stays yours:

Capability In lz-vending? Note
Subscription creation (alias) / adopt existing Yes Works with EA/MCA/MPA scopes, or subscription_id for existing subs
MG placement Yes Idempotent association
Tags, resource provider registration Yes RP list is configurable; defaults cover the usual suspects
Spoke VNet + peering (hub-spoke and vWAN) Yes hub_peering_enabled or vWAN connection per network
Role assignments Yes To groups/SPNs at subscription or relative scope
User-assigned MI + OIDC federation Yes GitHub/generic federated credentials for pipelines
Budgets Yes (recent v4 releases) Pair with action groups you own
CIDR allocation (IPAM) No Bring your own — a simple allocation table beats collisions at sub #80
Naming standards, archetype menu, approvals No That is your process wrapped around the module

One design note on CIDRs: vending is where address-space discipline lives or dies. Allocate spokes from a plan (e.g., corp prod gets 10.60.0.0/16 carved into /24s, one per landing zone) and record allocations in the vending data itself — the Terraform state is your IPAM if every spoke goes through it.

Platform subscriptions: identity, management and the connectivity choice

The identity subscription: yes, you still run domain controllers

Most enterprises still carry AD DS through their cloud decade, and those domain controllers belong in a dedicated identity subscription under platform/identity — not scattered through workload subscriptions, and not in the hub. A typical layout per primary region:

Component Sizing / choice Rationale
Domain controllers Standard_D2s_v5 (or D4s_v5 for >5k users), zone-separated AD is latency-tolerant; small SKUs suffice. Two zones, two DCs minimum
Identity spoke VNet Small dedicated /24, peered to hub DCs never live in the hub; peering carries LDAP/Kerberos/DNS
Disks Premium SSD; no host caching on the NTDS/SYSVOL data disk AD database consistency requirement
Backup Azure Backup with the Deploy-VM-Backup DINE + system-state awareness Restoring AD is a directory operation, not just a disk restore
DNS integration DCs as custom DNS on the identity spoke, or forwarders behind Private DNS Resolver Hybrid name resolution; resolver keeps you PaaS-first
Alternative Entra Domain Services (managed AAD DS) Only when no schema extensions/GPO depth needed; it is not a DC replacement for legacy trust topologies
Guardrails Identity MG deny set (no public IPs, no open mgmt ports, NSGs mandatory) Compromised DC = compromised estate; the policy layer enforces the perimeter

Access to DCs goes through the hub Bastion with PIM-activated identity-ops roles — no direct RDP path exists, and the Deny-MgmtPorts-Internet assignment makes creating one impossible rather than merely forbidden.

The management subscription: one workspace to rule them all

The management subscription hosts the observability estate the DINE layer feeds:

Component Decision Default answer
Log Analytics workspace One central workspace vs per-team One central (law-platform-prod); split only for hard data-boundary or table-plan reasons
Retention Interactive retention vs archive tiers 90 days interactive for platform tables; archive to 1–2 years for audit tables
Agents AMA + data collection rules The 2024+ ALZ library deploys AMA via DINE; the legacy MMA/automation-account pattern is retired
Sentinel Same workspace vs dedicated Same workspace is simplest; dedicate one if SecOps RBAC must be hard-separated
Alerting Azure Monitor Baseline Alerts (AMBA) A policy-driven alert pack assigned at MG scopes — alerts deploy with the estate, not after it
Access Resource-context RBAC Workload teams query their own resources’ logs in the central workspace without workspace-level rights

The central workspace resource ID is the single most-referenced parameter in the whole policy baseline — Deploy-AzActivity-Log, Deploy-Resource-Diag, and Deploy-MDFC-Config all take it. Deploy management first, capture the ID as an output, feed it to the policy assignment layer. Every implementation option below encodes that ordering.

The connectivity subscription: hub-spoke or Virtual WAN

The connectivity subscription carries the estate’s network core, and the structural decision inside it — traditional hub-spoke or Azure Virtual WAN — deserves its own article (hub-spoke vs Virtual WAN enterprise topology and the connectivity subscription deep dive). The decision matrix as it affects the landing zone:

Dimension Hub-spoke (self-managed) Virtual WAN (Microsoft-managed)
Hub Your VNet; you place firewall, gateways, Bastion, DNS Managed virtual hub; you attach services to it
Routing UDRs you author and maintain; transitive spoke-spoke via firewall Managed route tables + routing intent; any-to-any by default
Multi-region You build hub-per-region + global peering + UDR mesh Hubs interconnect automatically over the Microsoft backbone
Spoke scale Bounded by 500 peerings/VNet and your UDR discipline Designed for large fan-out; hub router scales via routing infrastructure units (~2–50 RIUs, ≈3–50 Gbps)
NVA support Anything — full control Curated NVA/SaaS integrations in-hub; less freedom
Branch connectivity (S2S/P2S/ER at scale) Manual per-gateway The core vWAN use case; thousands of branches
DNS Private DNS zones + resolver in hub — full control Private DNS still lives in a spoke/services VNet you manage
Cost profile Gateways + firewall + peering; no hub premium Hub hourly + data processing on top of gateways/firewall
IaC maturity Fully mature everywhere Mature (AVM avm-ptn-virtualwan); some features portal-first at release
Choose when <3 regions, heavy NVA/DNS customisation, cost-sensitive Many regions/branches, SD-WAN integration, small network team

Two facts anchor the choice. The management-group design is identical either wayconnectivity MG, connectivity subscription(s), same policy assignments; only the resources inside differ, so you are not locked at the hierarchy level. And vending abstracts the difference: lz-vending peers a spoke to a hub VNet or connects it to a vWAN hub with a parameter change, so workload teams never know which topology they are on. India-specific note: a Central India + South India pair with hub-spoke and zone-redundant firewalls remains the common pattern; vWAN earns its premium when branch counts climb past a few dozen sites.

Deploying it: accelerator, ALZ-Bicep and Terraform — and what to customize vs keep

Never hand-build this in the portal beyond a lab. Three supported implementation vehicles:

Vehicle What it is Deploys Best for Watch out
ALZ Portal Accelerator Microsoft’s guided portal deployment of the full conceptual architecture MG tree, policies, logging, hub or vWAN connectivity, identity Proof-of-value in days; small teams without IaC muscle Output is not source-controlled; day-2 changes drift unless you adopt IaC after
ALZ-Bicep + Accelerator bootstrap Modular Bicep (managementGroups, customPolicyDefinitions, alzDefaultPolicyAssignments, logging, hubNetworking, vwanConnectivity, subscriptionPlacement, orchestration modules) with a bootstrap that scaffolds repo + pipelines Everything, as pipeline-driven Bicep Bicep-first shops; tight ARM alignment You own module-version upgrades; read release notes each bump
Terraform (AVM pattern modules) Azure/avm-ptn-alz (MGs + policy from the versioned ALZ library, via the alz provider) + avm-ptn-alz-management + avm-ptn-hubnetworking / avm-ptn-virtualwan + lz-vending, bootstrapped by the ALZ Terraform Accelerator Everything, as state-managed Terraform Terraform shops; multi-cloud platform teams State for the platform is crown jewels; protect the backend
Legacy: Azure/caf-enterprise-scale The original Terraform ES module (archetype lib/ overlay pattern) MGs + policy + management + connectivity Existing deployments In maintenance; new builds should start on avm-ptn-alz

The Terraform current-generation core, with the customization mechanism visible — the library overlay:

terraform {
  required_providers {
    azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" }
    alz     = { source = "Azure/alz", version = "~> 4.0" }
  }
}

provider "alz" {
  # The ALZ library is versioned content (definitions, assignments, archetypes).
  # Pin the upstream release, then merge YOUR deltas on top — never fork upstream.
  library_references = [
    { path = "platform/alz", ref = "2025.02.0" },  # upstream, pinned
    { custom_url = "${path.root}/lib" }             # your overlay: deltas only
  ]
}

data "azurerm_client_config" "core" {}

module "alz" {
  source  = "Azure/avm-ptn-alz/azurerm"
  version = "~> 0.12"

  architecture_name  = "contoso"   # your archetype map from lib/, or "alz" stock
  parent_resource_id = data.azurerm_client_config.core.tenant_id
  location           = "centralindia"

  # Feed platform outputs into policy parameters estate-wide
  policy_default_values = {
    log_analytics_workspace_id = jsonencode({ value = module.management.log_analytics_workspace.id })
    ddos_protection_plan_id    = jsonencode({ value = module.hub.ddos_protection_plan.id })
  }
}

Your lib/ overlay holds only deltas: an architectures/contoso.alz_architecture_definition.json describing your tree, an extra initiative, a tightened parameter, an added archetype (confidential-corp). When Microsoft ships a new library release you bump ref, run terraform plan, and read the diff — upstream improvements arrive as a reviewable change-set while your customisations persist. This diff-and-merge discipline is the single biggest operational difference between platforms that stay current and platforms stranded on a 2023 snapshot. (If policy lifecycle at real scale becomes its own programme, EPAC — Enterprise Policy as Code — is the dedicated alternative for the policy layer, consuming the same ALZ library content.)

What to customize versus keep, explicitly:

Item Keep / customize Guidance
Tree shape (platform + landingzones + sandbox + decommissioned) Keep Deviating breaks the assumption every ALZ tool and doc makes
MG IDs / prefix Customize once, day one IDs are immutable; pick contoso-* and never look back
Corp/online archetype split Keep Add siblings (confidential-*) rather than mutating these
Default policy assignments Keep, tune parameters Locations, retention, security contacts, workspace IDs are meant to be set
Enforcement of specific denies Customize deliberately E.g., relax Deny-Public-IP-On-NIC to audit during migration waves — then re-tighten
DDoS Network Protection Customize (cost) ~US$2,944/month; small estates often waive it with a documented risk acceptance
The ALZ library content Never fork Overlay only; forks strand you on a snapshot
Policy at Tenant Root Group Never add Canary and quarantine depend on the root staying clean
Extra hierarchy depth (BU/env/region MGs) Never add The anti-pattern section explains the failure modes
Custom archetypes under landingzones Add when a distinct policy contract exists Reviewed platform change, not a team request
Sandbox guardrails (budget caps, SKU limits) Customize Stock sandbox is looser than most orgs want
Blueprints-based governance Do not adopt Azure Blueprints retires July 11, 2026 — this month; deployment stacks + template specs + policy replace it

Brownfield adoption: moving a live estate into the tree

Greenfield is easy. The real job — the one consultancies bill seven figures for — is brownfield: moving existing, live subscriptions full of running workloads into the hierarchy without breaking them. The failure mode is obvious and common: someone moves a busy subscription under corp, the deny set applies instantly to new requests, and the next pipeline run fails at 2 a.m. on a resource it has deployed successfully two hundred times.

The mechanics you must internalise first — exactly what changes at the moment a subscription moves:

Dimension What happens on move Latency Operational implication
Policy assignments Source-tree assignments stop applying; destination-tree assignments start New requests: near-immediate (allow up to ~30 min) Denies can start rejecting pipeline deployments the same hour
Compliance data Re-evaluated against the new assignment set Next evaluation cycle (≤24 h) or az policy state trigger-scan Don’t judge the move until a scan completes
Inherited RBAC Roles inherited from the source MG chain vanish; destination’s apply Immediate Users can silently lose access on move — inventory inherited roles first
Direct RBAC Role assignments at subscription scope and below Unchanged Your workload team’s access survives
Resources, tags, billing Nothing moves, nothing re-deploys, invoice unchanged An MG move is a control-plane re-parenting, not a migration
Deny assignments / locks Unchanged (they live on resources/scopes below)
Tenant Must be the same Entra tenant Cross-tenant estates are a different (much harder) programme

With that understood, run the migration as a staged pipeline. The proven sequence:

Phase Action Exit criteria
0. Deploy the platform Full tree + policy in place, but archetype denies at DoNotEnforce; platform subscriptions live Hierarchy verified; central logging receiving
1. Inventory Enumerate every subscription, its current MG, inherited RBAC, and public-facing resources A spreadsheet nobody disputes
2. Classify Assign each subscription a target archetype (corp/online/sandbox/decommissioned) Named owner per subscription confirms
3. Soft-land Move subscriptions in waves into target MGs while denies are DoNotEnforce Wave moved; nothing broke (nothing can — no enforcement yet)
4. Measure Trigger compliance scans; build the violation backlog per subscription Compliance report stable for ~1 week
5. Remediate Fix violations, run DINE remediation tasks, grant expiring exemptions for accepted cases Backlog at zero-or-exempted
6. Enforce Flip assignments to Default, one assignment at a time, deny-riskiest last One week of clean pipeline runs per flip
7. Steady state New subscriptions only via vending; hierarchy settings lock the tree Vending live; default-MG setting active

The commands that carry phases 1, 4 and 6:

# Phase 1 — every subscription and where it currently sits
az account management-group entities list \
  --query "[?type=='/subscriptions'].{name:displayName, id:name, parent:parent.id}" -o table

# Phase 4 — force evaluation, then read the violation backlog per assignment
az policy state trigger-scan --subscription "<sub-id>" --no-wait
az policy state summarize --management-group contoso-corp \
  --query "policyAssignments[].{assignment:policyAssignmentId, nonCompliant:results.nonCompliantResources}" -o table

# Phase 6 — flip one assignment to enforcing (repeat per assignment, riskiest last)
az policy assignment update \
  --name "Deny-Public-IP-On-NIC" \
  --scope "$(az account management-group show --name contoso-corp --query id -o tsv)" \
  --enforcement-mode Default

Two refinements separate smooth migrations from painful ones. First, the canary hierarchy: ALZ guidance is to run a parallel mini-tree (contoso-canary with the same children) holding one or two disposable subscriptions. Every policy change — new assignment, parameter change, library version bump — deploys to canary first, where a broken deny costs nothing. This is the answer to “how do you test policy?”, because there is no staging tenant for governance. Second, classification is where the value hides: in every brownfield I have run, the audit phase reveals that 15–25% of subscriptions were misclassified by their owners — “internal” apps with public endpoints that actually belong in online, or abandoned subscriptions that belong straight in decommissioned. Let the compliance data, not the interviews, make the final call.

Anti-patterns: the hierarchies that fail

Every one of these is real, encountered in the field, and expensive to unwind. The test for all of them is the same question: does this management group exist because of a distinct policy or RBAC contract? If not, it should not exist.

# Anti-pattern Why it feels right Why it fails Do instead
1 Org-chart tree (MG per BU/department) Mirrors how the company thinks Reorgs outpace security baselines; policy differences don’t follow reporting lines; every restructure re-parents live subscriptions Archetypes; tags for BU attribution
2 Environment MGs (dev/test/prod branches) “Prod needs stricter policy” Doubles/triples the tree; most policy is identical across envs; env is a subscription property Env-per-subscription in the same archetype; parameterise the few env-differences
3 Region MGs “EU data must stay in EU” Region is a resource property, not a governance contract Allowed-locations parameters per assignment
4 Six-level depth Maximum granularity Nobody can compute effective policy; assignment sprawl; hits the depth cap 3–4 levels; parameterise instead of subdividing
5 Policy at Tenant Root Group “Applies to everything, forever” Kills canary hierarchies, quarantine, and future branches; a bad deny at root can lock the whole tenant including your fix path Intermediate root carries the non-negotiables
6 Per-team archetype negotiation “Our workload is special” Fifty bespoke policy sets = no baseline at all; audit answers become per-team Small fixed menu; new archetypes only as reviewed platform changes
7 Forking the ALZ library “We just need one small edit” Stranded on a snapshot; upstream fixes and new policies never arrive Overlay (lib/ deltas) + pinned upstream ref
8 MGs as cost-reporting structure Finance asks for it Cost analysis pivots on tags and scopes fine without MGs; you warp governance to fit invoices Tag strategy for cost allocation
9 The mega-subscription “Easier to manage one” No blast-radius isolation; quota contention; RBAC knife-fights; policy can’t differentiate Democratization — subscription per workload per env

Anti-pattern 5 deserves its war story. A platform team assigned an allowed-locations deny at the Tenant Root Group, forgot the Microsoft.Resources/resourceGroups exclusion nuance they had at lower scopes, and discovered that their own recovery automation — which created a resource group in a paired region during a DR test — was blocked everywhere, including the subscription they would have used to fix it. The intermediate root exists precisely so that “everything” has an exit hatch above it.

Architecture at a glance

Read the diagram top-to-bottom, then left-to-right. The top lane is the containment chain: Tenant Root Group (kept empty of day-to-day policy) → the single contoso intermediate root → the four structural children. The platform branch fans into identity, management and connectivity, each holding exactly one specialised subscription: domain controllers and identity plumbing; the central Log Analytics workspace and monitoring stack; the hub network with firewall, gateways and private DNS. The landingzones branch fans into the corp and online archetypes where workload subscriptions actually land, while sandbox and decommissioned sit at the edges as lifecycle states — loose-but-isolated experimentation on one side, deny-everything wind-down on the other.

The right-hand lane is the part to internalise: policy inheritance as a layered union. A workload subscription placed under corp is evaluated against the intermediate root’s org-wide assignments (security benchmark, Defender config, central activity logging, classic-resource denies), plus the landingzones workload baseline (NSG/TLS/backup/AKS guardrails), plus corp’s private-by-default set (deny public endpoints and public-IP NICs, private-DNS DINE wiring) — every layer additive, nothing overridable, exemptions as the only sanctioned carve-out. Position in the tree is the governance contract.

Enterprise-scale landing zone management group hierarchy: Tenant Root Group at top with a single contoso intermediate root beneath it, branching into platform (with identity, management and connectivity children, each holding its platform subscription) and landingzones (with corp and online archetype children holding workload subscriptions), plus sandbox and decommissioned edge nodes; a layered policy-inheritance lane on the right shows a corp subscription governed by the union of assignments inherited from contoso, landingzones and corp scopes.

Real-world scenario

A financial-services platform team inherited 140 subscriptions accumulated over five years — every one parked directly under the Tenant Root Group, governed by nothing but ad-hoc per-subscription policy assignments (they counted 396 of them, no two alike). The trigger was an RBI-aligned audit finding: no demonstrable uniform control plane, and no way to prove that internet-facing workloads were segregated from core-banking-adjacent ones. Constraints: zero workload downtime, a six-month regulator deadline, and a platform team of five.

They deployed the platform first — the contoso tree via the Terraform AVM modules, management subscription with a central workspace (their 140 subscriptions turned out to emit ~38 GB/day of activity and resource logs once the DINEs ran), and a Central India hub with zone-redundant firewall. All archetype denies went in at DoNotEnforce. Then the waves: subscriptions moved in batches of ten per week into their classified archetypes, each batch left in audit for a full week while az policy state summarize built the remediation backlog.

The classification data earned its keep immediately. Owners had self-declared 12 subscriptions as internet-facing; the compliance reports (public-IP-on-NIC and public-endpoint violations against the corp deny set, evaluated in report-only mode) found 31. Nineteen “internal” applications had quietly grown public endpoints over the years — three of them with management ports open to the internet, which became the security team’s problem that same afternoon and validated the entire programme to the CISO. Those 31 were re-pointed to online; 9 abandoned subscriptions went straight to decommissioned, whose deny-deployments initiative promptly caught a forgotten cron-driven pipeline still deploying nightly builds into one of them.

Remediation ran eight weeks: 4,100 non-compliant resources at peak, burned down through DINE remediation tasks (diagnostics and backup enrolment fixed themselves — about 70% of the backlog), code fixes for the rest, and 23 expiring waivers with change-ticket references. Enforcement flipped one assignment at a time over three weeks, Deny-Public-IP-On-NIC last. Total: 22 weeks, zero workload downtime, and the audit closed on a single artefact — the hierarchy itself, exported with az account management-group show --expand --recursive, because “where does this subscription sit?” had become a complete answer to “how is it governed?”.

Advantages and disadvantages

Advantages Disadvantages
Governance scales with zero marginal effort — subscription #400 costs what #4 cost Meaningful up-front design and platform investment before the first workload lands
Provable, queryable compliance (one compliance API over one tree) Policy layering has a learning curve; effective-policy reasoning is non-trivial
Blast-radius isolation by default (subscription = boundary) Platform team becomes a hard dependency — under-staff it and vending queues form
Workload teams get real autonomy inside guardrails DINE magic can mask broken team practices (“the platform will fix my diagnostics”)
Brownfield estates get a deterministic adoption path A bad deny at high scope is a high-blast-radius outage — testing discipline required
Upstream ALZ library keeps improving underneath you Library upgrades are a recurring operational duty (review, canary, roll)
Tooling ecosystem (accelerators, AVM, vending, AMBA) is mature Terraform/Bicep state and pipelines for the platform are themselves crown-jewel assets to secure

The honest summary: this architecture trades up-front structure for marginal-cost-zero governance. Below ~10 subscriptions the ceremony can outweigh the benefit — a single well-policied subscription with rigorous tagging is defensible. Past ~20 subscriptions the trade inverts hard, and past 50 the absence of this structure is itself the biggest risk on the register.

Hands-on lab

Build a working miniature of the whole model — tree, layered policy, DINE identity, a governed subscription, a bitten deny — in a test tenant. Management groups and policy are free; the only step that could cost money is denied by design. You need Global Administrator (for one elevation) and an Owner-level subscription you can safely re-parent.

Step 1 — Elevate and verify root access (one-time bootstrap).

az rest --method post --url "/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"
az role assignment list --scope "/" --query "[?principalName=='$(az account show --query user.name -o tsv)'].roleDefinitionName" -o tsv

Expected output: User Access Administrator. (Remove this at teardown — it is a standing finding otherwise.)

Step 2 — Create the tree. Nine idempotent calls:

az account management-group create --name lab --display-name "Lab Root"
for mg in platform landingzones sandbox decommissioned; do
  az account management-group create --name "lab-$mg" --display-name "Lab ${mg}" --parent lab
done
for mg in identity management connectivity; do
  az account management-group create --name "lab-$mg" --display-name "Lab ${mg}" --parent lab-platform
done
for mg in corp online; do
  az account management-group create --name "lab-$mg" --display-name "Lab ${mg}" --parent lab-landingzones
done

Validate the shape:

az account management-group show --name lab --expand --recursive \
  --query "{root:displayName, children:children[].{name:displayName, children:children[].displayName}}" -o json

Expected: Lab platform (with three children), Lab landingzones (with two), Lab sandbox, Lab decommissioned.

Step 3 — Layer the policy. MCSB audit at the lab root, allowed-locations at the lab root, the NIC deny at corp:

LAB_ID=$(az account management-group show --name lab --query id -o tsv)
CORP_ID=$(az account management-group show --name lab-corp --query id -o tsv)

# Org-wide: Microsoft cloud security benchmark (audit-only initiative)
az policy assignment create --name "mcsb-baseline" \
  --display-name "Lab: MCSB baseline" --scope "$LAB_ID" \
  --policy-set-definition "1f3afdf9-d0c9-4c3d-847f-89da613e70a8"

# Org-wide: allowed locations (deny)
az policy assignment create --name "allowed-locations" \
  --display-name "Lab: allowed locations" --scope "$LAB_ID" \
  --policy "e56962a6-4747-49cd-b67b-bf8b01975c4c" \
  --params '{ "listOfAllowedLocations": { "value": ["centralindia", "southindia"] } }'

# Corp-only: NICs must not have public IPs (deny)
az policy assignment create --name "deny-nic-pip" \
  --display-name "Lab corp: no public IPs on NICs" --scope "$CORP_ID" \
  --policy "83a86a26-fd1f-447c-b59d-e51f44264114" \
  --enforcement-mode Default

Step 4 — Place a subscription and watch governance attach.

SUB_ID=$(az account show --query id -o tsv)
ORIG_MG=$(az account management-group entities list \
  --query "[?name=='$SUB_ID'].parent.id | [0]" -o tsv)   # note this for teardown
az account management-group subscription add --name lab-corp --subscription "$SUB_ID"

# The subscription now sees assignments from lab AND lab-corp (union via inheritance)
az policy assignment list --scope "/subscriptions/$SUB_ID" --disable-scope-strict-match \
  --query "[].{name:displayName, scope:scope}" -o table

Expected output: all three assignments, with scope showing two different management groups — the layering made visible.

Step 5 — Prove the deny bites. Public IP creation is allowed (the policy targets NICs); attaching it is not:

az group create -n rg-lab-deny -l centralindia
az network vnet create -g rg-lab-deny -n vnet-lab --address-prefix 10.99.0.0/24 \
  --subnet-name snet-a --subnet-prefix 10.99.0.0/26
az network public-ip create -g rg-lab-deny -n pip-lab --sku Standard   # succeeds — teaching moment

az network nic create -g rg-lab-deny -n nic-should-fail \
  --vnet-name vnet-lab --subnet snet-a --public-ip-address pip-lab

Expected error (this exact code is what your pipelines will surface):

{
  "code": "RequestDisallowedByPolicy",
  "message": "Resource 'nic-should-fail' was disallowed by policy. Policy identifiers:
   [{\"policyAssignment\":{\"name\":\"Lab corp: no public IPs on NICs\", ... }]"
}

Also try a wrong-region deploy (az group create -n rg-wrong -l eastus succeeds — resource groups are exempt from the resource-location policy; az network vnet create inside it in eastus fails). That nuance — why allowed-locations needs its resource-group companion policy e765b5de-1225-4ba3-bd56-1ac6695af988 — is a classic interview probe.

Step 6 — Read compliance like the platform team does.

az policy state trigger-scan --subscription "$SUB_ID" --no-wait
az policy state summarize --management-group lab \
  --query "policyAssignments[].{assignment:policyAssignmentId, nonCompliant:results.nonCompliantResources}" -o table

Expected: MCSB shows a non-zero non-compliant count (it always does — it is a benchmark, not a gate); the deny assignments show zero because the deny prevented the violation from ever existing.

Step 7 — Exempt, observe, and clean up the exemption.

az policy exemption create --name "lab-waiver" \
  --display-name "Lab waiver: pip allowed in this RG until Friday" \
  --exemption-category Waiver \
  --policy-assignment "$CORP_ID/providers/Microsoft.Authorization/policyAssignments/deny-nic-pip" \
  --scope "/subscriptions/$SUB_ID/resourceGroups/rg-lab-deny" \
  --expires-on "$(date -v+2d -u +%Y-%m-%dT%H:%M:%SZ)"

# Retry step 5's NIC create — it now succeeds inside the exempted RG
az network nic create -g rg-lab-deny -n nic-now-allowed \
  --vnet-name vnet-lab --subnet snet-a --public-ip-address pip-lab

Step 8 — Teardown. Reverse order: resources, exemption, assignments, subscription placement, MGs bottom-up (an MG must be empty to delete), elevation:

az group delete -n rg-lab-deny --yes --no-wait
az policy exemption delete --name "lab-waiver" --scope "/subscriptions/$SUB_ID/resourceGroups/rg-lab-deny" 2>/dev/null
for a in mcsb-baseline allowed-locations; do az policy assignment delete --name "$a" --scope "$LAB_ID"; done
az policy assignment delete --name "deny-nic-pip" --scope "$CORP_ID"
az account management-group subscription remove --name lab-corp --subscription "$SUB_ID"
# If you noted an original parent in step 4: az account management-group subscription add --name <orig> --subscription "$SUB_ID"
for mg in lab-corp lab-online lab-identity lab-management lab-connectivity \
          lab-landingzones lab-platform lab-sandbox lab-decommissioned lab; do
  az account management-group delete --name "$mg"
done
az role assignment delete --assignee "$(az account show --query user.name -o tsv)" --scope "/" --role "User Access Administrator"

Common mistakes & troubleshooting

The operational playbook — symptom to fix, in the order these actually turn up:

# Symptom Root cause Confirm Fix
1 New subscriptions appear outside the tree Default management group never set az account management-group hierarchy-settings list --name $TENANT_ID Set --default-management-group to sandbox/onboarding MG
2 Pipeline fails with RequestDisallowedByPolicy after a subscription move Destination archetype deny the workload violates Error body names the assignment; az policy assignment show Correct archetype, remediate the workload, or expiring exemption
3 DINE assignment exists, nothing ever deploys Assignment has no managed identity az policy assignment show --name X --query identity returns null Recreate with --mi-system-assigned --location; grant roles; remediate
4 Remediation tasks fail with AuthorizationFailed Identity missing the definition’s roleDefinitionIds at scope Deployment error in remediation task details az policy assignment identity assign --role ... --identity-scope ...
5 Existing resources stay non-compliant despite DINE DINE only fires on create/update az policy state list shows old resources untouched Create a remediation task with ReEvaluateCompliance
6 Deny works in portal test but pipelines bypass it Assignment at wrong scope; pipeline deploys to a sibling MG’s subscription az policy assignment list --scope <sub> --disable-scope-strict-match Re-scope assignment to the correct archetype MG
7 Users lost access after subscription move Inherited RBAC from the old MG chain vanished on re-parent Compare az role assignment list --all before/after Recreate needed grants at destination scope (groups, not users)
8 Cannot create the intermediate root No rights at root scope / az role assignment list --scope "/" is empty for you Global Admin elevates access; delegate; then remove elevation
9 az account management-group delete fails MG still has children or subscriptions az account management-group show --name X --expand Empty it first; delete leaf-to-root
10 Policy evaluates but compliance page looks stale Evaluation cycle hasn’t run since your change Timestamps in az policy state list az policy state trigger-scan; wait for completion
11 Assignment creation fails: limit reached 200 assignments per scope exceeded (loose-policy sprawl) Count assignments at the scope Consolidate into initiatives; delete redundant loose assignments
12 Workload team suddenly can’t peer their VNet That’s the design — peering is NetOps-only via custom role NotActions Role definition’s NotActions include virtualNetworks/peer/action Route the request through vending/NetOps, don’t grant the action
13 Sandbox workload can reach corp data Sandbox guardrails missing or peering created before Enforce-ALZ-Sandbox az network vnet peering list in the sandbox sub Assign the sandbox initiative; remove the peering; audit egress
14 Two “identical” corp subscriptions behave differently Direct subscription-scope assignments drifted (pre-vending legacy) az policy assignment list --scope <sub> (without strict-match disable) shows locals Remove local assignments; policy belongs at MG scope

The meta-mistake behind at least half of these rows: treating a policy change like a config change instead of a production deployment. Policy at management-group scope has a larger blast radius than almost any application release — it deserves the same pipeline, review, canary and rollback discipline.

Best practices

  1. One intermediate root, nothing at tenant root. Every future escape hatch — canary, quarantine, acquisition — depends on it.
  2. An MG exists only for a distinct policy/RBAC contract. Three to four levels; if you cannot name the contract, delete the node.
  3. Initiatives at the highest universally-true scope. Definitions stored once at the intermediate root; parameters do the per-scope variation.
  4. DINE/Modify over Deny wherever remediation is possible. Deny is for must-never-exist. Every Deny carries a nonComplianceMessage.
  5. Pin the ALZ library version and overlay your deltas. Upgrades become reviewable diffs; forks become archaeology.
  6. Canary every policy change in a parallel mini-hierarchy before it touches landingzones. There is no staging tenant for governance except the one you build.
  7. Vend, never hand-craft. Every subscription enters through the pipeline: create → place → baseline. Manual placements are drift by definition.
  8. Set the default-MG and hierarchy-protection settings on day one. The tree must be total; rogue subscriptions and rogue MGs are both closable holes.
  9. Groups get roles; humans get group membership; privileged scopes get PIM. Standing access above subscription scope is the exception you document, not the norm.
  10. Expiry-date every exemption. A waiver without an expiry is permanent risk acceptance wearing a temporary badge.
  11. Audit DINE identities quarterly. They are your largest silent permission surface; grants must match roleDefinitionIds exactly.
  12. Export and visualise the tree regularly (Azure Governance Visualizer / AzGovViz renders hierarchy + policy + RBAC as one artefact) — it doubles as your audit evidence pack.

Security notes

The hierarchy is itself a security boundary system; treat its own controls as tier-0:

Control Threat it counters Implementation
PIM-only Owner at intermediate root Standing global compromise via one phished account Eligible assignment, MFA + approval, 8 h max activation, alerts on activation
Hierarchy protection (requireAuthorizationForGroupCreation) Rogue ungoverned MG branches Hierarchy settings on the tenant root
Default-MG = sandbox/quarantine Out-of-band subscriptions landing ungoverned Hierarchy settings; sandbox guardrails apply instantly
Scoped vending SPN Pipeline compromise placing subs into platform MG-subscriptions-write on archetype MGs only; OIDC-federated, no secrets
DINE identity least-privilege Policy pipeline as privilege-escalation path Roles = roleDefinitionIds exactly; quarterly audit; protect who can edit definitions
AssignableScopes on custom roles Workload roles leaking into platform branch Scope custom roles to landingzones
DenyAction on platform plumbing Tampering with diagnostic settings / AMA identity DenyAction-DeleteUAMIAMA-style assignments at platform
MG activity-log diagnostics Silent hierarchy/policy tampering MG-level diagnostic settings to the central workspace; alert on policyAssignments/delete, MG move operations
Break-glass pair excluded from CA Locked out of your own tenant Two monitored cloud-only accounts, Owner at tenant root, alert on any sign-in

That MG-level diagnostic setting is obscure and valuable — management groups have their own activity log, and deleting a policy assignment at corp is exactly the kind of event that should page someone:

az rest --method put \
  --url "https://management.azure.com/providers/Microsoft.Management/managementGroups/contoso/providers/microsoft.insights/diagnosticSettings/mg-to-law?api-version=2020-01-01-preview" \
  --body "{\"properties\":{\"workspaceId\":\"$LAW_ID\",\"logs\":[{\"category\":\"Administrative\",\"enabled\":true},{\"category\":\"Policy\",\"enabled\":true}]}}"

Cost & sizing

The hierarchy itself is free — management groups, policy definitions, assignments, exemptions and compliance evaluation carry no charge. The bill lives in the platform subscriptions the hierarchy mandates, and in what the DINE layer switches on estate-wide:

Cost driver Where Typical monthly (USD ≈ INR) Lever
Log Analytics ingestion Management sub ~$2.75–3.00/GB → 40 GB/day ≈ $3,400 (≈ ₹2.9L) Commitment tiers from 100 GB/day (~15%+ off); DCR filtering; archive tiers
Azure Firewall Standard Connectivity sub ~$1.25/h + $0.016/GB ≈ $900+ (≈ ₹78k) Basic SKU for small estates; Premium only for TLS inspection needs
VPN / ExpressRoute gateways Connectivity sub VpnGw1 ~$140 (≈ ₹12k); ER gateways more Right-size; consolidate via hub
DDoS Network Protection Connectivity sub ~$2,944 flat, covers 100 public IPs (≈ ₹2.5L) The classic waive-with-risk-acceptance item below ~₹2L of exposed estate
Defender for Cloud plans Every sub (via Deploy-MDFC-Config) Servers P2 ~$15/server (≈ ₹1,250); P1 ~$7 Choose plans per resource type deliberately — this parameter is estate-wide
Azure Bastion Connectivity sub Standard ~$140 + per-hour scaling (≈ ₹12k) Shared via hub, not per-spoke
Identity DCs Identity sub 2× D2s_v5 + disks ≈ $180 (≈ ₹15k) Entra Domain Services (~$110 base) where AD DS depth isn’t needed
Backup (via Deploy-VM-Backup) Every sub ~$10/VM + storage (≈ ₹850/VM) Tune the policy’s inclusion filters; exclude ephemeral fleets

A worked example for a mid-size Indian enterprise — 60 subscriptions, ~350 VMs, Central India primary, hub-spoke:

Item Sizing Monthly ≈
Central Log Analytics 25 GB/day, 90-day interactive ₹1.9L
Hub network (Firewall Std, VpnGw2, Bastion Std, peering egress) 1 region + DR stub ₹1.5L
Defender for Servers P1 (deliberate choice over P2) 350 VMs ₹2.1L
Identity subscription 2 DCs + backup ₹18k
DDoS Network Protection Waived, documented risk acceptance ₹0
Platform total ≈ ₹5.7L/month (~$6,800)

The sizing insight: platform cost is a step function, not linear — that ₹5.7L serves 60 subscriptions or 200 with barely a change (log volume aside). Which is also the FinOps argument for the whole architecture: governance cost per workload falls as the estate grows. Show the platform as a per-subscription levy in showback (₹9.5k/sub at 60 subs, falling to ₹3k at 200) and the conversation with finance gets much easier.

Interview & exam questions

Mapped to AZ-305 (primary), AZ-104 and AZ-500 where noted.

  1. Why does ALZ use a single intermediate root instead of assigning policy at the Tenant Root Group? (AZ-305) — The tenant root governs everything forever, including future branches that must differ: canary hierarchies, quarantine MGs, acquired estates. An intermediate root carries the org-wide baseline while preserving an “above everything” escape hatch. It also keeps default-landed subscriptions from instantly inheriting denies mid-migration.
  2. A subscription needs both corp privacy rules and internet ingress. Where does it go? (AZ-305) — Nowhere as-is: one parent only, and archetypes don’t blend. Split the workload (private tier in corp, public edge in online), or place it in online and apply the needed corp-style controls as workload-level policy. The forcing of that decision is a feature — ambiguous exposure is what audits flag.
  3. Deny vs DeployIfNotExists — how do you choose per control? (AZ-305/AZ-500) — Deny for must-never-exist states (public IP on a corp NIC); DINE where a missing configuration can be added safely (diagnostics, backup, agents). Deny fails deployments and creates tickets; DINE fixes silently but needs identity/role plumbing and remediation tasks for existing estate.
  4. What exactly changes when you move a subscription between management groups? (AZ-104) — Policy context (source assignments stop, destination’s start — new requests near-immediately, compliance data on next scan) and inherited RBAC (source-chain roles vanish instantly). Resources, direct role assignments, tags and billing are untouched. Same-tenant only.
  5. Why must a DINE assignment have a managed identity, and what goes wrong via CLI? (AZ-104) — The remediation deployment executes as that identity, authorized by the definition’s roleDefinitionIds. The portal auto-creates the role assignments; az policy assignment create does not — assignment without granted roles yields silent non-remediation or AuthorizationFailed remediation tasks.
  6. How do you test a policy change when there’s no staging tenant? (AZ-305) — A canary management-group hierarchy: a parallel mini-tree with disposable subscriptions receiving every library bump and assignment change first, plus DoNotEnforce staging on production scopes before flipping to Default.
  7. Where do environment differences (dev/prod) live if not in the tree? (AZ-305) — In subscriptions within the same archetype. Environment is a subscription property; the guardrail delta between dev and prod is small enough for parameters and, where truly needed, subscription-scope assignments — not a doubled tree.
  8. Why are workload teams denied VNet peering writes in the custom subscription-owner role? (AZ-500) — A peering is a network trust decision: it can bypass the hub inspection path or bridge into the corp estate. Egress/ingress topology belongs to NetOps; vending creates the sanctioned peering at birth.
  9. What limits make initiative-based assignment mandatory at scale? (AZ-104) — 200 policy/initiative assignments per scope, with inheritance stacking several scopes deep; ~1,000 definitions fit in one initiative. Loose-policy assignment exhausts the budget and destroys auditability.
  10. Sandbox subscriptions are “loose” — what two controls stay hard, and why? (AZ-500) — No hybrid connectivity/peering toward the estate (a sandbox mistake must not reach corp data) and cost guardrails (budgets/SKU caps). Freedom inside an island.
  11. How does subscription vending assign network addressing without collisions? (AZ-305) — It doesn’t, natively — the module peers whatever CIDR you pass. You bring an allocation plan (e.g., /16 per archetype-region carved into /24 spokes) tracked in the vending data/state, which effectively becomes your IPAM.
  12. An auditor asks you to prove no corp workload can expose a public endpoint. What’s the artefact? (AZ-500) — The Deny-Public-Endpoints/Deny-Public-IP-On-NIC assignments at the corp MG (enforcement Default), the compliance state for the corp scope, and the exemption list with expiries — three API calls, not a spreadsheet exercise.

Quick check

  1. Which two tenant-level hierarchy settings should be configured on day one, and what does each prevent?
  2. A subscription under corp violates Deny-Storage-http. At which scope is that assignment actually made?
  3. Name the three platform management groups and the one subscription type each holds.
  4. Your DINE assignment shows compliant new VMs but hundreds of old non-compliant ones. What’s missing?
  5. Why should the vending service principal not have managementGroups/subscriptions/write on the platform MG?

Answers

  1. Default management group for new subscriptions (prevents ungoverned subscriptions landing at tenant root) and requireAuthorizationForGroupCreation (prevents arbitrary users creating rogue management groups).
  2. landingzones — it is true for every workload, corp and online alike, so it sits at the highest universally-true scope, not on corp.
  3. identity (identity subscription: DCs, identity plumbing), management (central Log Analytics/monitoring subscription), connectivity (hub network subscription).
  4. A remediation task — DINE only auto-fires on create/update; existing resources need az policy remediation create (ideally with ReEvaluateCompliance).
  5. So a compromised vending pipeline can never place a subscription into — or move one out of — the platform branch; its write scope is the archetype MGs only.

Glossary

Term Definition
Enterprise-scale / ALZ The CAF-aligned reference architecture and tooling for a governed, subscription-democratized Azure estate
Tenant Root Group The built-in root management group; ID equals the Entra tenant GUID; kept free of day-to-day policy
Intermediate root Your single top-level MG under tenant root; scope of org-wide non-negotiables
Archetype A management group plus its policy initiative set and RBAC contract; subscriptions are placed into archetypes
Corp / Online The two canonical workload archetypes: private hub-connected vs internet-facing
Platform subscriptions Identity, management and connectivity subscriptions delivering shared services under the platform branch
Subscription democratization Design principle: subscriptions as the cheap, isolated unit of scale, handed to teams by default
Policy-driven governance Design principle: guardrails as Azure Policy at MG scope rather than process documents
DINE DeployIfNotExists: policy effect that deploys missing companion configuration via a managed identity
DenyAction Policy effect blocking DELETE operations on matched resources
Enforcement mode Assignment property: Default (enforced) vs DoNotEnforce (evaluate/report only)
Exemption Scoped, categorised (Waiver/Mitigated), expiring carve-out from an assignment
Subscription vending Automated create → place → baseline pipeline for new subscriptions
ALZ library Microsoft’s versioned repository of ALZ policy definitions, assignments, archetypes and architectures
Canary hierarchy Parallel mini management-group tree for testing platform and policy changes
AMBA Azure Monitor Baseline Alerts: policy-driven alert baseline assigned at MG scopes

Next steps

landing-zoneenterprise-scalegovernanceazurepolicy-as-codemanagement-groupssubscription-vendingazure-policy
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading