Kubernetes ExternalDNS 自动化DNS管理实战

Kubernetes ExternalDNS 自动化DNS管理实战 1. 项目概述让Kubernetes服务自动“上户口”省掉手动改DNS的苦差事ExternalDNS 这个工具我第一次在 DigitalOcean 的 Kubernetes 集群里把它跑通的时候手都在抖——不是因为技术多难而是因为终于不用再半夜爬起来登录 DNS 控制台对着一串又一长串的 Service 和 Ingress 名字一个一个去填 A 记录、CNAME 记录了。你肯定也经历过刚把新服务部署上线前端同事问“域名什么时候能访问”运维同事翻着白眼说“等我手动配完 DNS”结果一等就是半小时或者更糟某次发布后忘了更新 DNS用户打不开页面监控告警狂响排查半天才发现是 CNAME 指向了旧的 LoadBalancer IP。这就是 ExternalDNS 要解决的核心问题把 DNS 记录的生命周期完全绑定到 Kubernetes 对象的声明周期上。它不是个“辅助工具”而是一套自动化 DNS 管理的基础设施层。你定义一个 Ingress它就自动创建对应的 DNS 记录你删掉这个 Ingress它几秒内就把记录干干净净地清理掉。背后依赖的是 Kubernetes 的 Informer 机制监听资源变更再通过 DigitalOcean 提供的 API Token调用其 DNS API 完成增删改查。整个过程不碰任何人工干预也不依赖外部脚本或 CI/CD 流水线里的额外步骤。对 DigitalOcean 用户特别友好因为它的 API 稳定、文档清晰、权限粒度细你可以只给它管理某个域名的权限不像某些云厂商的 DNS API 还要绕道 IAM 或者需要全局管理员权限。如果你正在用 Helm 部署应用那 ExternalDNS 就是 Helm Chart 的天然搭档——Chart 里定义好 ingress.hostsExternalDNS 就会自动把 hosts 里的域名解析到对应的 LoadBalancer 地址。这已经不是“锦上添花”而是现代云原生运维的“基础水电”。2. 整体架构设计与方案选型逻辑2.1 为什么必须用 ExternalDNS而不是自己写脚本很多人第一反应是“我写个 Python 脚本定时轮询 Kubernetes API拿到 Service 的 External-IP再调 DigitalOcean DNS API 更新不就行了”我试过而且不止一次。第一次写了个 cron job每分钟跑一次结果发现两个致命问题一是延迟高服务上线后平均要等 30 秒以上才能解析生效二是状态漂移严重比如你删了一个 Ingress脚本可能刚好错过这次事件导致 DNS 记录残留变成“幽灵域名”。后来改成用 Kubernetes Watch API 实时监听问题没根除反而引入了新的坑Watch 连接断开重连时的状态同步丢失、Event 重复触发导致 DNS 接口被限流、以及最麻烦的——如何准确判断一个记录该“创建”还是该“更新”比如同一个域名今天指向 Service A明天指向 Service B脚本得自己维护一个本地状态缓存还要处理缓存和实际 DNS 状态不一致的情况。ExternalDNS 把这些全封装好了。它内部有一套完整的“Source-Target-Sync”模型Source 是 Kubernetes 中的资源Ingress/ServiceTarget 是 DNS 提供商DigitalOceanSync 是一个幂等的 reconcile 循环。每次 reconcile 前它会先从 DigitalOcean 拉取当前所有相关域名的记录快照再和 Kubernetes 里的期望状态做 diff只执行真正需要的操作。这个设计保证了最终一致性也彻底规避了状态漂移。更重要的是它支持多 Provider 同时运行比如你既有 DigitalOcean 的公网 DNS又有内部 CoreDNS 的私有 DNS而脚本一旦写死 Provider扩展性就没了。2.2 为什么不直接用 DigitalOcean 的 Load Balancer 自带的 DNS 功能DigitalOcean 的 Load Balancer 确实有个“Assign a domain name”的选项点一下就能生成一个 do.co 的子域名。但这个功能有硬伤它只支持 do.co 域名不支持你自己的主域名比如 yourcompany.com它不支持基于路径的路由Path-based routing也就是无法为 /api 和 /web 分别指向不同后端它不支持 TLS 终止证书还得你自己管。而 ExternalDNS 是站在 Ingress Controller比如 Nginx Ingress 或 Traefik肩膀上工作的。Ingress 资源天然支持 host path 的双重路由规则支持 annotations 配置 Lets Encrypt 自动签发证书配合 cert-managerExternalDNS 只需读取 Ingress 的 spec.rules.host 字段就能精准生成你指定的任意域名记录。换句话说Load Balancer 的 DNS 是“单点快捷入口”ExternalDNS 是“全链路 DNS 编排系统”。前者适合临时测试后者才是生产环境的标配。2.3 Helm vs. 原生 YAML为什么推荐 Helm 部署 ExternalDNSExternalDNS 官方提供了两种安装方式Helm Chart 和纯 YAML 清单。我强烈建议用 Helm。原因很实在配置项太多且高度耦合。比如你要设置--sourceingress就得同时确保集群里有 Ingress Controller你要用 DigitalOcean Provider就必须提供--providerdigitalocean和--digitalocean-api-token你还得决定是否启用--registrytxt用 TXT 记录标记所有权防止误删是否开启--policyupsert-only只增不删适合灰度发布是否设置--domain-filteryourcompany.com限定只管理特定域名避免越权。这些参数如果手写 YAML光是 ConfigMap 和 Deployment 的 env 字段就得写半页出错概率极高。Helm Chart 把这些都抽象成了 values.yaml 里的结构化字段比如provider: name: digitalocean digitalocean: apiToken: your-api-token-here sources: - ingress - service policy: upsert-only domainFilters: - yourcompany.com txtOwnerId: prod-cluster-01你只需要改这十几行helm install external-dns bitnami/external-dns -f values.yaml一条命令就搞定。而且 Helm 的版本管理和 rollback 功能在你某次升级 ExternalDNS 版本导致 DNS 同步异常时能救命。我们线上就发生过一次 v0.13.5 升级到 v0.14.0 后因 DigitalOcean API 返回格式微调导致部分记录同步失败。用helm rollback external-dns 1回退到上个版本5 分钟内就恢复了全部解析比手动 patch Deployment 快得多。2.4 权限最小化设计ServiceAccount、RBAC 与 DigitalOcean Token 的安全边界ExternalDNS 不是“上帝进程”它必须遵循最小权限原则。我在生产环境踩过最大的坑就是一开始给了它 cluster-admin 权限结果某次误操作删掉了整个集群的 Ingress 规则ExternalDNS 看到后立刻把所有关联的 DNS 记录全清空了——这不是 bug是 feature 的副作用。正确的做法是分三层隔离第一层Kubernetes RBAC。ExternalDNS 只需要读取特定命名空间下的 Ingress 和 Service不需要写权限更不需要访问 Nodes 或 Secrets。我的 rbac.yaml 是这样写的apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns rules: - apiGroups: [] resources: [services, endpoints, pods] verbs: [get, watch, list] - apiGroups: [extensions, networking.k8s.io] resources: [ingresses] verbs: [get, watch, list] - apiGroups: [] resources: [nodes] verbs: [list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: kube-system第二层ServiceAccount 绑定。它必须运行在kube-system命名空间并且只允许访问default和production这两个业务命名空间的资源通过namespaceSelector或在 Helm values 里指定watchNamespace。第三层DigitalOcean Token 权限。这是最关键的。绝对不能用你的个人账号 API Token必须在 DigitalOcean 控制台创建一个专用的 Team把目标域名如 yourcompany.com加入该 Team然后为 ExternalDNS 创建一个只拥有 “Read and Write” 权限的 API Token。这个 Token 只能操作该 Team 下的 DNS 记录即使泄露攻击者也无法访问你的 Droplet 或数据库。我在 DO 控制台的截图里这个 Token 的描述我写的是 “K8s-ExternalDNS-Prod-Only”一眼就知道用途和范围。提示Helm Chart 默认会创建一个名为external-dns的 ServiceAccount并自动挂载 Token。你只需在 values.yaml 里确认serviceAccount.createtrue并把你的 DO Token 写进provider.digitalocean.apiToken字段即可无需手动创建 Secret。3. 核心细节解析与实操要点3.1 DigitalOcean DNS API 的关键限制与应对策略ExternalDNS 跑不起来90% 的原因是卡在 DigitalOcean API 的限制上。DO 的 DNS API 不是无限调用的它有两条硬性规则官方文档藏得很深但必须提前知道第一Rate Limit每分钟最多 5000 次请求。这听起来很多但 ExternalDNS 的 reconcile 循环默认是 1 分钟一次每次 reconcile 会先 GET 所有记录假设你有 100 个域名每个域名平均 5 条记录就是 500 次 GET再对每个需要变更的记录发起 POST/PUT/DELETE假设 20 个变更就是 20 次写操作。加起来轻松破千。一旦超限API 返回 429 Too Many RequestsExternalDNS 就会进入 backoff 重试最长可能卡住 5 分钟。解决方案有两个一是调大--interval参数比如设成5m5 分钟把压力摊薄二是在 values.yaml 里启用--metrics-address:7979然后用 Prometheus 监控external_dns_provider_requests_total这个指标一旦发现 429 错误率突增立刻扩容或调参。我在线上用的是组合拳interval: 3mmetricsAddress: :7979 AlertManager 配置阈值告警。第二TTL 最小值是 30 秒。这意味着你无法设置低于 30 秒的 TTL这对需要快速故障转移的场景是个瓶颈。比如你的服务做了健康检查后端挂了你希望 DNS 在 10 秒内切走。ExternalDNS 本身不控制 TTL它只是把 Ingress 或 Service 的 annotation 透传给 DO API。所以你必须在资源定义里显式声明apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-app annotations: # 这个 annotation 会被 ExternalDNS 读取并设置为 DNS 记录的 TTL external-dns.alpha.kubernetes.io/ttl: 30 spec: rules: - host: app.yourcompany.com http: paths: - path: / pathType: Prefix backend: service: name: my-app-svc port: number: 80注意这个 annotation 的 key 是固定的不能写错。如果没写ExternalDNS 会用 DO API 的默认 TTL1800 秒那就太长了。另外DO 的 DNS 解析生效时间还受客户端 DNS 缓存影响所以你在测试时别只看dig app.yourcompany.com一定要用dig 1.1.1.1 app.yourcompany.com直连 Cloudflare DNS来排除本地 ISP 缓存干扰。3.2 TXT 记录所有权标记为什么它是生产环境的“保险丝”ExternalDNS 默认行为是“所见即所得”它看到 Kubernetes 里有个 Ingress就去 DO 创建对应记录看到记录没了就删除。这很危险。想象一下你有个历史遗留的 DNS 记录legacy.yourcompany.com它指向一个老的 EC2 实例但这个实例在 Kubernetes 里没有任何对应资源。某天你误操作把legacy.yourcompany.com加进了某个 Ingress 的 hosts 列表ExternalDNS 会立刻把这个域名的解析切到新的 LoadBalancer 上导致老服务中断。TXT 记录就是为了解决这个问题。当你在 values.yaml 里设置txtOwnerId: prod-cluster-01后ExternalDNS 每次创建 A 或 CNAME 记录时会同时创建一个同名的 TXT 记录内容是heritageexternal-dns,external-dns/ownerprod-cluster-01。下次 reconcile 时它只会管理那些 TXT 记录里 owner 字段匹配的域名。这样legacy.yourcompany.com因为没有对应的 TXT 记录就会被 ExternalDNS 完全忽略彻底避免误操作。这个机制就像给 DNS 记录加了一把锁只有持有正确钥匙txtOwnerId的服务才能动它。我们在上线前会先用doctl dns records list yourcompany.com检查所有现有记录对需要托管的手动补上 TXT 记录对不想托管的确保它们没有 TXT 记录。这一步是上线前的必检项。3.3 多环境隔离如何让 staging 和 prod 共享一个 DO 域名但互不干扰一个常见需求是staging.yourcompany.com和prod.yourcompany.com都属于yourcompany.com这个主域名但它们部署在不同的 Kubernetes 集群staging-cluster 和 prod-cluster你希望两个集群的 ExternalDNS 互不干扰。靠domain-filter是不够的因为两个集群都会看到yourcompany.com。正确解法是利用 txtOwnerId domain-filter 双重过滤。在 staging 集群的 values.yaml 里domainFilters: - yourcompany.com txtOwnerId: staging-cluster-01在 prod 集群的 values.yaml 里domainFilters: - yourcompany.com txtOwnerId: prod-cluster-01然后你为 staging 的 Ingress 显式加上 annotationannotations: external-dns.alpha.kubernetes.io/hostname: staging.yourcompany.com # 注意这里指定了 owner强制 ExternalDNS 用这个 owner 去匹配 TXT 记录 external-dns.alpha.kubernetes.io/txt-owner-id: staging-cluster-01而 prod 的 Ingress 则用prod-cluster-01。这样即使两个集群的 ExternalDNS 都在监听yourcompany.com它们也只会各自管理自己 owner ID 对应的记录。我们甚至用这个机制实现了“金丝雀发布”新建一个canary.yourcompany.com在它的 Ingress annotation 里写txt-owner-id: canary-release-2024然后只在 canary 集群部署 ExternalDNS 并配置这个 owner ID流量就只切给 canary 集群prod 集群完全无感。3.4 Ingress 与 Service 双源模式什么情况下该用 ServiceExternalDNS 支持--sourceingress和--sourceservice两种模式。绝大多数人只用 Ingress因为它能自动解析 host。但 Service 源有它不可替代的价值。比如你有一个 Kafka 集群需要给客户端提供稳定的kafka-broker-0.yourcompany.com这样的域名而不是基于 HTTP 的 host。Kafka 是 TCP 协议没法走 Ingress。这时你就得用 Service 源。具体操作是创建一个 TypeLoadBalancer 的 Service然后加上 annotationapiVersion: v1 kind: Service metadata: name: kafka-broker-0 annotations: # 这个 annotation 告诉 ExternalDNS把这个 Service 的 External-IP 解析到指定域名 external-dns.alpha.kubernetes.io/hostname: kafka-broker-0.yourcompany.com spec: type: LoadBalancer selector: app: kafka broker-id: 0 ports: - port: 9092 targetPort: 9092ExternalDNS 会监听这个 Service 的status.loadBalancer.ingress[0].ip字段一旦 LoadBalancer IP 分配好就立刻创建 DNS 记录。注意Service 源有个坑它只支持 A 记录IPv4不支持 CNAME。所以如果你的 LoadBalancer 是 IPv6-onlyDO 目前不支持 IPv6 LB这条路就走不通。我们线上 Kafka 就是这么做的所有客户端连接字符串里写的都是kafka-broker-0.yourcompany.com:9092IP 变了完全不用改代码。4. 实操过程与核心环节实现4.1 准备工作DigitalOcean Token 创建与 Kubernetes RBAC 配置第一步登录 DigitalOcean 控制台点击右上角头像 - “API” - “Tokens/Keys” - “Generate New Token”。在弹窗里Token name 填k8s-external-dns-prod勾选 “Write” 权限必须因为要创建/删除记录在 “Resources” 区域点击 “Filter by team”选择你为 DNS 专门创建的 Team比如叫dns-team点击 “Generate Token”复制生成的 token 字符串。这个字符串只显示一次关掉页面就再也看不到请立刻存到密码管理器第二步在 Kubernetes 集群里创建专用的 ServiceAccount 和 RBAC。我习惯用一个独立的 YAML 文件rbac.yaml# 创建命名空间可选但推荐 kubectl create namespace external-dns # 应用 RBAC kubectl apply -f rbac.yamlrbac.yaml内容如下已按最小权限精简apiVersion: v1 kind: ServiceAccount metadata: name: external-dns namespace: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns rules: - apiGroups: [] resources: [services, endpoints, pods] verbs: [get, watch, list] - apiGroups: [extensions, networking.k8s.io] resources: [ingresses] verbs: [get, watch, list] - apiGroups: [] resources: [nodes] verbs: [list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: external-dns注意这里我把 ServiceAccount 放在了external-dns命名空间而不是kube-system。这是为了更好的隔离。Helm Chart 默认会创建 SA但如果你手动管理务必确保 SA 名称和 Helm values 里serviceAccount.name一致。4.2 Helm 部署 ExternalDNS逐行详解 values.yaml我们用 Bitnami 的 Helm Chartbitnami/external-dns它更新勤快社区支持好。先添加仓库helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update然后创建values.yaml。下面是我生产环境的完整配置每一行都加了注释说明为什么这么设# 全局配置 nameOverride: external-dns fullnameOverride: external-dns # 镜像配置用稳定版不追最新 image: registry: docker.io repository: bitnami/external-dns tag: 0.13.5-debian-11-r0 pullPolicy: IfNotPresent # ServiceAccount 配置指向我们刚创建的 serviceAccount: create: false # 我们已手动创建设为 false name: external-dns annotations: {} # RBAC 配置确保 Helm 不覆盖我们的手动设置 rbac: create: false # 我们已手动创建 ClusterRole 和 Binding # ExternalDNS 核心参数 args: # 指定数据源我们同时用 Ingress 和 Service - --sourceingress - --sourceservice # 指定 DNS 提供商 - --providerdigitalocean # 只管理 ourcompany.com 及其子域名 - --domain-filterourcompany.com # TXT 记录 owner ID用于所有权标记 - --txt-owner-idprod-cluster-01 # 同步策略只增不删避免误删生产环境强烈推荐 - --policyupsert-only # 同步间隔3分钟平衡实时性和 API 压力 - --interval3m # 日志级别调试时设 debug生产用 info - --log-levelinfo # 启用 metrics方便监控 - --metrics-address:7979 # DigitalOcean Provider 配置 provider: name: digitalocean digitalocean: # 这里填你刚才复制的 API Token apiToken: your-do-api-token-here # 资源限制根据集群规模调整 resources: limits: cpu: 200m memory: 256Mi requests: cpu: 100m memory: 128Mi # Pod 安全策略禁止特权模式 securityContext: runAsNonRoot: true runAsUser: 1001 fsGroup: 1001 # 亲和性确保它和 Ingress Controller 在同一可用区可选 affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app.kubernetes.io/name operator: In values: - external-dns topologyKey: topology.kubernetes.io/zone部署命令helm install external-dns bitnami/external-dns \ --namespace external-dns \ --create-namespace \ -f values.yaml部署后检查 Pod 状态kubectl get pods -n external-dns # 应该看到 external-dns-xxx-xxx Running 1/1 12s # 查看日志确认初始化成功 kubectl logs -n external-dns deploy/external-dns | head -20 # 正常输出应该包含 Created DigitalOcean client 和 All records are up to date4.3 验证与调试从零开始跑通第一个 DNS 记录现在我们来部署一个最简单的 Ingress验证整个链路。创建test-ingress.yamlapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: hello-world namespace: default annotations: # 告诉 ExternalDNS把这个 Ingress 的 host 解析到 DNS kubernetes.io/ingress.class: nginx # 指定要解析的域名 external-dns.alpha.kubernetes.io/hostname: hello.ourcompany.com # 强制使用我们设定的 owner ID external-dns.alpha.kubernetes.io/txt-owner-id: prod-cluster-01 spec: rules: - host: hello.ourcompany.com http: paths: - path: / pathType: Prefix backend: service: name: nginx-demo port: number: 80 --- # 一个简单的 nginx Service 作为后端 apiVersion: v1 kind: Service metadata: name: nginx-demo namespace: default spec: selector: app: nginx-demo ports: - port: 80 targetPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo namespace: default spec: replicas: 1 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80应用它kubectl apply -f test-ingress.yaml然后耐心等待 3 分钟因为我们设了--interval3m。期间你可以实时观察 ExternalDNS 的日志kubectl logs -n external-dns deploy/external-dns -f你会看到类似这样的日志流time2024-05-20T08:12:34Z levelinfo msgDesired change: CREATE hello.ourcompany.com A time2024-05-20T08:12:34Z levelinfo msgDesired change: CREATE hello.ourcompany.com TXT time2024-05-20T08:12:35Z levelinfo msgCreating records: hello.ourcompany.com A time2024-05-20T08:12:35Z levelinfo msgCreating records: hello.ourcompany.com TXT time2024-05-20T08:12:36Z levelinfo msgAll records are up to date最后用 dig 验证# 查询 A 记录 dig short hello.ourcompany.com ns1.digitalocean.com # 应该返回你的 LoadBalancer 的 IP 地址比如 159.203.123.45 # 查询 TXT 记录确认所有权标记 dig short hello.ourcompany.com TXT ns1.digitalocean.com # 应该返回 heritageexternal-dns,external-dns/ownerprod-cluster-01如果 dig 没返回别急先检查kubectl get ingress hello-world -o wide确认 ADDRESS 字段不为空LoadBalancer 已分配 IPkubectl get svc -n external-dns确认 ExternalDNS 的 Service 正常运行kubectl describe deploy external-dns -n external-dns看 Events 里有没有 ImagePullBackOff 或 CrashLoopBackOff4.4 高级配置实战为 Ingress 添加 TLS 并自动解析证书域名ExternalDNS 和 cert-manager 是绝配。我们来部署一个带 HTTPS 的 Ingress。首先确保 cert-manager 已安装用 Helm 安装最稳helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set installCRDstrue然后创建一个 Issuer用 Lets Encrypt 生产环境# issuer.yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: adminourcompany.com privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx应用issuer.yaml。接着部署带 TLS 的 IngressapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: secure-app namespace: default annotations: kubernetes.io/ingress.class: nginx # 这里指定了两个域名ExternalDNS 会为它们都创建 A 记录 external-dns.alpha.kubernetes.io/hostname: app.ourcompany.com,api.ourcompany.com # cert-manager 注解 cert-manager.io/cluster-issuer: letsencrypt-prod spec: tls: - hosts: - app.ourcompany.com - api.ourcompany.com secretName: app-tls-secret rules: - host: app.ourcompany.com http: paths: - path: / pathType: Prefix backend: service: name: app-svc port: number: 80 - host: api.ourcompany.com http: paths: - path: / pathType: Prefix backend: service: name: api-svc port: number: 80ExternalDNS 会自动为app.ourcompany.com和api.ourcompany.com创建 A 记录。cert-manager 会自动申请证书并把证书存到app-tls-secret这个 Secret 里。Nginx Ingress Controller 读取这个 Secret就完成了 HTTPS 终止。整个过程你只需要写一次 YAML剩下的全是自动化。我们线上所有对外服务都用这套模板新增一个域名改三行 YAML10 分钟内就 HTTPS 就绪。5. 常见问题与排查技巧实录5.1 问题速查表ExternalDNS 不工作90% 的情况在这里现象可能原因排查命令解决方案kubectl get pods -n external-dns显示ImagePullBackOff镜像仓库拉取失败或镜像 tag 不存在kubectl describe pod -n external-dns pod-name检查values.yaml里的image.tag是否拼写正确Bitnami Chart 的 tag 格式是0.13.5-debian-11-r0不是0.13.5kubectl logs -n external-dns deploy/external-dns显示failed to list *v1.Service: services is forbiddenRBAC 权限不足ClusterRole 没给services资源的list权限kubectl auth can-i list services --assystem:serviceaccount:external-dns:external-dns检查rbac.yaml确保rules数组里包含了services和listdig hello.ourcompany.com返回空但kubectl get ingress显示 ADDRESS 有值ExternalDNS 没监听到这个 Ingress可能命名空间不对kubectl get ingress -A --field-selector metadata.namespace!defaultExternalDNS 默认只监听default命名空间如果 Ingress 在production需在values.yaml里加- --namespaceproductiondig hello.ourcompany.com返回旧 IP新 LoadBalancer IP 没生效DigitalOcean DNS API 调用失败或 ExternalDNS reconcile 没触发kubectl logs -n external-dns deploy/external-dns | grep error|failed检查 DO Token 是否过期或是否被误删查看external_dns_provider_requests_total{code429}指标是否飙升dig hello.ourcompany.com TXT返回空但 A 记录存在txtOwnerId配置错误或 Ingress annotation 里的txt-owner-id不匹配kubectl get ingress hello-world -o yaml | grep -A5 txt-owner-id确保values.yaml的txtOwnerId和 Ingress annotation 的external-dns.alpha.kubernetes.io/txt-owner-id完全一致5.2 实战避坑那些文档里不会写的“血泪教训”坑一Ingress Class 名字大小写敏感且必须和 Ingress Controller 的--ingress-class参数一致。我们曾经把 Nginx Ingress Controller 的启动参数设为--ingress-classnginx小写但在 Ingress 的 annotation 里写了kubernetes.io/ingress.class: Nginx首字母大写。ExternalDNS 严格按字符串匹配结果它根本“看不见”这个 Ingress日志里安静得可怕。解决方案统一用小写并在values.yaml里加- --ingress-classnginx参数让 ExternalDNS 只监听指定 class。坑二DigitalOcean 的 NS 记录必须 100% 正确少一个点都不行。你在 DO 控制台为ourcompany.com设置 NS 时必须填ns1.digitalocean.com.末尾带点。如果填成ns1.digitalocean.com不带点DO 会把它当成相对域名自动补成ns1.digitalocean.com.ourcompany.com.导致 DNS 解析失败。这个点是 DNS 协议里的“根域标记”缺一不可。我们上线前会用dig NS ourcompany.com命令确认返回的 NS 记录末尾都有点。坑三ExternalDNS 的--policysync是“双刃剑”生产环境慎用。sync模式会主动删除 Kubernetes 里不存在的 DNS 记录。听起来很干净但风险极大。有一次我们想临时测试一个新域名test.ourcompany.com就手动在 DO 控制台创建了它没走 ExternalDNS。结果 ExternalDNS 的 sync 循环一跑发现 Kubernetes 里没有对应资源立刻把test.ourcompany.com的 A 记录删了。幸好我们有监控5 分钟内就发现了。从此我们生产环境一律用upsert-only所有“计划外”的 DNS 操作都走 DO 控制台ExternalDNS 只