Efficiently Creating Azure Management Groups In Terraform
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 keycan(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.

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