Azure Landing Zones Explained: A 2026 Enterprise-Scale Starter


Executive Snapshot

Platform Landing ZonesApplication Landing Zones«AzureSubscription»Connectivity[Subscription]«AzureVirtualNetwork»Hub VNet[Hub-spoke]«AzureSubscription»Identity + Mgmt[Subscriptions]«AzureVirtualNetwork»Spoke A[App VNet]«AzureVirtualNetwork»Spoke B[App VNet]«AzureManagementGroups»Root ManagementGroup[Tenant]Azure Policy assignedat management group scopepeeringpeering
Management-group hierarchy over platform and application landing zones with hub-spoke.
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;
Enterprise-scale sequence from management groups to peered application landing zones.
  • 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

Azure Landing Zone — CAF Management Group Hierarchy«managementGroup»Tenant Root Group«managementGroup»Platform«managementGroup»Connectivity«managementGroup»Identity«managementGroup»Management«managementGroup»Landing Zones«managementGroup»Corp«managementGroup»Online«managementGroup»Sandboxes«AzureSubscription»Connectivity[Hub VNets, ExpressRoute, DNS]«AzureSubscription»Identity[Entra ID, AD DS, PIM]«AzureSubscription»Management[Log Analytics, Defender,Automation]«AzureSubscription»App Team A[internal, connected]«AzureSubscription»App Team B[internal, connected]«AzureSubscription»eCommerce Platform[internet-facing, DMZ]«AzureSubscription»Dev Sandbox[time-limited, auto-expire 90d]«managementGroup»Decommissioned   Compute   Identity   Monitoring   Network

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)

Azure Landing Zone — Hub-and-Spoke Network Topology«subscription»Platform — Connectivity subscription«vnet»Hub VNet 10.0.0.0/16«subnet»AzureFirewallSubnet 10.0.0.0/26«subnet»GatewaySubnet 10.0.1.0/27«subnet»AzureBastionSubnet 10.0.2.0/27«subnet»snet-private-endpoints 10.0.4.0/24«subscription»Corp Landing Zones«vnet»Spoke — App Team A 10.10.0.0/16«vnet»Spoke — App Team B 10.11.0.0/16«subscription»Online Landing Zones«vnet»Spoke — eCommerce 10.20.0.0/16«AzureFirewall»Azure FirewallPremium[FQDN filtering, TLS inspect]«AzureExpressRouteCircuit»ExpressRouteGateway[to on-premises]«AzureVPNGateway»VPN Gateway[site-to-site backup]«AzureBastionHost»Azure Bastion[secure RDP/SSH]«AzureDNS»Private DNS Zones[private name resolution]«AzureAppService»App Team A workload[internal]«AzureAppService»App Team B workload[internal]«AzureApplicationGateway»App Gateway + WAF[L7 + OWASP rules]«AzureAppService»eCommerce workload[internet-facing]«actor»On-Premises192.168.0.0/16  ExpressRoute / VPN  VNet peering  VNet peering  VNet peering   Compute   Network──Data flow──Control flow

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

  1. No standing Owner or Contributor on production scopes. Use PIM eligible assignments only.
  2. RBAC at management group scope for platform-wide roles; use custom roles to grant least-privilege at subscription scope for workload teams.
  3. Workload Identity Federation for CI/CD pipelines — no service principal client secrets stored anywhere.
  4. 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.

Thorsteinn Halldorsson Senior Cloud Engineer

Senior Cloud Engineer with 25+ years of hands-on experience across the datacenter-to-cloud stack: fiber SAN and disk storage, IBM/Lenovo blade and Dell/HP/Lenovo servers, Hyper-V and VMware clusters, and SQL and Remote Desktop Services (RDS) clusters. Deep in the Microsoft platform — Active Directory, PKI/certificate services, SQL, Power BI, Dynamics 365 Business Central (NAV) and AX (Axapta), Microsoft 365, Entra, and Intune — with a focus on Azure operations, FinOps, and applying AI tools like GitHub Copilot and Claude in real workflows. Writes practical, no-nonsense guides for IT professionals who need to ship real solutions.

Leave a Reply

Your email address will not be published. Required fields are marked *