Terraform `for_each` 精讲:优雅地自动化多域名证书验证

发布于:2025-07-09 ⋅ 阅读:(18) ⋅ 点赞:(0)

大家好,在云原生和自动化运维的世界里,Terraform无疑是基础设施即代码(IaC)领域的王者。它强大的声明式语法让我们能够轻松地描述和管理云资源。然而,即使是经验丰富的工程师,在面对某些动态场景时也可能会遇到挑战。

今天,我们就来深入探讨一个非常经典且实用的场景:如何使用 for_each 表达式,优雅地自动化处理 AWS ACM (Certificate Manager) 证书的 DNS 验证,特别是当一个证书包含多个域名时。在这里插入图片描述

问题的提出:多域名证书的DNS验证之痛

在现代Web应用中,一个服务拥有多个访问域名是非常普遍的。例如,主域名 test.core.org,以及一系列子域名如 admin-test.core.orgagent-test.core.org 等。为了启用HTTPS,我们需要为所有这些域名申请一个SSL/TLS证书。AWS ACM提供了一个便捷的方式,就是将这些域名都作为“主题备用名称”(Subject Alternative Names, SANs)添加到一个证书中。

让我们来看一下申请这样一个证书的Terraform代码:

resource "aws_acm_certificate" "cloudfront" {
  provider          = aws.cloudfront
  domain_name       = "test.core.org"
  validation_method = "DNS"

  subject_alternative_names = [
    "admin-test.core.org",
    "agent-test.core.org",
    "mmts-test.core.org",
    "static-test.core.org"
  ]

  tags = {
    Environment = "test"
  }

  lifecycle {
    create_before_destroy = true
  }
}

代码很直观,我们定义了一个主域名和四个备用域名。我们选择了 DNS 作为验证方法,这意味着AWS会为 每一个 域名(包括主域名和所有SANs)生成一个唯一的CNAME记录,我们需要将这些记录发布到我们的DNS服务商(比如AWS Route 53)的DNS区域(Zone)中,以证明我们对这些域名的所有权。

问题来了:AWS为5个域名生成了5组不同的验证信息。我们该如何为这5个域名动态创建对应的DNS记录呢?手动创建?当然不行,这违背了IaC的初衷。写5个 aws_route53_record 资源块?代码冗余且难以维护。

这时,for_each 就该闪亮登场了。

解决方案:for_each 的魔法

for_each 是 Terraform 0.12.6 版本引入的重大特性,它允许我们基于一个 map 或 set of strings 来创建多个相似资源的实例。这正是解决我们问题的完美工具。

当我们创建 aws_acm_certificate 资源后,Terraform会通过其 domain_validation_options 属性导出一组对象,其中包含了每个域名验证所需的所有信息。 这是一个列表(list),每个对象包含 domain_nameresource_record_nameresource_record_typeresource_record_value 等关键信息。

我们的目标就是遍历这个列表,为每个元素创建一个 aws_route53_record 资源。

下面就是使用了 for_each 的解决方案代码,也是本次讲解的核心:

data "aws_route53_zone" "selected" {
  name         = "core.org."
  private_zone = false
}

resource "aws_route53_record" "cloudfront_validation" {
  # for_each 的魔法在这里!
  for_each = {
    for dvo in aws_acm_certificate.cloudfront.domain_validation_options : dvo.domain_name => dvo
  }

  zone_id = data.aws_route53_zone.selected.zone_id
  name    = each.value.resource_record_name
  type    = each.value.resource_record_type
  records = [each.value.resource_record_value]
  ttl     = 60
}

让我们来逐行解析这段代码的精髓。

深入解析 for_each 表达式

for_each 的核心在于它接受一个 map 类型的值。然而,aws_acm_certificate.cloudfront.domain_validation_options 是一个 list of objects。因此,我们需要使用Terraform的 for 表达式将其转换成一个 map。

{
  for dvo in aws_acm_certificate.cloudfront.domain_validation_options : dvo.domain_name => dvo
}

这行代码做了什么?

  1. for dvo in ...: 这是一个循环,遍历 domain_validation_options 列表中的每一个对象,并将当前对象赋值给临时变量 dvo
  2. ... : dvo.domain_name => dvo: 这是 for 表达式生成map的关键部分。它遵循 key => value 的语法。
    • dvo.domain_name 作为 map 的 key。我们使用每个待验证的域名(例如 “test.core.org”)作为键。这非常重要,因为 for_each 要求的 map key 必须是唯一的字符串,而域名正好满足这个条件。
    • dvo 作为 map 的 value。我们将完整的验证信息对象 dvo 作为值。

经过这个转换,我们就得到了一个类似下面这样的map(值为示意):

{
  "test.core.org" = { domain_name = "test.core.org", resource_record_name = "_c123.test.core.org", ... },
  "admin-test.core.org" = { domain_name = "admin-test.core.org", resource_record_name = "_c456.admin-test.core.org", ... },
  ...
}
使用 each 对象引用值

一旦 for_each 接收到这个map,Terraform就会为map中的每一个键值对创建一个 aws_route53_record 资源的实例。在资源块内部,我们可以通过特殊的 each 对象来访问当前实例的 key 和 value。

  • each.key: 在我们的例子中,它会是 “test.core.org”, “admin-test.core.org” 等域名字符串。
  • each.value: 它会是我们赋给 map value 的 dvo 对象。

因此,代码中的这几行就变得非常好理解了:

  • name = each.value.resource_record_name: 设置DNS记录的名称,例如 _c123.test.core.org
  • type = each.value.resource_record_type: 设置DNS记录的类型,通常是 CNAME
  • records = [each.value.resource_record_value]: 设置DNS记录的值,这是AWS提供的用于验证的唯一字符串。

通过这种方式,我们仅用一个资源块就动态地为所有需要验证的域名创建了对应的DNS记录,代码简洁、可读性高,且易于扩展。如果未来证书增加了新的SAN,我们只需要修改 aws_acm_certificate 资源中的 subject_alternative_names 列表,apply 时 Terraform 会自动为新域名创建验证记录。

实用建议与注意事项
  1. 依赖关系:Terraform足够智能,它能理解 aws_route53_record.cloudfront_validation 依赖于 aws_acm_certificate.cloudfront。它会先创建证书请求,获取 domain_validation_options,然后再创建DNS记录。
  2. 等待验证完成:请注意,上面的代码只负责创建DNS记录。证书的状态此时仍然是 PENDING_VALIDATION。在实际生产中,你还需要使用 aws_acm_certificate_validation 资源来明确告诉Terraform,需要等待DNS记录生效且证书验证通过后,才能继续创建依赖此证书的其他资源(如Load Balancer Listener或CloudFront Distribution)。
  3. key 的选择:将 for 表达式转换为map时,选择一个稳定且唯一的字符串作为key至关重要。dvo.domain_name 是一个绝佳的选择。
结论

Terraform 的 for_each 循环远不止是创建多个资源的语法糖。它是一种处理动态、集合类基础设施的强大范式。通过今天对AWS ACM证书DNS验证案例的剖析,我们可以看到:

  • 提升了代码的简洁性:避免了大量重复的资源定义。
  • 增强了代码的可维护性:当域名列表变化时,无需修改DNS记录部分的代码。
  • 体现了IaC的核心思想:将基础设施的逻辑关系清晰地表达出来,实现了真正的自动化。

希望这篇文章能帮助大家更深入地理解 for_each 的强大之处,并将其应用到我们的日常工作中,解决更多自动化运维的难题。


网站公告

今日签到

点亮在社区的每一天
去签到