← All posts
Zühlke Engineering AG · Senior Cloud Engineer

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.

AzureLanding ZonesTerraformCAF2026-08-15 · 9 min read

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.