如何从 Terraform 中的所有可用区域中选择不同的子网

How to pick distinct subnets from all the available zones in terraform

当尝试通过 terraform 在 AWS 中创建 elb(经典负载均衡器)时,我发送了从另一个模块创建的 public 子网 ID 的列表。在这种情况下,我有 4 个子网,它们跨越 3 个可用区。当我尝试 运行 terraform 时,我有 2 个来自 az-1a 的子网,我收到一条错误消息 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]
}

有什么方法可以从给定列表中 select 子网,这样我只能从不同的 AZ 获取子网 ID。

subnetid1 -- az1-a
subnetid2 -- az1-b
subnetid3 -- az1-c
subnetid4 -- az1-a

现在我需要获得子网 1,2 和 3 或子网 2,3 和 4 的输出。

听起来这个问题可以分解成两个更小的问题:

  1. 确定每个子网的可用区。
  2. 对于每个不同的可用性区域,选择属于它的任何一个子网。 (我在这里假设如果两个子网都在同一个 AZ 中,则没有理由优先选择另一个子网。)

对于第一步,如果我们还没有由当前配置管理的相关子网(这里似乎是这种情况——您正在从输入变量中接收它们),那么我们可以使用 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 查找每一个。

data "aws_subnet" "public" {
  for_each = toset(var.public_subnets)

  id = each.key
}

以上将使 data.aws_subnet.public 显示为从子网 ID 到子网对象的映射,每个子网对象都有 availability_zone 属性指定每个子网属于哪个区域。对于我们的第二步,反转该映射更方便,因此键是可用区,值是子网 ID:

locals {
  availability_zone_subnets = {
    for s in data.aws_subnet.public : s.availability_zone => s.id...
  }
}

上面是一个for expression,在本例中是使用...后缀来激活分组模式,因为我们希望找到每个可用性区域有多个子网。因此,local.availability_zone_subnets 将是一个从可用区名称到一个或多个子网 ID 列表的映射,如下所示:

{
  "az1-a" = ["subnetid1", "subnetid4"]
  "az1-b" = ["subnetid2"]
  "az1-c" = ["subnetid3"]
}

这为我们提供了实现问题第二部分所需的信息:从每个列表中选择任何一个元素。 “任何一个”最简单的定义是取第一个,使用 [0] 取第一个元素。

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"
  }
}

上述解决方案有一些重要的注意事项需要考虑:

  • 获取每个子网 ID 列表的第一个元素意味着配置可能对 var.public_subnets 中元素的顺序敏感,但是这个 特殊 [=上面的 72=] 组合隐含地避免了初始 for_each 中的 toset(var.public_subnets),它丢弃了 var.public_subnets 的原始排序并导致所有下游表达式按词法排序对结果进行排序的子网 ID。换句话说,这将在进行词法排序时选择 ID 为“最低”的子网。

    我真的不喜欢这种隐含的决定,因为这可能会让未来的维护者感到困惑,他们可能会更改设计并惊讶地看到它现在为每个可用区选择不同的子网。我可以看到几种不同的方法来缓解这种情况,如果我正在编写 long-lived 模块,我可能会同时使用这两种方法:

    • 确保 variable "public_subnets" 具有 type = set(string) 作为其类型约束,而不是 type = list(string),明确表示此模块丢弃给定的子网顺序由来电者。如果这样做,您可以将 toset(var.public_subnets) 更改为 var.public_subnets,因为它已经是一个集合。

    • 在最后的 for 表达式中为每个可用区选择第一个子网,包括对 sort 的显式调用。这个调用与我的示例中其余部分的实现方式是多余的,但我认为这是未来 reader 的一个很好的线索,它使用词法排序来决定使用哪个子网:

      subnets = [
        for subnet_ids in local.availability_zone_subnets : sort(subnet_ids)[0]
      ]
      

这些更改实际上都不会立即影响行为,但像这样的添加可能对未来的维护者有帮助,因为他们阅读以前可能不熟悉的模块,因此他们不需要阅读整个模块了解其中的一小部分。