It sounds like this problem decomposes into two smaller problems:
- Determine the availability zone of each of the subnets.
- For each distinct availability zone, choose any one of the subnets that belongs to it. (I'm assuming here that there is no reason to prefer one subnet over another if both are in the same AZ.)
For step one, if we don't already have the subnets in question managed by the current configuration (which seems to be the case here -- you are receiving them from an input variable) then we can use the aws_subnet
data source to read information about a subnet given its ID. Because you have more than one subnet here, we'll use resource for_each
to look up each one.
data "aws_subnet" "public" {
for_each = toset(var.public_subnets)
id = each.key
}
The above will make data.aws_subnet.public
appear as a map from subnet id to subnet object, and the subnet objects each have availability_zone
attributes specifying which zone each subnet belongs to. For our second step it's more convenient to invert that mapping, so that the keys are availability zones and the values are subnet ids:
locals {
availability_zone_subnets = {
for s in data.aws_subnet.public : s.availability_zone => s.id...
}
}
The above is a for
expression, which in this case is using the ...
suffix to activate grouping mode, because we're expecting to find more than one subnet per availability zone. As a result of this, local.availability_zone_subnets
will be a map from availability zone name to a list of one or more subnet ids, like this:
{
"az1-a" = ["subnetid1", "subnetid4"]
"az1-b" = ["subnetid2"]
"az1-c" = ["subnetid3"]
}
This gets us the information we need to implement the second part of the problem: choosing any one of the elements from each of those lists. The easiest definition of "any one" is to take the first one, by using [0]
to take the first element.
resource "aws_elb" "loadbalancer" {
depends_on = [aws_autoscaling_group.private_ec2]
name = "loadbalancer-terraform"
subnets = [for subnet_ids in local.availability_zone_subnets : subnet_ids[0]]
listener {
instance_port = 80
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
}
There are some caveats of the above solution which are important to consider:
Taking the first element of each list of subnet ids means that the configuration could potentially be sensitive to the order of elements in var.public_subnets
, but this particular combination above implicitly avoids that with the toset(var.public_subnets)
in the initial for_each
, which discards the original ordering of var.public_subnets
and causes all of the downstream expressions to order the results by a lexical sort of the subnet ids. In other words, this will choose the subnet whose id is the "lowest" when doing a lexical sort.
I don't really like it when that sort of decision is left implicit, because it can be confusing to future maintainers who might change the design and be surprised to see it now choosing a different subnet for each availability zone. I can see a couple different ways to mitigate that, and I'd probably do both if I were writing a long-lived module:
Make sure variable "public_subnets"
has type = set(string)
for its type constraint, rather than type = list(string)
, to be explicit that this module discards the ordering of the subnets as given by the caller. If you do this, you can change toset(var.public_subnets)
to just var.public_subnets
, because it will already be a set.
In the final for
expression to choose the first subnet for each availability zone, include an explicit call to sort
. This call is redundant with how the rest of this is implemented in my example, but I think it's a good clue to a future reader that it's using a lexical sort to decide which of the subnets to use:
subnets = [
for subnet_ids in local.availability_zone_subnets : sort(subnet_ids)[0]
]
Neither of those changes will actually affect the behavior immediately, but additions like this can be helpful to future maintainers as they read a module they might not be previously familiar with, so they don't need to read the entire module to understand a smaller part of it.