Skip to main content
  1. blog/
  2. Flattening is Happening/

Part 2: Time to get flat

5 mins
Terraform Data-Structures Iac
HCL Nested Fors - This article is part of a series.
Part 2: This Article

flatten 101
#

This article drills into the practical usage of Terraform’s flatten function. Before we start, let’s quickly recap its theoretical definition:


flatten takes a list and replaces any elements that are lists with a flattened sequence of the list contents.
Hashicorp Documentation

As we saw in part 1, Terraform developers often need to receive nested input to dynamically create zero-to-many resources (or sub-resources), specifically by using either

  • the for_each meta-argument, or
  • a dynamic block.

Because iterators in Terraform are not logical operators, they aren’t great at handling “multi-dimensional” nested input directly. For instance, if you need multiple subnets, you should provide a single flat list rather than separate nested lists for public and private subnets:

locals {
  # Good
  subnets = [
    { name = string, cidr = string },
    { name = string, cidr = string },
    { name = string, cidr = string }
  ]

  # Not good
  bad_subnets = [
    [{ name = string, cidr = string }, { name = string, cidr = string }],
    [{ name = string, cidr = string }]
  ]
}

When using the for_each meta-argument, each instance of a [resource | data source | module | etc] requires a unique ‘key’ in string format. for_each collections behave like hash tables, so that key acts as the item’s index.

  • This is why for_each requires either a list of known-before-apply strings, or a list of key-value objects (i.e. key => value expressions.)

When composing lists of “two-dimensional” objects, ensure each object includes a unique string attribute suitable for identifying resources easily in the Terraform state file. Usually, the object’s name attribute suffices, but this might differ based on your use case.

TL;DR
#

flatten can be applied in several contexts, but it’s most commonly (by a lot) used to normalize nested lists for use with for_each.

With this in mind, let’s go back to our fruit salad.


Fruit Kebabs
#

We’ll reference some content from the last post, so to save you the trouble of tabbing back and forth here’s the relevant part.

Quick Recap
#

In part 1, we introduced a scenario where we needed to action a group of fruits as described in the following YAML snippet:

colors:
  - color: red
    flavor: sweet
    fruits:
      - name: strawberry
        size: small
      - name: apple
        size: big
  - color: green
    flavor: tart
    fruits:
      - name: grape
        size: small
      - name: watermelon
        size: big

Using nested for expressions, we converted that YAML into nested tuples:

# Explicitly defined version of the final value of local.colored_fruits:
locals{
  flattened_for_output = [
    [
      { 
        name        = "strawberry"
        size        = "small"
        color       = "red"
        flavor      = "sweet"
        description = "This small, red strawberry tastes sweet." 
      },
      {       
        name        = "apple"
        size        = "big"
        color       = "red"
        flavor      = "sweet"
        description = "This big, red apple tastes sweet."
      }
    ],
    [
      { 
        name        = "grape"
        size        = "small"
        color       = "green"
        flavor      = "tart"
        description = "This small, green grape tastes tart."
      },
      {
        name   = "watermelon"
        size   = "big"
        color  = "green"
        flavor = "tart"
        # TODO: Verify sweetness of watermelons.
        description = "This big, green watermelon tastes tart."
      } 
    ]
  ]
}

Moving on
#

Suppose we want to make multiple resources (in this case, four of them) called fruit_chunk. The fruit_chunk resource (from our fictitious tastycorp/fruit provider) has the following arguments:

NameDescriptionTypeDefault
nameCommon name of the fruitstringrequired
colorRough color of the fruit’s exteriorstringrequired
flavorPrimary flavor profile of the fruitstringrequired
largeWhether the fruit is bigboolrequired
descriptionFriendly sentence talking about the fruitstring“Hey, look at that!”

To create zero-to-many fruit chunks for a kebab, we can use a for_each block:

resource "fruit_chunk" "kebab" {
  # The key, or effective 'index', of each fruit_chunk in the kebab is its name.
  # This means we assume name is unique.
  for_each = { for fruit in local.fruits: fruit.name => fruit }
  # These strings are all direct attributes of the object.
  name        = each.value.name
  color       = each.value.color
  flavor      = each.value.flavor
  description = each.value.description
  # Normalize to permit either 'large' or 'big' as a descriptor of a big, yummy fruit
  large = contains(["large", "big"], each.value.size)
}

Eagle-eyed observers will note that this for_each argument requires a list of objects.

I wish I could say this grand reveal is complicated and flashy, but it’s not:

locals{
  fruits = flatten(local.colored_fruits)
}

That simple flatten statement will convert a nested list structure ([[obj, obj], [obj, obj]]) to a flat one ([obj, obj, obj, obj]) in one go. The flattened version is suitable for a for_each meta-argument.


A last note on for_each
#

Let’s review the for_each structure: for_each = { for fruit in local.fruits: fruit.name => fruit }

  • The opening { indicates a map of key-value pairs.
  • for fruit in local.fruits defines the iterator. This iterator is scoped to the expression: it is mentioned after the : in the for statement, and might not not be consumed in the resource at all.
  • fruit.name => fruit specifies that:
    • The key is the name attribute of each fruit (an object within the collection local.fruits)
    • The value (accessible as each.value in the resource) is the entire fruit object, including the name.

With that, we’re done with our fruit salad example! Now it’s time to explore some real-world scenarios.

HCL Nested Fors - This article is part of a series.
Part 2: This Article