1. DNS 在基础设施中的位置
一句话总结: DNS 是唯一「改动即全局可见」的配置层,它既是流量的入口,也是证书签发的验证通道,因此必须比应用资源更保守地变更。
大多数资源改错了只影响自己,DNS 改错了会影响所有依赖该域名的系统。更棘手的是:证书签发往往依赖 DNS 验证,形成一条「DNS → 证书 → 负载均衡」的隐式依赖链。
data "aws_route53_zone" "primary" {
name = var.root_domain
private_zone = false
}
resource "aws_route53_record" "app" {
zone_id = data.aws_route53_zone.primary.zone_id
name = "app.${var.root_domain}"
type = "A"
alias {
name = aws_lb.app.dns_name
zone_id = aws_lb.app.zone_id
evaluate_target_health = true
}
}
依赖链的实际形态:
| 环节 | 依赖对象 | 失败表现 |
|---|---|---|
| 托管区域 | 域名注册商 NS 记录 | 解析完全不生效 |
| 证书 | 托管区域中的验证记录 | 证书卡在 Pending |
| 负载均衡监听器 | 已签发的证书 ARN | 无法创建 HTTPS 监听 |
| 应用记录 | 负载均衡 DNS 名 | 流量打不到后端 |
一句话:DNS 变更的验收标准是「解析生效」,而不是「Terraform apply 成功」。
2. 托管区域与记录建模
一句话总结: 托管区域要么新建、要么用数据源接管既有;记录则应按「用途」拆成独立资源,而不是把整个 zone 塞进一个文件。
新建区域适合全新域名,force_destroy = false 保证误删时不会连带删除全部记录:
resource "aws_route53_zone" "new" {
name = var.new_domain
force_destroy = false
tags = local.common_tags
}
接管既有域名则用数据源,避免 Terraform 试图「重建」一个已经存在且承载流量的区域:
data "aws_route53_zone" "existing" {
name = var.existing_domain
private_zone = false
}
记录类型的选择直接影响可用性:
| 类型 | 用途 | 关键参数 |
|---|---|---|
| A / AAAA | 指向 IP 或别名 | alias 块 |
| CNAME | 指向另一个域名 | 不能用于根域名 |
| MX | 邮件路由 | priority |
| TXT | 验证与策略 | 值需引号包裹 |
| NS | 子域委派 | 由服务商管理 |
# TXT 记录的值必须带引号,这是 DNS 协议要求而非 Terraform 语法
resource "aws_route53_record" "spf" {
zone_id = data.aws_route53_zone.primary.zone_id
name = var.root_domain
type = "TXT"
ttl = 3600
records = ["\"v=spf1 include:_spf.example.com ~all\""]
}
一句话:根域名只能用 A/AAAA 别名,用 CNAME 会在 apply 时报错——这是 DNS 协议的限制,不是工具的限制。
3. 证书签发与 DNS 验证
一句话总结: DNS 验证要求「先有验证记录、后确认证书」,Terraform 必须显式表达这一顺序,否则证书会一直卡在等待状态。
resource "aws_acm_certificate" "app" {
domain_name = "app.${var.root_domain}"
validation_method = "DNS"
subject_alternative_names = ["www.${var.root_domain}"]
lifecycle {
create_before_destroy = true
}
}
关键点是验证记录必须被真正创建,并且证书要等待验证完成:
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.app.domain_validation_options :
dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.aws_route53_zone.primary.zone_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
resource "aws_acm_certificate_validation" "app" {
certificate_arn = aws_acm_certificate.app.arn
validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}
下游资源必须引用 aws_acm_certificate_validation.app.certificate_arn,而不是 aws_acm_certificate.app.arn——前者才表示「已验证」,这是最容易被忽略的一处。
| 引用对象 | 含义 | 后果 |
|---|---|---|
aws_acm_certificate.app.arn | 证书已创建 | 可能尚未验证 |
aws_acm_certificate_validation.app.certificate_arn | 证书已验证 | 可安全绑定 |
aws_acm_certificate.app.domain_validation_options | 待写入的验证记录 | 需 for_each 展开 |
一句话:证书编排里唯一重要的一行,是下游引用「验证完成」而不是「创建完成」。
4. 通配符证书与多域名
一句话总结: 通配符证书能覆盖整个子域层级,但只覆盖一级;多域名证书则通过 SAN 列表扩展,两者的验证记录数量与续期风险不同。
resource "aws_acm_certificate" "wildcard" {
domain_name = "*.${var.root_domain}"
subject_alternative_names = [var.root_domain]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
*.example.com 覆盖 api.example.com,但不覆盖 example.com,也不覆盖 a.b.example.com——所以需要把根域名加进 SAN。
| 方案 | 覆盖范围 | 验证记录数 | 风险 |
|---|---|---|---|
| 单域名 | 一个主机名 | 1 | 域名多则证书多 |
| 通配符 | 一级子域 + 根域 | 1~2 | 任一子域泄漏影响全部 |
| 多域名 SAN | 明确列举 | 每个域名 1 条 | 列表变更需重新签发 |
多域名证书的 for_each 展开与通配符相同,验证记录会随域名数量线性增长。
resource "aws_acm_certificate" "multi" {
domain_name = "app.${var.root_domain}"
subject_alternative_names = [
"api.${var.root_domain}",
"admin.${var.root_domain}",
]
validation_method = "DNS"
}
4.1 跨区域与 CloudFront 的限制
CloudFront 只接受位于 us-east-1 的证书,其他服务则要求证书与资源同区域:
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
resource "aws_acm_certificate" "edge" {
provider = aws.us_east_1
domain_name = "cdn.${var.root_domain}"
validation_method = "DNS"
}
| 使用方 | 证书区域要求 |
|---|---|
| ALB / NLB | 与负载均衡同区域 |
| CloudFront | 必须在 us-east-1 |
| API Gateway 自定义域名 | 与 API 同区域 |
一句话:区域限制是证书编排中最常见的「本地测试通过、上线报错」来源。
5. 记录生命周期与 TTL 策略
一句话总结: TTL 决定变更的传播速度与查询成本,迁移期用低 TTL、稳定期用高 TTL,是 DNS 运维的基本节奏。
resource "aws_route53_record" "app" {
zone_id = data.aws_route53_zone.primary.zone_id
name = "app.${var.root_domain}"
type = "A"
ttl = var.dns_ttl
records = [aws_eip.app.public_ip]
}
别名记录(alias)不使用 TTL,由 Route53 内部管理,且不产生查询费用——只要目标支持别名,就应优先使用别名。
| 场景 | 建议 TTL | 理由 |
|---|---|---|
| 迁移切换前 | 60 秒 | 快速回退 |
| 稳定生产 | 300~3600 秒 | 降低查询成本 |
| 静态资源 CDN | 86400 秒 | 变更极少 |
| 故障演练 | 30~60 秒 | 快速切流 |
记录也需要防护:
resource "aws_route53_record" "app" {
# ……
lifecycle {
prevent_destroy = true
}
}
对承载流量的 A 记录加 prevent_destroy,可以避免一次误删导致的全站不可达。
一句话:DNS 回滚的速度取决于 TTL,而 TTL 必须在变更之前就调低。
6. 迁移与接管既有域名
一句话总结: 接管既有域名的正确姿势是「先导入、后管理」,直接让 Terraform 创建会与现有记录冲突,甚至造成解析中断。
第一步是把既有记录导入 state,而不是重新创建:
terraform import aws_route53_record.app \
Z0123456789ABCDEF_app.example.com_A
导入前建议先导出为 HCL 骨架,减少手写偏差:
terraform plan -generate-config-out=generated.tf
第二步是处理 NS 委派。若域名原本在其他服务商,需要先把 NS 记录指向新的托管区域,再逐步迁移记录:
| 阶段 | 操作 | 风险 |
|---|---|---|
| 1 建立区域 | 新建托管区域,不切 NS | 无 |
| 2 复制记录 | 逐条导入并核对 | 低 |
| 3 调低 TTL | 切换前 24 小时 | 无 |
| 4 切换 NS | 在注册商处修改 | 中,需回退预案 |
| 5 观察清理 | 确认解析正常后清理旧区域 | 低 |
# 子域委派:把子域交给另一个托管区域管理
resource "aws_route53_record" "delegation" {
zone_id = data.aws_route53_zone.primary.zone_id
name = "sub.${var.root_domain}"
type = "NS"
ttl = 172800
records = aws_route53_zone.sub.name_servers
}
一句话:DNS 迁移没有「快速方案」,只有「可回退方案」。
7. 续期、监控与排查
一句话总结: ACM 证书自动续期依赖验证记录持续存在,因此绝不能手工删除验证记录;监控要覆盖「到期时间」与「解析正确性」两类信号。
resource "aws_cloudwatch_metric_alarm" "cert_expiry" {
alarm_name = "${local.prefix}-cert-expiry"
comparison_operator = "LessThanThreshold"
evaluation_periods = 1
metric_name = "DaysToExpiry"
namespace = "AWS/CertificateManager"
period = 86400
statistic = "Minimum"
threshold = 30
alarm_description = "证书将在 30 天内到期"
}
验证记录被手工删除后,证书会静默地无法续期——直到到期前一天才暴露。
# 快速核对解析是否符合预期
dig +short app.example.com
dig +short NS example.com
# 查看证书状态与验证记录
aws acm describe-certificate --certificate-arn "$ARN" \
--query 'Certificate.{Status:Status,Domain:DomainName}'
| 检查项 | 命令 | 关注点 |
|---|---|---|
| 解析结果 | dig +short | 是否为预期 IP |
| 权威 NS | dig NS | 是否指向正确区域 |
| 证书状态 | aws acm describe-certificate | 是否 ISSUED |
| 到期时间 | CloudWatch DaysToExpiry | 是否低于阈值 |
一句话:证书续期失败几乎总是「验证记录被删」或「区域限制」,而不是 ACM 本身的问题。
8. 总结
DNS 与证书编排的核心是把隐式依赖显式化:
| 环节 | 要点 |
|---|---|
| 区域 | 新建用资源,接管用数据源 |
| 记录 | 根域名用别名,TXT 值需带引号 |
| 证书 | 下游引用「验证完成」而非「创建完成」 |
| 验证 | for_each 展开验证记录,TTL 取 60 秒 |
| 通配符 | 只覆盖一级子域,根域名需加 SAN |
| 区域限制 | CloudFront 证书必须在 us-east-1 |
| TTL | 迁移前调低,稳定后调高 |
| 迁移 | 先导入后管理,NS 切换需回退预案 |
一句话收尾:DNS 与证书是「外部世界看到的你」。应用内部怎么改都不会被用户感知,但一条 A 记录或一张证书出问题,全世界立刻知道。把验证记录纳入 for_each、让下游依赖「已验证」的 ARN、给承载流量的记录加 prevent_destroy、为到期时间配告警——这四件事做完,域名与证书这一层基本就不再需要「救火」。下一篇讨论模块的注册表与分发,那是把本文的编排能力沉淀成可复用资产的方式。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。