Azure Landing Zones with CAF and Terraform
How I design enterprise landing zones that scale: management group hierarchy, subscription governance, and Azure Policy guardrails delivered as code.
The problem
Every enterprise cloud journey starts the same way. At one consulting client, three platform teams had spent a year creating subscriptions ad-hoc and spinning up resources directly. By the time I was involved, there were roughly 200 resources scattered across a dozen subscriptions: some were real workloads, some were experiments nobody remembered, and none had a naming convention, a budget owner, or any way to say "no" at scale. Every quarter, security ran a manual compliance sweep that produced a 40-page spreadsheet nobody acted on.
That is the real problem — shadow IT, cost sprawl, and guardrails that are advisory instead of enforced. When clients say "we want a cloud strategy", what they actually need is a foundation that makes the right choice the easy choice.
How I resolved it
I resolved it in two parts: a target hierarchy from the Microsoft Cloud Adoption Framework (CAF), and Terraform code that made the hierarchy real and self-enforcing. CAF answers the hardest questions before you write a line of Terraform:
- Who owns what? The management group hierarchy maps to the org chart, not the current subscription list.
- What can each team do? RBAC and Azure Policy at the management group scope cascade down, so guardrails follow workloads everywhere they go.
- How does it stay compliant? Policy-as-Code means compliance is enforced continuously, not audited quarterly.
The landing zone is not a one-time project. It is a platform that teams consume.
What I built
The core deliverable is a Terraform codebase that provisions the full hierarchy:
resource "azurerm_management_group" "platform" {
name = "mg-platform"
display_name = "Platform"
}
resource "azurerm_management_group_subscription_association" "connectivity" {
management_group_id = azurerm_management_group.platform.id
subscription_id = var.connectivity_subscription
}
The guardrails are code too. Policy assignments at the management group scope cascade to every subscription below them, and budget alerts give each team owner a number to watch:
resource "azurerm_management_group_policy_assignment" "platform_guardrails" {
name = "platform-guardrails"
management_group_id = azurerm_management_group.platform.id
policy_definition_id = azurerm_policy_set_definition.governance.id
}
resource "azurerm_consumption_budget_subscription" "team" {
name = "team-${var.team_name}-budget"
subscription_id = data.azurerm_subscription.current.id
amount = var.budget_amount
time_grain = "Monthly"
time_period {
start_date = "2026-01-01"
end_date = "2027-01-01"
}
notification {
operator = "GreaterThanOrEqualTo"
threshold = 80
contact_emails = [var.budget_owner_email]
}
}
Beyond the hierarchy, the important pieces are:
- Subscription governance — a documented naming convention, budget alerts at subscription scope, and a clear ownership model so nobody has to guess who pays for what.
- Azure Policy guardrails — custom and built-in initiatives covering allowed locations, resource tags, and the "not allowed" resource types that cause the most drift.
- RBAC — custom role definitions scoped to management groups with PIM for just-in-time elevation.
- Modular Terraform — versioned modules with remote state and a review workflow so changes to the foundation are themselves governed.
Results
The measurable outcome I aim for on every engagement: new teams provision compliant environments in hours instead of weeks, and the platform team stops being the bottleneck. Guardrails run continuously — if someone tries to deploy a disallowed resource, Azure blocks it at the API, not at a human review.
Lessons learned
- Put policy at the management group, not the subscription. It cascades. Re-architecting later is painful.
- Landing zones are code. If the hierarchy is only documented in a whiteboard diagram, it will drift on day two.
- Start boring, stay boring. The first version should use the most standard building blocks possible. Innovation belongs in workloads, not in the foundation.
A landing zone is not the destination. It is the point where "we should do cloud properly" turns into something teams can actually use.