All posts
Automation Azure Scripts Terraform

Efficiently Creating Azure Management Groups In Terraform

· Mike Hosker

As part of a recent project I have been writing a Terraform module to bring all of our tenant IAM settings into state. This includes amongst many other things Azure management groups.

The Problem

Azure management groups support up to 6 levels of nesting, where each level depends on its parent existing first. The naive approach is to write a separate resource block per level — but that's repetitive, hard to maintain, and doesn't scale. The goal was to accept a single hierarchical input map and create all groups with proper dependency ordering using for_each.

Input Structure

The module accepts a hierarchical map like this:

management_groups = {
  "MH" = {
    name = "Mike Hosker"
    children = {
      "MH-Prod" = {
        name = "Production"
        children = {
          "MH-Prod-Apps" = {
            name = "Applications"
            children = {}
          }
        }
      }
    }
  }
}

Level 1 — Root Groups

The first level is straightforward — create groups directly under the tenant root:

resource "azurerm_management_group" "level_1" {
  for_each     = var.management_groups
  name         = each.key
  display_name = each.value.name
}

Levels 2–6 — Using zipmap to Flatten Children

For each subsequent level the pattern is the same: extract children from the previous level, build path-like composite keys that encode the full ancestry chain, then use those keys to reference the correct parent.

locals {
  level_2 = zipmap(
    flatten([
      for key, value in var.management_groups :
        formatlist("${key}/%s", keys(value.children))
        if can(value.children)
    ]),
    flatten([
      for value in var.management_groups :
        values(value.children)
        if can(value.children)
    ])
  )
}

This produces keys like MH/MH-Prod — a slash-delimited path that encodes the full ancestry.

The resource block for level 2 then becomes:

resource "azurerm_management_group" "level_2" {
  for_each                   = local.level_2
  name                       = basename(each.key)
  display_name               = each.value.name
  parent_management_group_id = azurerm_management_group.level_1[
    trimsuffix(each.key, "/${basename(each.key)}")
  ].id
  depends_on = [azurerm_management_group.level_1]
}

Key functions at work:

  • basename() — extracts the final segment of the path (the child's own ID)
  • trimsuffix() — strips the child segment to isolate the parent's path key
  • can(value.children) — safely filters entries that have children, avoiding errors on leaf nodes

Levels 3–6 repeat the same pattern, each time feeding off the local from the level above. The depends_on chain ensures Terraform creates groups in the correct order.

Result

The complete hierarchy is created from a single variable with no code duplication. Adding a new branch only requires updating the input map — the Terraform is unchanged.

Azure Management Groups hierarchy created by the Terraform module

The full module is available on GitHub: mhosker/terraform-azure-management-groups