Reputation: 175
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
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 .
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.
Upvotes: 11
Views: 9698
Reputation: 74694
It sounds like this problem decomposes into two smaller problems:
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.
Upvotes: 20