TravelingLex
TravelingLex

Reputation: 477

Reference count resources within a for_each loop

I'm currently trying to create aws_route resources leveraging the for_each function and associate these routes with a number of aws_route_table resources that are built using the count function. I was wondering if there was a way for me to still use the for_each function within the aws_route resource and reference the aws_route_table resources.

My end goal is to create and apply ~5 routes to ~7 route tables.

Here is my code for reference:

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.vpc.id
  count  = length(split(",", var.private_subnets))

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = join(",", aws_nat_gateway.nat.*.id)
  }

  route {
    cidr_block = var.stage_vpc_cidr
    vpc_peering_connection_id = var.stage_peering_connection_id
  }

  route {
    cidr_block = var.production_vpc_cidr
    vpc_peering_connection_id = var.production_peering_connection_id
  }

  tags = {
    Name        = "${var.name}-private.${element(split(",", var.azs), count.index)}"
    Department  = var.tag_department
    Lifecycle   = var.tag_lifecycle
    Account     = var.tag_account
    Application = var.tag_application
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route" "vpn-route" {
  for_each = toset(var.vpn_subnets)
  route_table_id = element(aws_route_table.private.*.id, count.index)
  gateway_id = var.vpn_gateway_id
  destination_cidr_block = each.value
}

and my variable

variable "vpn_subnets" {
  type = list
  default = ["192.168.0.0/24","192.168.2.0/24","192.168.4.0/24","192.168.5.0/24","192.168.10.0/24","192.168.253.0/24"]
}

Upvotes: 0

Views: 747

Answers (1)

Martin Atkins
Martin Atkins

Reputation: 74064

It sounds like your requirement is to have one aws_route instance for each combination of route table instance and var.vpn_subnets element.

The operation of finding all of the combinations of elements of two collection is called the Cartesian Product and Terraform has the setproduct function to calculate that.

locals {
  route_table_vpn_subnets = [
    for pair in setproduct(aws_route_table.private, var.vpn_subnets) : {
      route_table_name       = pair[0].tags.Name
      route_table_id         = pair[0].id
      destination_cidr_block = pair[1]
    }
  ]
}

This is using setproduct in combination with a for expression to produce a series of objects that described all of the combinations of route table ID and destination CIDR block, like this:

[
  {
    route_table_name       = "example-private.0"
    route_table_id         = "rtb-12323453"
    destination_cidr_block = "192.168.0.0/24"
  },
  {
    route_table_name       = "example-private.0"
    route_table_id         = "rtb-12323453"
    destination_cidr_block = "192.168.2.0/24"
  },
  # ...
  {
    route_table_name       = "example-private.1"
    route_table_id         = "rtb-abcd"
    destination_cidr_block = "192.168.0.0/24"
  },
  {
    route_table_name       = "example-private.1"
    route_table_id         = "rtb-abcd"
    destination_cidr_block = "192.168.2.0/24"
  },
  # ...
]

This data structure now meets two key requirements for for_each:

  • There's one element of this collection per instance we want to create.

  • Each object has a set of attributes that can together serve as a unique identifier built from values decided in the configuration, as opposed to values decided by the remote system.

    In this case, that's served by including both route_table_name and route_table_id, because the name is chosen by your configuration while the id is chosen by the remote system, and so the id would not be suitable for use as an instance identifier.

To use it in for_each we just need to do one more step to project it into a map where those unique attribute values are combined into unique string keys:

resource "aws_route" "vpn" {
  for_each = {
    for obj in local.route_table_vpn_subnets :
    "${obj.route_table_name}:${obj.destination_cidr_block}" => obj
  }

  route_table_id         = each.value.route_table_id
  gateway_id             = var.vpn_gateway_id
  destination_cidr_block = each.value.destination_cidr_block
}

The key expression in the for expression here is telling Terraform to assign these instances addresses like the following, which should meet the requirement of being unique across all instances:

  • aws_route.vpn["example-private.0:192.168.0.0/24"]

  • aws_route.vpn["example-private.0:192.168.0.2/24"]

  • ...

  • aws_route.vpn["example-private.1:192.168.0.0/24"]

  • aws_route.vpn["example-private.1:192.168.0.2/24"]

  • ...


Note that, because you're using numeric indices for your route tables, if you change the number of elements in the split of var.private_subnets then that's likely to cause the route tables to get renumbered. Because the number is part of the name and the name is the part of the unique key of each route instance, that'll also cause the route instances to be renumbered, which I mention only because a typical reason for using for_each is to avoid that sort of renumbering.

There's likely a different design you could follow where you describe your route tables using a collection of objects that meets the two criteria for for_each I described above, so that the route table instances can also be declared using for_each with meaningful unique keys, but that'd be a pretty significant change outside of the scope of what you asked and so I won't go into the details here.

Upvotes: 2

Related Questions