Count conditional with loop

Hi guys.

I’m trying to create a few routes with aws_route for my VPC Peering connection.

resource "aws_route" "vpc_peering_from_dev_test_to_prod" {
  count  = terraform.workspace == "dev" || terraform.workspace == "test" ? 1 : 0
  route_table_id            = tolist(data.aws_route_tables.private_foo[count.index].ids)[count.index]
  destination_cidr_block    = data.terraform_remote_state.network_prod.outputs.vpc_cidr_block
  vpc_peering_connection_id = aws_vpc_peering_connection.test_dev_peering_vpc[count.index].id
}

However, there are more than one route table that will require the new route. How can I introduce a loop with the count condition above?

Terraform documentation [1] recommends the following:

count                     = length(data.aws_route_tables.rts.ids)

I’ve tried for_each but was unsuccessful:

for_each = terraform.workspace == "dev" || terraform.workspace == "test" ? {
  count = length(data.aws_route_tables.private_foo[count.index].ids)
  } : 0

[1] Terraform Registry

Looks like I’m getting there, but am facing a different issue now.

resource "aws_route" "vpc_peering_from_dev_test_to_prod" {
  for_each = terraform.workspace == "dev" || terraform.workspace == "test" ? {
     for s in data.aws_route_tables.private_foo : jsonencode(s.ids) => {
       ids   = s.ids
     }
   } : {}
  route_table_id            = jsonencode(each.value.ids)
  destination_cidr_block    = data.terraform_remote_state.network_prod.outputs.vpc_cidr_block
  vpc_peering_connection_id = aws_vpc_peering_connection.test_dev_peering_vpc[count.index].id
}

But that’s not right, because it isn’t looping:

+ resource "aws_route" "vpc_peering_from_dev_test_to_prod" {
      + destination_cidr_block    = "10.0.0.0/18"
      + id                        = (known after apply)
      + instance_id               = (known after apply)
      + instance_owner_id         = (known after apply)
      + network_interface_id      = (known after apply)
      + origin                    = (known after apply)
      + route_table_id            = jsonencode(
            [
              + [
                  + "rtb-00ccd032ef75a6837",
                  + "rtb-01f07b5a962e8e61c",
                  + "rtb-06b47581daaed5bd5",
                ],
            ]
        )
      + state                     = (known after apply)
      + vpc_peering_connection_id = "pcx-072ea97151ec6fd1b"
    }

You are looping over data.aws_route_tables.private_foo but that’s a single data resource.

I think you actually intend to be looping over data.aws_route_tables.private_foo.ids - a list which can be accessed from that one data resource.

Hmm, sorry, I do not know what you mean. Could you provide an example, please? Thanks

Untested, because I don’t have any experience with AWS nor an environment to test with, but probably something like this:

resource "aws_route" "vpc_peering_from_dev_test_to_prod" {
  for_each = (
    (terraform.workspace == "dev" || terraform.workspace == "test") ?
    data.aws_route_tables.private_foo.ids : []
  )
  route_table_id            = each.value
  destination_cidr_block    = data.terraform_remote_state.network_prod.outputs.vpc_cidr_block
  vpc_peering_connection_id = aws_vpc_peering_connection.test_dev_peering_vpc[0].id
}

Note that I had to make some guesses about the rest of your config. In your previous code snippet you were referring to count.index despite no longer having a count configured on your resource … I’m surprised that didn’t cause an error.

1 Like

The requirement for for_each is to provide a mapping that has one element for each instance you want to declare. You can therefore declare no instances by providing an empty mapping.

In your case you seem to have what is effectively a list of objects which each contain a set of ids. The top-level list is created automatically by Terraform to handle the dynamic number of data.aws_route_tables.private_foo indices (zero or one, in your case) and the nested set is part of the implementation of the aws_route_tables data source.

I think that makes this a good candidate for Flattening nested structures for for_each in the flatten function’s documentation, although this particular application of it can be simplified a bit compared to the more complex example in the documentation:

  for_each = toset(flatten(data.aws_route_tables.private_foo[*].ids))

Breaking this down into smaller steps:

  • data.aws_route_tables.private_foo[*].ids returns a list of all of the values of ids across all elements of data.aws_route_tables.private_foo. Because ids is a set(string) value, the result here is a list of sets of strings.
  • flatten(...) turns that list of sets of strings into a flat list of strings, by eliminating the intermediate sets.
  • The wrapping toset(...) turns that list of strings back into a set of strings in order to meet the requirements of for_each.

You will therefore end up declaring one instance of aws_route for each distinct route table, which I think is what you intended to do here.

1 Like

Thanks, guys!

@apparentlymart 's suggestions worked. Cheers!