Azure Landing Zones Explained: A 2026 Enterprise-Scale Starter
Executive Snapshot
| Category | Recommendation |
|---|---|
| Deployment Method | Azure Verified Modules (AVM) via Bicep or Terraform |
| Management Group Hierarchy | CAF-aligned: Root → Platform → Landing Zones → Sandboxes |
| Policy Engine | Azure Policy + Policy Initiatives (Deny + DeployIfNotExists) |
| Connectivity Model | Hub-and-Spoke or Azure Virtual WAN (vWAN) |
| Identity Plane | Microsoft Entra ID + Privileged Identity Management (PIM) |
| Minimum IAM Pattern | RBAC at management group scope, no standing Owner assignments |
| IaC Toolchain | Bicep (first-party) or AzureRM Terraform provider ≥ 4.x |
| Monitoring Baseline | Azure Monitor + Defender for Cloud (Standard tier) |
| Estimated Bootstrap Time | 2–4 hours for greenfield; 1–2 weeks for brownfield migration |
TL;DR
flowchart TD
Start([Begin enterprise-scale setup]) --> MG[Define management
group hierarchy]
MG --> Pol[Assign Azure Policy
at management groups]
Pol --> Plat[Build platform
landing zones]
Plat --> Conn[Deploy connectivity
hub network]
Plat --> Idn[Deploy identity
subscription]
Plat --> Mgmt[Deploy management
subscription]
Conn --> App[Provision application
landing zones]
Idn --> App
Mgmt --> App
App --> Peer[Peer spokes to hub]
Peer --> Done([Workloads deployable])
classDef step fill:#1f6feb,stroke:#58a6ff,color:#fff;
classDef plat fill:#6e40c9,stroke:#a371f7,color:#fff;
classDef term fill:#238636,stroke:#2ea043,color:#fff;
class MG,Pol,App,Peer step;
class Plat,Conn,Idn,Mgmt plat;
class Start,Done term;
- Azure landing zones are the pre-configured, governance-ready environment your workloads land in — not a product, but an architectural pattern codified by Microsoft's Cloud Adoption Framework (CAF).
- Skipping the landing zone phase is the #1 cause of Azure sprawl, accumulating technical debt that costs 3–5× more to remediate post-migration.
- The 2026 reference implementations use Azure Verified Modules (AVM), replacing the legacy AzOps/ALZ-Bicep standalone repos with a unified, versioned module registry.
- Policy-as-Code is non-negotiable at enterprise scale — you need DINE (DeployIfNotExists) policies that self-heal non-compliant resources, not dashboards you manually review.
- Identity, network, and management are platform responsibilities — workload teams should inherit controls, not configure them from scratch.
Introduction
Every enterprise Azure journey eventually collides with the same wall: subscriptions proliferating without governance, network topologies that block connectivity between workloads, compliance teams discovering that logging was never enabled, and security teams finding production resources with public IP addresses that nobody authorised.
This wall has a name — it's called landing zone debt — and it is almost always the result of skipping foundational architecture work in the rush to migrate the first workload.
Azure landing zones are Microsoft's answer to this problem. They represent the environmental scaffolding — management group hierarchy, policy guardrails, network topology, identity controls, and monitoring baselines — that must exist before a workload arrives, not after. Think of them as the pre-poured concrete and pre-run conduit in a building: invisible once the walls go up, but catastrophic to retrofit.
This guide walks through every layer of a 2026 enterprise-scale landing zone: the concepts, the architecture decisions, the Bicep and Terraform code you'll actually deploy, and the operational patterns that keep it governed at scale. If you're onboarding Azure at an organisation with more than 50 users or multiple compliance requirements, this is your starting point.
Prerequisites
Before deploying anything, confirm you have:
- Azure CLI ≥ 2.61 (
az --version) with Bicep CLI ≥ 0.28 (az bicep version) - Terraform ≥ 1.9 if using the HashiCorp path (
terraform version) - Azure AD / Entra ID Global Administrator role (required for management group and Entra configuration; can be removed post-bootstrap)
- Elevated access toggled on at the Entra tenant root (
az rest --method post --url /providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01) - Owner on the root management group (assigned after elevation)
- A dedicated "platform" Azure subscription for connectivity, identity, and management workloads (three separate subscriptions is the recommended pattern)
- Git and a CI/CD platform (Azure DevOps or GitHub Actions) for IaC pipelines
- Familiarity with Azure Policy, RBAC, and either Bicep or Terraform syntax
1. Understanding the Landing Zone Mental Model
Before you write a single line of IaC, you need to internalise what a landing zone actually is — and what it isn't.
What It Is
An Azure landing zone is the sum of environment configuration that ensures any workload deployed into it automatically inherits:
- Security controls (Defender for Cloud, diagnostic settings, no public IP without justification)
- Networking (connectivity to on-premises, DNS resolution, firewall egress)
- Identity and access (RBAC roles, PIM eligibility, no standing privileged access)
- Cost management (budget alerts, tagging policies, chargeback tags)
- Compliance posture (regulatory initiative assignments, audit logs, immutable log storage)
What It Is Not
- A single Azure subscription
- A network virtual appliance
- A product you buy from Microsoft
- Something you configure once and ignore
The CAF Hierarchy, Visualised
This hierarchy is the spine of everything. Every policy, RBAC assignment, and compliance initiative flows downward through inheritance. Get the hierarchy wrong and you'll fight it forever.
2. Management Group Hierarchy: Design Decisions
The management group structure is the single highest-leverage decision in a landing zone deployment. It cannot be easily changed post-workload deployment without re-assigning policies and RBAC.
Core Principles
Policy inheritance, not duplication. Assign policies once at the highest appropriate scope. A Deny public IP policy assigned at the Landing Zones management group applies to every subscription under it automatically.
Environment separation via management groups, not tags. Tags are queryable metadata — they are not a security boundary. Separate Production, Non-Production, and Sandbox into distinct MG branches so you can apply different policy intensities.
Subscription-per-workload-team is the unit of scale. Subscriptions are free to create and provide hard blast radius limits. Don't pack multiple workload teams into one subscription to "save complexity" — that complexity debt compounds.
Recommended Management Group Layout
| Management Group | Purpose | Key Policy Assignments |
|---|---|---|
Tenant Root |
Policy defaults for ALL subscriptions | Require tags, deny classic resources |
Platform/Connectivity |
Hub VNets, ExpressRoute GWs, Azure Firewall | Restrict VNet peering, protect route tables |
Platform/Identity |
Entra ID DCs, AAD DS | Deny public endpoints, require backup |
Platform/Management |
Log Analytics, Automation, Defender | Deny data export outside tenant |
LandingZones/Corp |
Internal-facing workload subscriptions | Deny public IPs, require UDR to hub firewall |
LandingZones/Online |
DMZ and internet-facing workloads | Allow curated public IPs, WAF required |
Sandboxes |
Exploration, PoC subscriptions | Allow almost anything, auto-expire after 90 days |
3. Policy-as-Code: The Guardrail Engine
Policies are what transform a management group hierarchy from an organisational chart into an active governance system. There are four policy effects you must understand:
| Effect | Behaviour | Use Case |
|---|---|---|
Deny |
Blocks the ARM request entirely | Prevent public IPs, restrict SKUs |
Audit |
Allows the resource, creates compliance finding | Report on missing tags |
DeployIfNotExists (DINE) |
Deploys a companion resource if absent | Auto-deploy diagnostic settings |
Modify |
Patches the resource at create/update time | Add missing tags automatically |
The DINE pattern is the most powerful for landing zones because it self-heals. When a new subscription drops into the Corp management group, DINE policies automatically:
- Create a Log Analytics workspace link
- Enable Defender for Cloud plans
- Deploy the Azure Monitor Agent
- Configure diagnostic settings on all supported resources
Assigning a Built-in Initiative with Bicep
// modules/policy/assign-initiative.bicep
targetScope = 'managementGroup'
@description('The management group ID to assign the initiative to.')
param managementGroupId string
@description('Location for the managed identity used by DINE policies.')
param location string = 'australiaeast'
@description('Log Analytics workspace resource ID for diagnostic routing.')
param logAnalyticsWorkspaceId string
var policyAssignmentName = 'alz-corp-diagnostics'
var deployDiagnosticsInitiativeId = '/providers/Microsoft.Authorization/policySetDefinitions/0884adba-2312-4468-abeb-5422caed1038'
resource policyAssignment 'Microsoft.Authorization/policyAssignments@2024-04-01' = {
name: policyAssignmentName
scope: managementGroup(managementGroupId)
location: location
identity: {
type: 'SystemAssigned' // Required for DINE effect
}
properties: {
displayName: 'ALZ Corp - Deploy Diagnostic Settings'
policyDefinitionId: deployDiagnosticsInitiativeId
parameters: {
logAnalyticsWorkspace: {
value: logAnalyticsWorkspaceId
}
}
enforcementMode: 'Default'
}
}
// Grant the managed identity the permissions it needs to deploy resources
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(policyAssignment.id, 'Contributor')
scope: managementGroup(managementGroupId)
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') // Contributor
principalId: policyAssignment.identity.principalId
principalType: 'ServicePrincipal'
}
}
Deny Public IP Policy (Custom)
// modules/policy/deny-public-ip.bicep
targetScope = 'managementGroup'
resource denyPublicIpPolicy 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {
name: 'deny-public-ip-creation'
properties: {
displayName: 'Deny creation of Public IP addresses'
description: 'Prevents creation of Public IP addresses except in Online and Connectivity management groups.'
policyType: 'Custom'
mode: 'All'
metadata: {
category: 'Network'
version: '1.0.0'
}
policyRule: {
if: {
allOf: [
{
field: 'type'
equals: 'Microsoft.Network/publicIPAddresses'
}
{
field: 'Microsoft.Network/publicIPAddresses/sku.name'
notEquals: 'Basic' // Basic SKU is being retired; belt-and-suspenders
}
]
}
then: {
effect: 'Deny'
}
}
}
}
4. Network Topology: Hub-and-Spoke vs. Azure Virtual WAN
The network layer is where landing zone architecture choices have the biggest day-two operational impact. You have two primary patterns.
Hub-and-Spoke (Classic)
Choose Hub-and-Spoke when: You have a small number of regions (1–3), want full control over the hub network appliances, and have an existing NVA investment.
Choose Azure Virtual WAN when: You have 5+ regions, need automated any-to-any routing, or want Microsoft to manage the routing backbone.
Deploying the Hub VNet with Terraform
# platform/connectivity/hub_vnet.tf
# Requires: azurerm provider >= 4.0, terraform >= 1.9
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.0"
}
}
backend "azurerm" {
resource_group_name = "rg-tfstate-platform"
storage_account_name = "satfstateplatform001"
container_name = "tfstate"
key = "connectivity/hub.tfstate"
}
}
locals {
hub_address_space = "10.0.0.0/16"
subnets = {
AzureFirewallSubnet = "10.0.0.0/26" # Name is fixed by Azure
GatewaySubnet = "10.0.1.0/27" # Name is fixed by Azure
AzureBastionSubnet = "10.0.2.0/27" # Name is fixed by Azure
snet-management = "10.0.3.0/24"
snet-private-endpoints = "10.0.4.0/24"
}
}
resource "azurerm_resource_group" "connectivity" {
name = "rg-connectivity-${var.environment}-${var.location_short}"
location = var.location
tags = var.common_tags
}
resource "azurerm_virtual_network" "hub" {
name = "vnet-hub-${var.environment}-${var.location_short}-001"
resource_group_name = azurerm_resource_group.connectivity.name
location = azurerm_resource_group.connectivity.location
address_space = [local.hub_address_space]
dns_servers = var.custom_dns_servers # Point to Identity subscription DNS
tags = var.common_tags
}
resource "azurerm_subnet" "subnets" {
for_each = local.subnets
name = each.key
resource_group_name = azurerm_resource_group.connectivity.name
virtual_network_name = azurerm_virtual_network.hub.name
address_prefixes = [each.value]
}
resource "azurerm_firewall_policy" "hub" {
name = "afwp-hub-${var.environment}-${var.location_short}-001"
resource_group_name = azurerm_resource_group.connectivity.name
location = azurerm_resource_group.connectivity.location
sku = "Premium"
threat_intelligence_mode = "Alert"
dns {
proxy_enabled = true # Critical: enables FQDN filtering in network rules
servers = var.custom_dns_servers
}
tags = var.common_tags
}
resource "azurerm_firewall" "hub" {
name = "afw-hub-${var.environment}-${var.location_short}-001"
resource_group_name = azurerm_resource_group.connectivity.name
location = azurerm_resource_group.connectivity.location
sku_name = "AZFW_VNet"
sku_tier = "Premium"
firewall_policy_id = azurerm_firewall_policy.hub.id
ip_configuration {
name = "ipconfig-hub-firewall"
subnet_id = azurerm_subnet.subnets["AzureFirewallSubnet"].id
public_ip_address_id = azurerm_public_ip.firewall.id
}
tags = var.common_tags
}
resource "azurerm_public_ip" "firewall" {
name = "pip-afw-hub-${var.environment}-${var.location_short}-001"
resource_group_name = azurerm_resource_group.connectivity.name
location = azurerm_resource_group.connectivity.location
allocation_method = "Static"
sku = "Standard"
zones = ["1", "2", "3"]
tags = var.common_tags
}
5. Identity and Access Management at Landing Zone Scale
Identity is the new perimeter. Landing zone IAM design must enforce least privilege from day one because RBAC sprawl is nearly impossible to clean up retroactively.
IAM Principles
- No standing Owner or Contributor on production scopes. Use PIM eligible assignments only.
- RBAC at management group scope for platform-wide roles; use custom roles to grant least-privilege at subscription scope for workload teams.
- Workload Identity Federation for CI/CD pipelines — no service principal client secrets stored anywhere.
- Break-glass accounts: two emergency Global Admin accounts, cloud-only, hardware MFA, stored credentials in physical safe, alerts on every sign-in.
Bootstrap RBAC with Azure CLI
#!/usr/bin/env bash
# bootstrap-rbac.sh
# Run ONCE during landing zone initialisation with Global Admin + Owner on root MG
# Requires: az cli >= 2.61, jq
set -euo pipefail
TENANT_ID=$(az account show --query tenantId -o tsv)
ROOT_MG_ID="${TENANT_ID}" # Root MG ID equals the Tenant ID
PLATFORM_MG_ID="mg-platform-${TENANT_ID:0:8}"
LZ_MG_ID="mg-landingzones-${TENANT_ID:0:8}"
LOCATION="australiaeast"
echo "==> Creating management group hierarchy..."
az account management-group create \
--name "${PLATFORM_MG_ID}" \
--display-name "Platform" \
--parent "${ROOT_MG_ID}"
az account management-group create \
--name "mg-platform-connectivity" \
--display-name "Connectivity" \
--parent "${PLATFORM_MG_ID}"
az account management-group create \
--name "mg-platform-identity" \
--display-name "Identity" \
--parent "${PLATFORM_MG_ID}"
az account management-group create \
--name "mg-platform-management" \
--display-name "Management" \
--parent "${PLATFORM_MG_ID}"
az account management-group create \
--name "${LZ_MG_ID}" \
--display-name "Landing Zones" \
--parent "${ROOT_MG_ID}"
az account management-group create \
--name "mg-landingzones-corp" \
--display-name "Corp" \
--parent "${LZ_MG_ID}"
az account management-group create \
--name "mg-landingzones-online" \
--display-name "Online" \
--parent "${LZ_MG_ID}"
az account management-group create \
--name "mg-sandboxes" \
--display-name "Sandboxes" \
--parent "${ROOT_MG_ID}"
az account management-group create \
--name "mg-decommissioned" \
--display-name "Decommissioned" \
--parent "${ROOT_MG_ID}"
echo "==> Management group hierarchy created."
echo "==> Assigning platform team RBAC (PIM eligible assignments must be done in portal or via MS Graph API)..."
# Assign Network Contributor to the platform networking SP at Connectivity MG
NETWORK_SP_OID=$(az ad sp show --id "api://platform-network-sp" --query id -o tsv 2>/dev/null || echo "")
if [[ -n "${NETWORK_SP_OID}" ]]; then
az role assignment create \
--assignee-object-id "${NETWORK_SP_OID}" \
--assignee-principal-type ServicePrincipal \
--role "Network Contributor" \
--scope "/providers/Microsoft.Management/managementGroups/mg-platform-connectivity"
echo "==> Network Contributor assigned to platform networking SP."
else
echo "WARN: platform-network-sp not found — skipping RBAC assignment. Create the SP first."
fi
echo "==> Bootstrap complete. Verify hierarchy in the Azure Portal > Management Groups."
6. Management Baseline: Logging, Monitoring, and Defender
The management platform subscription hosts the shared services that every workload subscription consumes. Skipping this means every workload team recreates their own logging stack — a compliance and cost nightmare.
What Lives in the Management Subscription
| Resource | Purpose |
|---|---|
| Log Analytics Workspace (centralised) | Single pane of glass for all diagnostic logs |
| Azure Monitor Workspace | Prometheus-compatible metrics for AKS and VMs |
| Automation Account | Update management, runbooks, change tracking |
| Microsoft Defender for Cloud (Standard) | CSPM, workload protection, regulatory compliance |
| Azure Policy Guest Configuration | In-VM OS compliance scanning |
| Sentinel (optional at bootstrap) | SIEM/SOAR — add when security ops team is ready |
Enable Defender for Cloud Across All Subscriptions (PowerShell)
# Enable-DefenderPlans.ps1
# Enables core Defender for Cloud plans on every subscription under the Landing Zones MG
# Requires: Az.Security module >= 6.0, Az.ResourceGraph module >= 1.0
#Requires -Modules Az.Security, Az.ResourceGraph, Az.Accounts
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$LandingZoneManagementGroupId,
[Parameter()]
[ValidateSet('Standard', 'Free')]
[string]$PricingTier = 'Standard'
)
$ErrorActionPreference = 'Stop'
# Enumerate all subscriptions under the target MG recursively
$query = @"
resourcecontainers
| where type == 'microsoft.resources/subscriptions'
| where properties.managementGroupAncestorsChain contains '$LandingZoneManagementGroupId'
| project subscriptionId = name, displayName = properties.displayName
"@
$subscriptions = Search-AzGraph -Query $query -First 1000
Write-Host "Found $($subscriptions.Count) subscriptions under '$LandingZoneManagementGroupId'"
$plans = @(
'VirtualMachines',
'SqlServers',
'AppServices',
'StorageAccounts',
'Containers',
'KeyVaults',
'Arm',
'Dns',
'OpenSourceRelationalDatabases',
'SqlServerVirtualMachines'
)
foreach ($sub in $subscriptions) {
Write-Host "`nProcessing subscription: $($sub.displayName) ($($sub.subscriptionId))"
try {
Set-AzContext -SubscriptionId $sub.subscriptionId | Out-Null
foreach ($plan in $plans) {
$result = Set-AzSecurityPricing -Name $plan -PricingTier $PricingTier
Write-Host " [OK] $plan -> $PricingTier"
}
# Ensure auto-provisioning of MMA/AMA is enabled
Set-AzSecurityAutoProvisioningSetting -Name 'mma' -EnableAutoProvision | Out-Null
Write-Host " [OK] Auto-provisioning enabled"
}
catch {
Write-Warning " [FAIL] $($sub.subscriptionId): $_"
}
}
Write-Host "`nDefender for Cloud configuration complete."
7. Subscription Vending: Automating Workload Onboarding
The subscription vending pattern is what transforms the landing zone from a static architecture into a self-service platform. Instead of platform teams manually provisioning subscriptions, workload teams submit a request (a PR or a service catalogue item) and automation delivers a fully configured, policy-compliant subscription within minutes.
Subscription Vend Flow
sequenceDiagram
actor Dev as Workload Team
participant PR as Git Pull Request
participant CI as GitHub Actions / ADO Pipeline
participant ARM as Azure Resource Manager
participant EA as EA / MCA Billing Account
participant MG as Management Group
Dev->>PR: Submit subscription-request.yaml\n(team, workload, env, cost-centre)
PR->>CI: PR merge triggers pipeline
CI->>EA: Create Subscription via Billing API
EA-->>CI: subscriptionId returned
CI->>MG: Move subscription to correct MG\n(Corp or Online)
CI->>ARM: Deploy baseline resources\n(RGs, budgets, tags, diagnostic settings)
ARM->>ARM: Policy DINE effects trigger\n(Log Analytics link, Defender, AMA)
ARM-->>CI: Deployment complete
CI->>Dev: Notify via Teams / email\n(subscriptionId, RBAC, docs link)
Subscription Request YAML Schema
# subscription-requests/app-team-checkout-prod.yaml
# Validated by JSON Schema in CI before merge
apiVersion: v1alpha2
kind: SubscriptionRequest
metadata:
name: checkout-prod
requestedBy: "platform-team@contoso.com"
requestDate: "2026-06-22"
spec:
displayName: "sub-checkout-prod-001"
billingScope: "/providers/Microsoft.Billing/enrollmentAccounts/EA-ACCOUNT-ID"
managementGroup: "mg-landingzones-corp"
environment: production
workloadOwner: "checkout-team@contoso.com"
costCentre: "CC-4821"
businessUnit: "eCommerce"
dataClassification: confidential
networking:
connectToHub: true
vnetAddressSpace: "10.12.0.0/16"
region: "australiaeast"
enablePrivateDns: true
iam:
contributorGroups:
- "grp-checkout-team-contributors"
readerGroups:
- "grp-security-ops-readers"
budget:
monthlyLimitAUD: 5000
alertThresholds: [50, 80, 100]
alertEmails:
- "checkout-team@contoso.com"
- "finance-cloud@contoso.com"
8. Pitfalls to Avoid
These are the patterns most commonly responsible for failed or painful landing zone deployments.
❌ Assigning Policies Before Moving Subscriptions
If you move a subscription into a management group before the DINE policy remediation tasks run, you'll have a flood of non-compliant resources with no auto-remediation triggered. Always trigger a remediation task immediately after assigning a DINE policy.
# Trigger remediation for a DINE policy assignment
az policy remediation create \
--name "remediate-diagnostics-$(date +%Y%m%d)" \
--policy-assignment "/providers/Microsoft.Management/managementGroups/mg-landingzones-corp/providers/Microsoft.Authorization/policyAssignments/alz-corp-diagnostics" \
--resource-discovery-mode ReEvaluateCompliance \
--management-group "mg-landingzones-corp"
❌ Using a Single Flat Subscription for "Simplicity"
Flat subscription architectures feel simple at day one and become unmanageable by day 180. Every workload team has access to every other workload's resources. RBAC becomes a permission explosion. Blast radius for incidents is the entire subscription.
❌ Omitting the Decommissioned Management Group
Without a Decommissioned management group, cancelled subscriptions stay in a limbo state within production management groups — inheriting policies, appearing in compliance dashboards, and causing alert noise. Always move subscriptions to Decommissioned before cancellation.
❌ Not Locking the Hub VNet
The hub VNet is a shared blast radius. If a workload team has permissions to modify hub route tables or peerings, one misconfiguration takes down connectivity for all spokes. Apply a CanNotDelete resource lock and Deny policy on route tables in the connectivity subscription.
❌ Bootstrap Identity Left Privileged Post-Deployment
The service principal or user account used to bootstrap the landing zone is given Owner at root scope. This is a privileged identity that must be removed or moved to PIM eligible-only assignment once bootstrapping is complete. Leaving it as a permanent assignment is a critical security risk.
❌ Using the Portal Instead of IaC for Policy Assignments
Portal-created policy assignments are invisible to your IaC state and will create drift. Every governance resource must live in source control and be deployed via pipeline, or you will eventually overwrite or lose it.
9. Troubleshooting
Error: AuthorizationFailed when Creating Management Groups
Cause: The executing identity doesn't have Owner on the root management group (/providers/Microsoft.Management/managementGroups/<TENANT_ID>).
Fix:
# Step 1: Toggle Global Admin elevation
az rest --method post \
--url "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"
Wrap-up. Elevate access only long enough to apply the root-scope role assignment, then remove the elevation. With the management-group hierarchy, policies, and platform subscriptions in place, the landing zone is ready to onboard workloads: govern every change through the same IaC pipeline from here.
Leave a Reply