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.42. 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`。

 

posted @ 2026-09-07 11:05  JameMei  阅读(5)  评论(0)    收藏  举报