1
votes

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.

1

1 Answers

2
votes

It sounds like this problem decomposes into two smaller problems:

  1. Determine the availability zone of each of the subnets.
  2. 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.