Terraform
flatten Function
flatten takes a list and replaces any elements that are lists with a
flattened sequence of the list contents.
Examples
> flatten([["a", "b"], [], ["c"]])
["a", "b", "c"]
If any of the nested lists also contain directly-nested lists, these too are flattened recursively:
> flatten([[["a", "b"], []], ["c"]])
["a", "b", "c"]
Indirectly-nested lists, such as those in maps, are not flattened.
Flattening nested structures for for_each
The
resource for_each
and
dynamic block
language features both require a collection value that has one element for
each repetition.
Sometimes your input data structure isn't naturally in a suitable shape for
use in a for_each argument, and flatten can be a useful helper function
when reducing a nested data structure into a flat one.
For example, consider a module that declares a variable like the following:
variable "networks" {
  type = map(object({
    cidr_block = string
    subnets    = map(object({ cidr_block = string }))
  }))
  default = {
    "private" = {
      cidr_block = "10.1.0.0/16"
      subnets = {
        "db1" = {
          cidr_block = "10.1.0.1/16"
        }
        "db2" = {
          cidr_block = "10.1.0.2/16"
        }
      }
    },
    "public" = {
      cidr_block = "10.1.1.0/16"
      subnets = {
        "webserver" = {
          cidr_block = "10.1.1.1/16"
        }
        "email_server" = {
          cidr_block = "10.1.1.2/16"
        }
      }
    }
    "dmz" = {
      cidr_block = "10.1.2.0/16"
      subnets = {
        "firewall" = {
          cidr_block = "10.1.2.1/16"
        }
      }
    }
  }
}
The above is a reasonable way to model objects that naturally form a tree, such as top-level networks and their subnets. The repetition for the top-level networks can use this variable directly, because it's already in a form where the resulting instances match one-to-one with map elements:
resource "aws_vpc" "example" {
  for_each = var.networks
  cidr_block = each.value.cidr_block
}
However, in order to declare all of the subnets with a single resource
block, we must first flatten the structure to produce a collection where each
top-level element represents a single subnet:
locals {
  # flatten ensures that this local value is a flat list of objects, rather
  # than a list of lists of objects.
  network_subnets = flatten([
    for network_key, network in var.networks : [
      for subnet_key, subnet in network.subnets : {
        network_key = network_key
        subnet_key  = subnet_key
        network_id  = aws_vpc.example[network_key].id
        cidr_block  = subnet.cidr_block
      }
    ]
  ])
}
resource "aws_subnet" "example" {
  # local.network_subnets is a list, so we must now project it into a map
  # where each key is unique. We'll combine the network and subnet keys to
  # produce a single unique key per instance.
  for_each = tomap({
    for subnet in local.network_subnets : "${subnet.network_key}.${subnet.subnet_key}" => subnet
  })
  vpc_id            = each.value.network_id
  availability_zone = each.value.subnet_key
  cidr_block        = each.value.cidr_block
}
The above results in one subnet instance per subnet object, while retaining the associations between the subnets and their containing networks.
Related Functions
- setproductfinds all of the combinations of multiple lists or sets of values, which can also be useful when preparing collections for use with- for_eachconstructs.