external-dns & Kubernetes Ingress 使用实战
# external-dns external-dns 自动将 Kubernetes Ingress 的 hostname 同步到 AWS Route53,免去手动维护 DNS 记录。本文档覆盖架构、工作原理、部署、配置与排错。 > 参考文档:[kubernetes-sigs/external-dns](https://github.com/kubernetes-sigs/external-dns)(官方仓库)|[官方文档](https://kubernetes-sigs.github.io/external-dns/)|[AWS Provider 配置说明](https://kubernetes-sigs.github.io/external-dns/latest/providers/aws/) --- ## 一、架构说明 ### 1.1 整体拓扑 external-dns 是一个跑在集群里的控制器,以 Ingress 资源为输入,以 Route53 为输出。它从 Ingress 的 `spec.rules[].host` 读出域名,从 Ingress `status.loadBalancer.ingress.hostname` 读出 ALB DNS 名,然后在 Route53 Hosted Zone 中创建/维护一条 **A Alias record** 指向该 ALB DNS 名。 关键设计点: - **用 Route53 Alias record,而非普通 A record**:ALB 后端 IP 随弹性伸缩变化,Alias record 指向的是 ALB 的 DNS 名称,Route53 内部解析该名称并自动跟随 IP 变化;普通 A record 写死 IP 做不到自动跟踪。 - **TXT ownership record**:与 A 记录一起写入,value 为本集群的 `txtOwnerId`,作为"这条记录归谁管"的归属标记,防止多实例互相覆盖。 - **domainFilters 准入**:本集群只处理 host 后缀命中 `domainFilters` 的 Ingress,不命中一律忽略。 ### 1.2 IAM 拓扑:两种链路 四个集群按 Route53 Zone 是否与集群同账号,分两种授权链路。 **链路 A — 同账号直连**(prod-devops-eks、test-eks):集群账号 = Route53 Zone 账号(63488xxx),IRSA role 直接操作 Route53。 | 来源 | 操作 | 目标 | |------|------|------| | prod-devops-eks IRSA role(账号 63488xxx) | 直接操作 Route53 | Route53 Hosted Zone(账号 63488xxx) | | test-eks IRSA role(账号 63488xxx) | 直接操作 Route53 | Route53 Hosted Zone(账号 63488xxx) | Route53 Hosted Zone 管理域名:unixin.org(public)/ qa.svc.unix.market(private) **链路 B — 跨账号 AssumeRole**(prod-testnet-funds-eks、prod-testnet-dex-eks):集群在 funds/dex 账号,Zone 在 63488xxx。需先 AssumeRole 到 63488xxx 的 `external-dns-route53-role`,再由该 role 操作 Route53。 | 来源 | 操作 | 目标 | |------|------|------| | prod-testnet-funds-eks IRSA role(账号 84812xxx) | sts:AssumeRole | external-dns-route53-role(账号 63488xxx) | | prod-testnet-dex-eks IRSA role(账号 43333xxx) | sts:AssumeRole | external-dns-route53-role(账号 63488xxx) | | external-dns-route53-role(账号 63488xxx) | 操作 Route53 | Route53 Hosted Zone unixin.org(账号 63488xxx) | 链路为:funds/dex 集群 IRSA role ──sts:AssumeRole──▶ external-dns-route53-role ──操作 Route53──▶ Route53 Hosted Zone **图例** - 🟡 黄:Route53 Hosted Zone(DNS 真正落地处) - 🟠 橙:中间授权角色 `external-dns-route53-role`(仅跨账号链路出现) - 🔵 蓝:各集群 IRSA role(external-dns pod 挂载的 ServiceAccount 身份) **跨账号链路双向授权(排错根因)** - 🔵 IRSA role 的 Permission Policy → 必须含 `sts:AssumeRole` 到 🟠 route53-role - 🟠 route53-role 的 Trust Policy → 必须信任 🔵 IRSA role ARN - 🟠 route53-role 的 Permission Policy → 必须有对 🟡 Zone 的 `route53:ChangeResourceRecordSets` 等 --- ## 二、工作原理 ### 2.1 同步循环 external-dns 的同步循环如下: 1. **监听 Ingress**:监听集群内带 `ingressClassName: alb` 的 Ingress 资源。 2. **读取域名**:从 `spec.rules[].host`(或 `external-dns.alpha.kubernetes.io/hostname` annotation)取出要管理的域名。 3. **过滤**:只处理命中本集群 `domainFilters` 后缀的域名,不匹配的忽略。 4. **取 ALB 地址**:从 Ingress `status.loadBalancer.ingress.hostname` 读 ALB DNS 名。**若该列还没值(ALB 未就绪),跳过该 Ingress,等下一轮。** 5. **写 Route53**:在对应 Hosted Zone 创建/更新一条 A 记,类型为 Route53 Alias,alias target 指向上一步拿到的 ALB DNS 名。 6. **写 TXT ownership**:同时创建/更新一条 TXT record,value 为 `txtOwnerId`,作为归属标记。 7. **循环**:由 Ingress 创建/更新事件触发,并配合定时轮询(默认 30s)兜底。 > **触发同步的前提**:① host 命中 `domainFilters`;② ALB 已就绪(Ingress ADDRESS 列有值)。两者缺一,对应 DNS 记录都不会被创建。 ### 2.2 同步全过程时序图 | 步骤 | 参与方 | 动作 | 备注 | |------|--------|------|------| | ① | kubectl | apply Ingress,host=foo.qa.svc.unix.market | 入口 | | ② | ALB Controller | 创建 ALB,Ingress status.hostname 写入 | ADDRESS 列开始有值 | | ③ | external-dns | 收到 Ingress 事件(事件触发 + 30s 轮询兜底) | — | | ④ | external-dns | 读 host,校验 domainFilters | **判断分支点** | | ④a | external-dns | ✗ 不命中后缀 → 跳过本 Ingress,不写任何记录,等下轮 | 红色分支 | | ④b | external-dns | ✓ 命中 qa.svc.unix.market → 继续 | 绿色分支 | | ⑤ | external-dns | 读 Ingress status.hostname = ALB DNS 名 | 无值则等下轮 | | ⑥ | Route53 | ListHostedZones 定位匹配 Zone | — | | ⑦ | Route53 | 创建/更新 A Alias record,alias target → ALB DNS | 真正改 DNS | | ⑧ | Route53 | 创建/更新 TXT ownership record,value=txtOwnerId | 归属标记 | **参与方颜色对照**:🔵 蓝 = 集群侧(kubectl、ALB Controller、external-dns 内部处理);🟡 黄 = Route53 侧(真正改 DNS 记录的几步);🟢 绿 = 命中通过;🔴 红 = domainFilters 未命中分支,直接跳过。 --- ## 三、部署 | 集群 | AWS 账号 | Route53 Zone 类型 | 管理域名 (domainFilters) | Policy | txtOwnerId | |------|---------|-------------------|--------------------------|--------|------------| | prod-devops-eks | 63488xxx | public | unixin.org | upsert-only | devops-eks | | test-eks | 63488xxx | private | qa.svc.unix.market | sync | test-eks | | prod-testnet-funds-eks | 84812xxx | public | unixin.org | upsert-only | testnet-funds-eks | | prod-testnet-dex-eks | 43333xxx | public | unixin.org | upsert-only | testnet-dex-eks | **说明** - prod-devops-eks / test-eks:与 Route53 同账号,直接 IRSA(链路 A)。 - prod-testnet-funds-eks / prod-testnet-dex-eks:跨账号,通过 aws-assume-role assume 到 63488xxx 的 Route53 role(链路 B)。 - `txtOwnerId` 每集群唯一,是 TXT ownership 记录的归属标识,决定"哪些记录归本集群管"。多集群共管同一域名(如 funds/dex/prod-devops 都管 unixin.org)时尤其重要,必须唯一,否则一个集群会把另一个集群的记录当成"孤儿"删掉。 --- ### 部署 external-dns 到集群 external-dns 以 Deployment 形式部署在集群的 `external-dns` 命名空间,通过 IRSA(IAM Roles for Service Accounts)给 pod 绑定 AWS 权限,才能扫描 Ingress 并操作 Route53。没绑 IRSA = pod 没有 AWS 凭证,扫到 Ingress 也写不进 Route53。推荐用 Helm 部署。 **Helm 安装** ```bash helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/ helm repo update helm install external-dns external-dns/external-dns \ -n external-dns --create-namespace \ -f values.yaml ``` **values.yaml — 同账号直连(以 test-eks 为例)** ```yaml provider: aws policy: sync domainFilters: - qa.svc.unix.market txtOwnerId: test-eks managedRecordTypes: - A extraArgs: - --aws-zone-type=private serviceAccount: create: true name: external-dns annotations: eks.amazonaws.com/role-arn: arn:aws:iam::63488xxx:role/external-dns-test-eks ``` **跨账号场景额外参数(funds/dex)** ```yaml extraArgs: - --aws-assume-role=arn:aws:iam::63488xxx:role/external-dns-route53-role ``` 此时 ServiceAccount 注解指向 funds/dex 账号自己的 IRSA role(84812xxx / 43333xxx),该 role 的 Permission Policy 须含 `sts:AssumeRole` 到上面的 route53-role(详见 IAM 配置章节)。 **关键参数对照** | 参数 | 含义 | 取值 | |------|------|------| | `provider` | DNS 提供商 | aws | | `policy` | 操作范围 | upsert-only / sync | | `domainFilters` | 域名准入后缀 | unixin.org / qa.svc.unix.market | | `txtOwnerId` | 集群归属标识 | devops-eks / test-eks / ... | | `managedRecordTypes` | 管理的记录类型 | [A] | | `--aws-zone-type` | Zone 类型过滤 | public / private | | `--aws-assume-role` | 跨账号 assume 目标 role | arn:aws:iam::63488xxx:role/external-dns-route53-role | | `serviceAccount.annotations` | IRSA role 绑定 | eks.amazonaws.com/role-arn | > IRSA 注解是部署的核心:external-dns pod 挂载带注解的 ServiceAccount,EKS 临时向其注入该 role 的凭证,pod 用此凭证调用 Route53(同账号)或先 STS AssumeRole 再调用 Route53(跨账号)。 **验证部署** ```bash kubectl get deploy -n external-dns external-dns kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns ``` 约 30s 内日志应出现对应 host 的 A record 创建记录;若日志出现 `AccessDenied`,对照 IAM 配置章节逐项核查。 --- ## 四、Ingress 使用方式 ### 4.1 自动管理(默认) 只要 Ingress 的 `spec.rules[].host` 命中集群的 `domainFilters`,无需任何 annotation,external-dns 自动创建 DNS 记录: ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-service annotations: alb.ingress.kubernetes.io/group.name: my-alb-group spec: ingressClassName: alb rules: - host: my-service.qa.svc.unix.market # external-dns 自动处理 http: paths: - path: / pathType: Prefix backend: service: name: my-service port: number: 8080 ``` ### 4.2 显式指定域名(可选) 当一个 Ingress 对应多域名、或想用的域名与 `rules[].host` 不一致时,用 annotation 覆盖: ```yaml annotations: external-dns.alpha.kubernetes.io/hostname: foo.qa.svc.unix.market,bar.qa.svc.unix.market ``` > 注意:annotation 指定的域名仍需命中 `domainFilters`,否则同样被忽略。 --- ## 五、关键配置说明 ### 5.1 policy 控制 external-dns 对 DNS 记录的操作范围: | 值 | 创建 | 更新 | 删除 | 适用 | |----|------|------|------|------| | `upsert-only` | ✅ | ✅ | ❌ | 生产环境。Ingress 删除后 DNS 记录保留,需手动清理,防止误删 | | `sync` | ✅ | ✅ | ✅ | 测试环境。Ingress 删除后 DNS 记录自动删除 | | `create-only` | ✅ | ❌ | ❌ | 仅创建,不改动已存在记录 | > 取舍:生产用 `upsert-only` 防"删 Ingress → DNS 跟着没了"的误伤;测试用 `sync` 让环境自动清理。代价是 `upsert-only` 会留下孤儿记录,需定期巡检。 ### 5.2 txtOwnerId 每集群唯一的归属标识,写入 TXT ownership record。external-dns 删除/更新一条记录前,先读其 TXT,**只有 txtOwnerId 匹配自己时才敢动**。多集群共管同一域名时,必须保证各集群 txtOwnerId 互不相同,否则会出现"A 集群把 B 集群的记录当孤儿删掉"的互相覆盖。 ### 5.3 managed-record-types 所有集群统一只配置 `A`。含义: - external-dns **只对 managed-record-types 列表内的记录类型做 reconcile**;列表里没有的类型它"看不见",既不创建也不删除。 - 因此只创建 A,不创建 AAAA(IPv6)。 - **副作用**:存量 AAAA 记录不会被 external-dns 清理,必须到 Route53 控制台手动删,否则双栈客户端可能解析到 AAAA 而 ALB 侧 IPv6 未配好导致访问异常。 ### 5.4 domainFilters 本集群只处理 host 后缀命中以下域名之一的 Ingress: | 集群 | domainFilters | |------|---------------| | prod-devops-eks | unixin.org | | test-eks | qa.svc.unix.market | | prod-testnet-funds-eks | unixin.org | | prod-testnet-dex-eks | unixin.org | > 是 host → DNS 记录的"准入闸口"。FAQ「DNS 记录没被创建」第一排查项就是它。 --- ## 六、IAM 配置 详见 [iam-setup.md]。按链路区分: **链路 A(同账号直连)** - IRSA role 的 Permission Policy 直接含对 `unixin.org` / `qa.svc.unix.market` Hosted Zone 的 `route53:ChangeResourceRecordSets`、`route53:ListResourceRecordSets`、`route53:ListHostedZones` 等。 **链路 B(跨账号 AssumeRole)双向要求** - 🔵 funds/dex 的 IRSA role 的 Permission Policy:须含 `sts:AssumeRole` 到 `arn:aws:iam::63488xxx:role/external-dns-route53-role` - 🟠 `external-dns-route53-role` 的 Trust Policy:须允许 funds/dex 的 IRSA role ARN Assume - 🟠 `external-dns-route53-role` 的 Permission Policy:须含对 `unixin.org` Hosted Zone 的 Route53 读写权限 --- ## 七、常见问题 ### 7.1 DNS 记录没有被创建 按顺序排查: 1. Ingress 的 host 后缀是否命中本集群 `domainFilters`?(见 5.4) 2. ALB 是否已就绪——`kubectl get ingress` 看 ADDRESS 列是否有值(ALB DNS 名)。没有 = external-dns 还没东西可写,等下一轮。 3. external-dns pod 日志:`kubectl logs -n external-dns -l app.kubernetes.io/name=external-dns`,搜对应 host/域名关键字。 4. 跨账号场景:确认 AssumeRole 链路通(见 7.4)。 ### 7.2 DNS 记录没有被删除 - `upsert-only` policy 下**设计如此**,不会自动删。手动到 Route53 删,或临时切 `sync` 让它清一次再切回。 - 确认对应 Ingress 已删,且 Route53 里的 TXT ownership record 也一并清理,否则切 `sync` 后 external-dns 可能因 ownership 不匹配而不删。 ### 7.3 存量 AAAA 记录残留 `managed-record-types` 只含 A,external-dns 不会动 AAAA。需手动到 Route53 删除 AAAA 记录。 ### 7.4 跨账号 Assume 失败 双向核对(对照架构图链路 B): 1. funds/dex 账号 IRSA role 的 Permission Policy 含 `sts:AssumeRole` → `external-dns-route53-role`。 2. `external-dns-route53-role` 的 Trust Policy 含对应账号 IRSA role ARN。 3. `external-dns-route53-role` 对 `unixin.org` Zone 有 Route53 读写权限。 4. external-dns pod 日志查 `AccessDenied` / `not authorized to perform sts:AssumeRole`。
http://www.cnblogs.com/Jame-mei
浙公网安备 33010602011771号