Skip to content

Instantly share code, notes, and snippets.

@BennyG93
Created October 14, 2019 12:58
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save BennyG93/4b7953c6b351f1e726ca33d1d4efca1a to your computer and use it in GitHub Desktop.
Save BennyG93/4b7953c6b351f1e726ca33d1d4efca1a to your computer and use it in GitHub Desktop.
How to produce dynamic resources in terraform 12 from 2 lists

Producing dynamic resources in Terraform 12 from 2 lists

Now that Terraform 12 has been released and iterated on a few times, the highly anticipated for_each argument has officially been integrated directly inside the resource and data blocks. Allowing for the creation of dynamic blocks.

This for_each resource loop accepts any map or set to produce its dynamic set of resources.

e.g.

Map (A collection of values where each is identified by a unique string label):

for_each = {
  a_group = "eastus"
  another_group = "westus2"
}

Set (A collection of unique values that do not have any secondary identifiers or ordering):

for_each = ["a", "b", "c"]

When life gets more complex than the examples

The above examples are great if you have a single list or a flat map structure, but but sometimes we are faced with challenges that go beyond what the examples show us.

As a real world example, I found myself needing to create a Terraform module to manage creating Transit Gateway Attachments in AWS between various VPCs and create all the required network routes between those VPCs. The module would need to be able to accept 2 lists, one list of route table IDs and a second list of destination CIDR addresses. I needed a dynamic and flexible solution as the number routes created would vary between the VPCs.

For example, in one VPC I may want to create routes to 3 different VPCs and add the new routes to 4 different route tables within the source VPC. In this example I would need to create 4 * 3 = 12 new route resources. The data set would look something like this:

Route Table ID Destination CIDR
Route_tbl_1 10.1.0.0/16
Route_tbl_1 10.2.0.0/16
Route_tbl_1 10.3.0.0/16
Route_tbl_2 10.1.0.0/16
Route_tbl_2 10.2.0.0/16
Route_tbl_2 10.3.0.0/16
Route_tbl_3 10.1.0.0/16
Route_tbl_3 10.2.0.0/16
Route_tbl_3 10.3.0.0/16
Route_tbl_4 10.1.0.0/16
Route_tbl_4 10.2.0.0/16
Route_tbl_4 10.3.0.0/16

With this information in mind, we should now be able to see how the AWS examples above do not quite fit the data set due to the amount of the duplicated values. We cannot use a set because we need to store multiple values in each loop. We cannot use a flat structure map because every key must be unique, e.g. this is invalid:

{
  Route_tbl_1 = "10.1.0.0/16"
  Route_tbl_1 = "10.2.0.0/16"
  Route_tbl_2 = "10.1.0.0/16"
  Route_tbl_3 = "10.1.0.0/16"
}

And for clarity... there was no way I wanted to manage a complex, hand written map of routes for every VPC (at the time we had around 20 different VPCs)

Terraform and looping

In order to get all the information I needed from the 2 lists in order to create a valid data structure for the for_each argument I needed to take a deep dive into Terraforms looping capabilities, and sometimes I felt I was pushing it to its limits.

My first attempt seemed logical to me, within a local var, I would create a nested loop which would iterate over every item in the 2 lists one by one and create a map of unique pairs.

Example code:

locals {
  route_dest_pairs = flatten([
    for route in var.route_tables : [
      for dest in var.destination_cidr_blocks: {
        route = route
        dest  = dest
      }
    ]
  ])
}

Example output:

route_dest_pairs = [
    {
        route = route_tbl_1,
        dest = 10.1.0.0/16
    },
    {
        route = route_tbl_1,
        dest = 10.2.0.0/16
    },
    {
        route = route_tbl_2,
        dest = 10.1.0.0/16
    },
    {
        route = route_tbl_3,
        dest = 10.1.0.0/16
    }
]

However it was extremely frustrating to find out at this point that although this output is a list of unique maps, it was not a valid data structure for the for_each resource loop. After this realisation, I spent more time than I'm willing to admit trying to create the correct data structure that I needed from within this local var. I must have produced lists of maps of maps, maps of lists of maps and everything in between... I was exhausted.

The type of data structure I needed was a map of maps which at the top level had a unique key identifier and nested underneath would be the route table and destination CIDR pairs. The reason for this structure is because as Terraform dynamically creates resources, it needs to create a unique label to identify that specific resource. Terraform uses the key value in order to create that label, as by the definition of a map, the key should always be unique, and in all of my attempts, I was never able to produce an output which had a unique key value.

Desired example output:

variable_name = {
    unique_value_1 = {
      label1 = "foo"
      label2 = "bar"
    }
    unique_value_2 = {
      label1 = "baz"
      label2 = "qux"
    }

The main problem I was facing at this point was trying to create the unique identifier at the top level of the map. Using only the information I had available from the 2 lists I knew I could create a unique name by combining the 2 nest values, e.g. rtb_cidr, as they would always be unique pairs. However I needed to make this label at the top level of the map, which means creating it within the first loop of the for loop used to create the structure, and at that point in my function above, I did not have access to both items from the 2 lists, at this point I would still only be iterating through the first list.

After many attempts I realised the only answer to this was to take the incorrect list of maps output, and then pass that through another local var for loop, so use 2 separate loops. The result of the first loop function would produce a data structure which would allow me to access both pieces of the puzzle within a single loop. That second loop function would iterate through the 2 route and dest values available within every object iteration and combine them to create a unique label used as map key.

Double looping example:

locals {
  route_dest_pairs = flatten([
    for route in var.route_tables : [
      for dest in var.destination_cidr_blocks: {
        route = route
        dest  = dest
      }
    ]
  ])
}

locals {
  route-association-map = {
    for obj in local.route_dest_pairs : "${obj.route}_${obj.dest}" => obj
  }
}

Finally after learning my lesson that not all problems can be solved within a single loop function, I was able to produce the data structure I had longed for:

route-association-map = {
    route_tbl_1_10.1.0.0/16 = {
      route = route_tbl_1
      dest  = 10.1.0.0/16
    }
    route_tbl_1_10.2.0.0/16 = {
      route =  route_tbl_1
      dest  = 10.2.0.0/16
    }
    route_tbl_2_10.1.0.0/16 = {
      route =  route_tbl_2
      dest  = 10.1.0.0/16
    }
    route_tbl_3_10.1.0.0/16 = {
      route =  route_tbl_3
      dest  = 10.1.0.0/16
    }
}

In order to access these values now and create the dynamic resource we would need to use each.value.route and each.value.dest

The grand finale:

resource "aws_route" "tgw-routes" {
  for_each = local.route-association-map

  route_table_id         = each.value.route
  destination_cidr_block = each.value.dest
  transit_gateway_id     = var.transit_gateway_id
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment