Question:
When trying to create elb(classic load balancer) in AWS via terraform, I am sending a list of public subnet ids that were created from another module. In this case I have 4 subnets which are spanned across 3 az’s. I have 2 subnets from az-1a when I am trying to run the terraform , I get an error saying same az can't be used twice for ELB
1 2 3 4 5 6 7 8 9 10 11 12 13 |
resource "aws_elb" "loadbalancer" { name = "loadbalancer-terraform" subnets = var.public_subnets listener { instance_port = 80 instance_protocol = "http" lb_port = 80 lb_protocol = "http" } depends_on = [aws_autoscaling_group.private_ec2] } |
Is there any way where I can select subnets from the given list in such a way I can only get subnet id’s from distinct AZ’s .
1 2 3 4 5 |
subnetid1 -- az1-a subnetid2 -- az1-b subnetid3 -- az1-c subnetid4 -- az1-a |
now I need to get an output either subnet-1,2 and 3 or subnet-2,3 and 4.
Answer:
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.
1 2 3 4 5 6 |
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:
1 2 3 4 5 6 |
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:
1 2 3 4 5 6 |
{ "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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 thetoset(var.public_subnets)
in the initialfor_each
, which discards the original ordering ofvar.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"
hastype = set(string)
for its type constraint, rather thantype = 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 changetoset(var.public_subnets)
to justvar.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 tosort
. 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:
1234subnets = [for subnet_ids in local.availability_zone_subnets : sort(subnet_ids)[0]]
- Make sure
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.