Every failed Azure adoption I have seen shares the same root cause. Teams start creating subscriptions and resources before they design the foundation. Six months later they have forty subscriptions, no naming standard, inconsistent networking, and no idea who owns what. Fixing that after the fact is painful. Building it right from day one is not hard, and that is exactly what an Azure Landing Zone gives you.
What a Landing Zone Actually Is
A landing zone is the pre-provisioned environment where workloads land. It covers four pillars: a management group hierarchy that reflects how you govern, Azure Policy assignments that enforce standards automatically, a shared connectivity layer (hub network, firewall, DNS), and an identity baseline. Microsoft formalizes this in the Cloud Adoption Framework, and the reference hierarchy looks like this:
Tenant Root
└── Contoso (Intermediate Root)
├── Platform
│ ├── Identity
│ ├── Management
│ └── Connectivity
├── Landing Zones
│ ├── Corp (internal, hub connected)
│ └── Online (internet facing)
├── Sandbox
└── Decommissioned
The separation matters because policy inheritance flows down the tree. Policies assigned at the Landing Zones level apply to every workload subscription, while the Sandbox group can have relaxed rules and Platform subscriptions get their own hardened set.
Management Groups with Terraform
resource "azurerm_management_group" "root" {
display_name = "contoso"
}
resource "azurerm_management_group" "platform" {
display_name = "platform"
parent_management_group_id = azurerm_management_group.root.id
}
resource "azurerm_management_group" "connectivity" {
display_name = "connectivity"
parent_management_group_id = azurerm_management_group.platform.id
}
resource "azurerm_management_group" "landing_zones" {
display_name = "landing-zones"
parent_management_group_id = azurerm_management_group.root.id
}
resource "azurerm_management_group" "corp" {
display_name = "corp"
parent_management_group_id = azurerm_management_group.landing_zones.id
}
For production I recommend the official Azure Landing Zones Terraform module (Azure/avm-ptn-alz) rather than hand rolling every group and policy, but you should understand what it creates, and the raw resources above are what sit underneath.
Policy Driven Governance
Policies are what make a landing zone self enforcing. Instead of writing a wiki page that says “all storage accounts must deny public access” you assign a policy that makes it impossible to do otherwise. A few assignments I consider mandatory at the Landing Zones scope: deny public network access on storage accounts and SQL, require diagnostic settings to a central Log Analytics workspace, restrict allowed regions, and enforce tagging for cost ownership.
resource "azurerm_management_group_policy_assignment" "allowed_locations" {
name = "allowed-locations"
management_group_id = azurerm_management_group.landing_zones.id
policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/e56962a6-4747-49cd-b67b-bf8b01975c4c"
parameters = jsonencode({
listOfAllowedLocations = {
value = ["westeurope", "northeurope"]
}
})
}
resource "azurerm_management_group_policy_assignment" "require_tags" {
name = "require-costcenter"
management_group_id = azurerm_management_group.landing_zones.id
policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/871b6d14-10aa-478d-b590-94f262ecfa99"
parameters = jsonencode({
tagName = { value = "costcenter" }
})
}
Subscription Vending
The last piece most teams skip is subscription vending: a repeatable pipeline that creates a new subscription, places it under the right management group, wires it to the hub network, and assigns the owning team their RBAC roles. When a team requests an environment, they should get a compliant subscription in under an hour without a single manual portal click. With an Enterprise Agreement or MCA billing account, Terraform can create the subscription itself:
resource "azurerm_subscription" "workload" {
subscription_name = "sub-corp-payments-prod"
billing_scope_id = data.azurerm_billing_mca_account_scope.this.id
}
resource "azurerm_management_group_subscription_association" "assoc" {
management_group_id = azurerm_management_group.corp.id
subscription_id = "/subscriptions/${azurerm_subscription.workload.subscription_id}"
}
Lessons from Real Deployments
Keep the hierarchy shallow. Three or four levels is enough, and deep trees based on org charts break the moment the org changes. Assign policies in audit mode first, review compliance for two weeks, then flip to deny. Never assign RBAC to individuals, always to Entra ID groups. And treat the landing zone code as a product with its own repo, pipeline, and versioning, because it will evolve constantly as the platform team learns.
In the coming posts this month I will build on this foundation and go deep into the networking, security, and workload services that sit on top of it.
Cheers
Osama
Leave a comment