Terraform flatten: Tackling Complex Data Structures in Azure Deployments

In the world of infrastructure automation, we’re constantly juggling concepts like “clean state”, “idempotency”, and “predictable output”.

But when you’re deep into Azure — where everything from subnets to role assignments tends to be wrapped in nested lists or objects — things can get messy fast. That’s where Terraform’s flatten function steps in. It’s not just another utility function — it’s a lifesaver when your data starts to spiral out of control.


Why flatten Even Matters in Azure

Azure’s resource configurations are often deeply nested. Think of virtual networks and their subnets, or assigning roles across different scopes. You’re pulling data from modules, locals, and data sources, and before you know it, you’re staring at a list of lists.

Terraform, especially in modular designs, makes you deal with layers: for_each, dynamic blocks, maps of lists, outputs from modules — all of which can lead to nested data structures that Terraform doesn’t like.

Here’s the thing: many Azure use cases break if you don’t tidy up your lists. Terraform will start throwing type errors, or worse, silently misbehaving. Flattening those lists is often the only clean way out.


So, What Does flatten Actually Do?

flatten([["A", "B"], ["C"], ["D", "E"]])

Gives you:

["A", "B", "C", "D", "E"]

Simple, right? But this little trick can be a game-changer in real-world Azure scenarios:

      • You’re merging output from multiple modules.
      • You have dynamic nested loops for resources.
      • You’re pulling in inputs that come from several layers of logic.

Basically, whenever you’re passing data into a for_each or a resource argument and it’s a list of lists — you’ll want flatten in your toolbox.


Real-Life Example 1: Merging Subnet Outputs from Modules

Let’s say you’ve got two modules — one for each VNet — and they each return a list of subnet IDs. Now you want to associate a Network Security Group (NSG) to all those subnets.

module "vnet_a" {
  source = "./modules/vnet"
  name   = "vnet-a"
}

module "vnet_b" {
  source = "./modules/vnet"
  name   = "vnet-b"
}

locals {
  all_subnets_nested = [
    module.vnet_a.subnet_ids,
    module.vnet_b.subnet_ids
  ]

  all_subnets = flatten(local.all_subnets_nested)
}

Why this matters: without flatten, Terraform sees a nested list and freaks out when you try to iterate over it.

resource "azurerm_network_interface_security_group_association" "nsg_binding" {
  for_each = toset(local.all_subnets)
  ...
}

Boom — now you’re good. NSGs get attached across all subnets without a hitch.


Real-Life Example 2: Role Assignments Across Different Scopes

Maybe you’re managing RBAC across both subscription and resource group levels. You’ve got two separate lists of roles, and now you want to unify them before looping through.

locals {
  subscription_roles = [
    {
      principal_id = "xyz"
      role         = "Reader"
      scope        = "/subscriptions/abc"
    }
  ]

  rg_roles = [
    {
      principal_id = "xyz"
      role         = "Contributor"
      scope        = "/subscriptions/abc/resourceGroups/rg-dev"
    }
  ]

  combined_roles = flatten([local.subscription_roles, local.rg_roles])
}

resource "azurerm_role_assignment" "assign" {
  for_each = {
    for idx, role in local.combined_roles :
    "${idx}" => role
  }

  principal_id         = each.value.principal_id
  role_definition_name = each.value.role
  scope                = each.value.scope
}

This is super handy when you want your logic to treat roles across scopes uniformly. flatten glues the pieces together.


Some Gotchas and Tips

🔍 Don’t Flatten Too Early

Sometimes you actually need those nested structures — like when you’re grouping subnets by VNet. If you flatten too soon, you lose that grouping and your logic might go sideways.

🛠️ Don’t Use flatten as a Fix-All

Terraform throwing a type error? Resist the urge to slap flatten on it blindly. It might mask the real issue. Fix the data structure first — then flatten only when you need a flat list at the point of consumption.

🎯 Combine with for Expressions

This is where flatten really shines:

flatten([for vnet in local.vnets : vnet.subnets])

It keeps your logic neat, especially when building up values from multiple nested loops.

⚠️ Be Careful with null

flatten drops null values. So if null means something important (like skipping a resource), you’ll want to handle that before flattening — maybe using compact() or conditional logic.


My Take as an Architect

In big Azure environments with a lot of moving parts, flatten is like a utility knife. It helps you clean up data structures so your modules and resources can work together smoothly. It’s especially useful when you’re consuming outputs from reusable modules or combining logic from different parts of your codebase.

That said, flattening isn’t always the right move. If you flatten too aggressively, you can lose track of relationships — like which subnet belongs to which VNet — and that can lead to bugs that are hard to trace.

Use it at the edges, close to where your data is being consumed. Keep the rich structure where it’s helpful, and simplify only when needed.


Wrapping Up

Terraform’s flatten might look like a small helper, but in practice, it solves big headaches. Especially in Azure, where nested configurations are everywhere, it helps you wrangle data into shape so Terraform doesn’t trip up.

The key? Don’t just know how to use it — understand why and when to use it. That’s what separates a working solution from a truly clean, scalable one.

If you’re building real-world infrastructure with Terraform and Azure, flatten isn’t optional — it’s part of your core toolkit.

 

Leave a comment