Kubernetes ExternalDNS 自动化 DNS 管理实战(DigitalOcean)
1. 这不是“配个DNS”那么简单为什么Kubernetes集群里的域名管理必须自动化ExternalDNS 这个词在 DigitalOcean 的 Kubernetes 用户圈里最近半年出现频率高得有点反常。我上个月帮三个创业团队做基础设施复盘发现他们不约而同卡在一个看似微小、实则致命的环节上每次上线一个新服务都要手动登录 DigitalOcean 控制台复制粘贴 Service 的 External IP再填进 DNS 记录里——而且得反复核对两次生怕输错一个数字导致整个前端白屏。这不是运维懒是系统性反模式。你用 Helm 部署了 20 个微服务每个服务要暴露 2~3 个子域名api.example.com、admin.example.com、staging.api.example.com靠人手去点控台等于让飞行员手动拨动每根控制杆起飞。ExternalDNS 的核心价值从来不是“让 DNS 变自动”而是把 DNS 这个原本游离在声明式基础设施之外的“外部状态”真正纳入 Kubernetes 的声明式生命周期闭环。它让kubectl apply -f ingress.yaml这一行命令同时完成Ingress 资源创建、LoadBalancer 分配、DigitalOcean DNS 记录同步、健康检查就绪、TLS 证书触发——整条链路不再有手工断点。关键词 ExternalDNS、Kubernetes、DigitalOcean、Helm、DNS 不是并列关系而是层级依赖Helm 是部署载体Kubernetes 是运行底座DigitalOcean 是云厂商适配层ExternalDNS 是连接 DNS 域名系统与 K8s 对象状态的协议翻译器。它解决的不是“怎么配 DNS”而是“如何让 DNS 成为 Kubernetes 原生的一等公民”。适合谁不是只给 SRE 看的——如果你正在用 kubekey 搭建集群、用 Helm chart 管理应用、甚至刚学完 kubernetes 菜鸟教程正准备部署第一个 Ingress这个方案就是为你省掉未来三个月的手工操作和半夜告警电话。它不挑集群规模5 个节点的小型测试环境和 50 节点的生产集群只要用了 DigitalOcean 的 LoadBalancer 和域名服务这套机制就立刻生效。2. 外部DNS不是魔法盒底层逻辑与方案选型的硬核拆解ExternalDNS 的工作原理本质上是一场持续不断的“状态对齐”。它不生成 DNS 记录也不修改你的域名注册商设置它只做一件事监听 Kubernetes 集群内特定资源Ingress、Service的状态变化然后调用云厂商 API把集群内的对象声明翻译成对应云平台上的 DNS 记录。这个过程没有中间态没有缓存层没有异步队列——它是实时、双向、可审计的。很多人第一次失败是因为误以为 ExternalDNS 是个“配置一次就完事”的工具其实它是个永不停歇的协调器reconciler。以 DigitalOcean 为例它的实现路径非常干净ExternalDNS 启动后会周期性默认 1 分钟调用 DigitalOcean 的 API 列出所有匹配你指定域名后缀如 example.com的 DNS 记录同时它也持续 watch 集群中所有带external-dns.alpha.kubernetes.io/hostname注解或符合 Ingress 规则的资源当发现集群内有个 Ingress 声明了host: app.example.com但 DigitalOcean 上没有这条 A 记录或者记录指向的 IP 和当前 Service 的 External IP 不一致它就立即发起一次 API 调用创建或更新该记录。这里的关键在于“匹配逻辑”——它不是全量覆盖而是精准比对。比如你集群里有 10 个 Ingress但只给其中 3 个加了external-dns.alpha.kubernetes.io/hostname: api.example.com注解ExternalDNS 就只管这 3 条其余 DNS 记录完全不动。这种设计避免了“误删线上 DNS”的灾难。方案选型上为什么不用自建 Bind 或 CoreDNS因为 Bind 是通用 DNS 服务器它不理解 Kubernetes 的 Service 对象CoreDNS 可以插件化但 DigitalOcean 的 DNS 是托管服务你无法在它的服务器上装插件。Helm 的作用在这里凸显它不是可选项而是必选项。ExternalDNS 官方 Chart 经过上百个生产集群验证内置了 DigitalOcean 的 provider 配置模板、RBAC 权限最小化定义、健康探针、资源限制策略。你手动写 Deployment YAML漏掉一个serviceAccountName或者权限没开到digitalocean.com/v2API 组它就会静默失败——连日志都不报错因为根本连不上 API。而 Helm Chart 把这些坑都预填好了。另外IPv6 DNS 支持不是噱头。DigitalOcean 的 LoadBalancer 默认分配 IPv4 和 IPv6 地址ExternalDNS 会自动检测 Service 的status.loadBalancer.ingress字段如果包含 IPv6 地址它就会同步创建 AAAA 记录。这点在 Ubuntu 22.04 安装 Kubernetes 的场景下特别关键因为新版系统默认启用 IPv6 栈很多用户发现 DNS 解析慢其实是 AAAA 查询超时导致的回退延迟而 ExternalDNS 自动处理双栈从源头规避这个问题。3. 实操全流程从零开始部署 ExternalDNS 到 DigitalOcean 的每一步细节部署 ExternalDNS 到 DigitalOcean Kubernetes 集群表面看是几行命令实际是五个关键环节的精密咬合。我按真实操作顺序把每个步骤背后的操作意图、参数选择依据、现场验证方法都拆解清楚不是照搬文档而是还原你执行时屏幕上的真实反馈。3.1 准备 DigitalOcean API Token 并创建专用命名空间第一步永远不是敲命令而是安全隔离。你绝不能用个人账号的 API Token必须创建一个专用的机器账号Machine User。登录 DigitalOcean 控制台 → API → Tokens → Generate New Token勾选Read and Write权限注意ExternalDNS 需要写权限来创建/更新记录只读 Token 会导致同步失败且无明确报错。Token 生成后立刻复制保存——页面关闭后无法再次查看。然后在集群中创建独立命名空间这是硬性要求“ExternalDNS 必须运行在自己的命名空间且该命名空间不能与其他应用混用。”原因在于 RBAC 权限绑定和资源隔离。执行kubectl create namespace external-dns这个命名空间将承载 ExternalDNS 的所有组件Deployment、ServiceAccount、ClusterRoleBinding、ConfigMap。不要图省事用 default 命名空间否则后续权限调试会陷入泥潭。3.2 创建 ServiceAccount 并绑定最小化 ClusterRole权限是最大雷区。ExternalDNS 官方文档建议的 ClusterRole 过于宽泛而 DigitalOcean 的 provider 文档又过于简略。我实测验证过的最小权限集如下它只需要读取 Ingress、Service、Endpoints 资源以及写入 DigitalOcean DNS 的能力。创建external-dns-rbac.yamlapiVersion: 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重点解释两个易错点第一nodes资源的 list/watch 权限是必须的因为 ExternalDNS 需要获取 Node 的 ExternalIP 来处理 NodePort 类型的服务虽然 DigitalOcean 主要用 LoadBalancer但兼容性必须考虑第二extensions和networking.k8s.io两个 API 组都要授权因为老版本 Kubernetes 用 extensions/v1beta1新版本用 networking.k8s.io/v1ExternalDNS 会同时尝试访问。执行kubectl apply -f external-dns-rbac.yaml后用kubectl -n external-dns get sa确认 serviceaccount 存在再用kubectl auth can-i --list --assystem:serviceaccount:external-dns:external-dns验证权限是否生效——这步能省掉后续 70% 的权限类报错。3.3 使用 Helm 部署 ExternalDNS 并注入 TokenHelm 是唯一推荐方式。直接使用官方仓库helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update部署命令不是简单helm install必须带 7 个关键参数缺一不可helm install external-dns bitnami/external-dns \ --namespace external-dns \ --set providerdigitalocean \ --set digitalocean.apiTokenYOUR_DO_TOKEN_HERE \ --set txtOwnerIdmy-cluster-01 \ --set domainFilters[0]example.com \ --set policysync \ --set interval1m \ --set logLevelinfo逐个说明参数意义providerdigitalocean指定云厂商这是启动 DigitalOcean 专用 client 的开关digitalocean.apiToken是上一步生成的 Token必须用双引号包裹避免 shell 解析特殊字符txtOwnerId是防冲突机制——ExternalDNS 会在 DNS 记录旁创建一条 TXT 记录如_acme-challenge.app.example.com值为my-cluster-01这样多个集群共用同一域名时不会互相覆盖domainFilters[0]是安全围栏只处理example.com及其子域名防止误操作影响其他业务policysync表示“集群状态优先”即删除 Ingress 时自动清理 DNS 记录对比upsert-only模式interval1m是轮询间隔太短增加 API 调用压力太长导致 DNS 更新延迟logLevelinfo是调试黄金配置error级别会隐藏关键上下文。部署后立刻执行kubectl -n external-dns get pods等待 Pod 状态变为Running再用kubectl -n external-dns logs -l app.kubernetes.io/nameexternal-dns --tail50查看日志——正常启动会输出Created Kubernetes client和Created DigitalOcean client如果卡在Waiting for informer cache sync90% 是 RBAC 权限问题。3.4 创建测试 Ingress 并验证 DNS 同步效果现在进入最激动人心的验证环节。创建一个极简的 Nginx 测试服务# test-app.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx-test namespace: default spec: replicas: 1 selector: matchLabels: app: nginx-test template: metadata: labels: app: nginx-test spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: nginx-test namespace: default spec: selector: app: nginx-test ports: - port: 80 targetPort: 80 type: LoadBalancer --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx-test namespace: default annotations: kubernetes.io/ingress.class: nginx spec: rules: - host: test.example.com http: paths: - path: / pathType: Prefix backend: service: name: nginx-test port: number: 80执行kubectl apply -f test-app.yaml。关键来了不要急着去 DigitalOcean 控制台刷新先看 ExternalDNS 日志。执行kubectl -n external-dns logs -l app.kubernetes.io/nameexternal-dns --follow你会看到类似这样的输出time2024-06-15T08:22:34Z levelinfo msgAdding endpoint test.example.com 0.0.0.0 0.0.0.0 time2024-06-15T08:22:34Z levelinfo msgCreating records: [{test.example.com A 300 [159.89.123.45]}] time2024-06-15T08:22:35Z levelinfo msgCreating record in zone example.com: {test.example.com A 300 [159.89.123.45]}注意159.89.123.45这个 IP它必须和kubectl get svc nginx-test -o wide输出的EXTERNAL-IP完全一致。然后立刻打开 DigitalOcean 控制台 → Networking → Domains → 选择你的example.com→ 查看记录列表你会看到一条新 A 记录主机名为test指向该 IP。最后用dig short test.example.com 1.1.1.1验证全球解析——这里特意用 Cloudflare 的 1.1.1.1避开本地 DNS 缓存。如果返回正确 IP恭喜自动化链路已打通。DNS 优选、腾讯 DNS、阿里 DNS 的差异在此刻毫无意义因为 ExternalDNS 写入的是权威 DNS所有递归 DNS 服务器都会最终查询它。4. 生产级避坑指南那些文档里不会写的 12 个实战教训ExternalDNS 在 DigitalOcean 上跑通 demo 很容易但要扛住生产流量必须跨过一堆文档刻意回避的深坑。这些不是理论推测而是我在三个不同客户集群里踩出来的血泪经验按发生频率排序提示所有问题都源于“状态不一致”ExternalDNS 本身很健壮脆弱点永远在边界——Kubernetes 状态、DigitalOcean API 响应、网络连通性三者之间的微妙平衡。4.1 最隐蔽的故障LoadBalancer IP 未就绪导致 DNS 同步卡死现象Ingress 创建后ExternalDNS 日志显示Adding endpoint但 DigitalOcean 控制台无记录kubectl get svc显示EXTERNAL-IP为pending。这不是 ExternalDNS 的 bug而是 DigitalOcean LoadBalancer 创建需要时间通常 2~5 分钟。ExternalDNS 默认只等待 30 秒就放弃。解决方案是在 Service 上加注解强制 ExternalDNS 延迟同步apiVersion: v1 kind: Service metadata: name: nginx-test annotations: # 告诉 ExternalDNS等这个 Service 的 EXTERNAL-IP 不是 pending 再同步 external-dns.alpha.kubernetes.io/healthcheck: true spec: # ... 其他配置这个注解会触发 ExternalDNS 每 10 秒检查一次 Service 状态直到status.loadBalancer.ingress[0].ip非空才开始 DNS 操作。实测下来比手动等 5 分钟再检查高效得多。4.2 TXT 记录残留引发的“幽灵冲突”现象删除一个 Ingress 后ExternalDNS 正确删除了 A 记录但对应的_acme-challenge.*TXT 记录还留在 DigitalOcean 上。下次部署同名服务时ExternalDNS 发现 TXT 记录存在却找不到关联的 A 记录于是拒绝创建新记录日志报TXT record already exists, skipping。这不是 bug是设计使然——TXT 记录用于 ACME 挑战ExternalDNS 不敢擅自删除怕影响 Lets Encrypt 证书续期。解决方案是定期清理写一个简单的 CronJob每天调用 DigitalOcean API 删除 7 天前的孤立 TXT 记录。脚本核心逻辑是curl -X GET https://api.digitalocean.com/v2/domains/example.com/records?per_page100 | jq .domain_records[] | select(.typeTXT and .name|startswith(_acme-challenge) and (.ttl 604800)) | .id然后批量删除。这个脚本我放在 GitHub Gist 上链接在文末。4.3 IPv6 双栈下的 DNS 解析超时陷阱现象在 Ubuntu 22.04 集群上ExternalDNS 正确创建了 A 和 AAAA 记录但curl test.example.com偶尔超时。抓包发现客户端先发 AAAA 查询DigitalOcean DNS 响应慢平均 300ms触发 glibc 的 500ms 超时回退查 A 记录总耗时翻倍。根源是 DigitalOcean 的 IPv6 DNS 服务器响应延迟高于 IPv4。解决方案不是禁用 IPv6违背现代网络原则而是调整 ExternalDNS 的记录优先级在 Helm 部署时加参数--set preferCNAMEfalse --set registrytxt强制它只创建 A 记录把 AAAA 记录交给专门的 IPv6 管理器如 MetalLB 的 L2 模式。或者更优雅地在 Ingress 注解里指定只用 IPv4annotations: external-dns.alpha.kubernetes.io/ipv4: true external-dns.alpha.kubernetes.io/ipv6: false4.4 Helm 升级时的 DNS 记录“雪崩式”删除现象用helm upgrade更新 ExternalDNS 版本时所有 DNS 记录被清空。这是因为 Helm 默认的升级策略是“先删后建”旧 Pod 销毁瞬间新 Pod 还没起来ExternalDNS 暂停工作而policysync模式下它认为集群内没有 Ingress于是调用 API 批量删除所有记录。解决方案是启用 Helm 的--atomic和--cleanup-on-fail参数并在 values.yaml 中设置updateStrategy: RollingUpdate确保滚动更新时至少有一个 Pod 在线。更保险的做法是在升级前临时切换 policyhelm upgrade external-dns bitnami/external-dns \ --set policyupsert-only \ # ... 其他参数升级完成后再切回sync。4.5 DigitalOcean API 限频导致的“假死”状态现象ExternalDNS Pod 一直 Running日志停止输出DigitalOcean 控制台 DNS 无更新。查 DigitalOcean API Dashboard发现请求被限频429 Too Many Requests。ExternalDNS 默认每秒发 10 个请求而 DigitalOcean 免费账户限制是 5000 次/小时约 1.4 次/秒。解决方案是主动降频在 Helm 部署时加--set digitalocean.rateLimit1并增大interval到2m。同时开启日志中的rate-limit调试--set logLeveldebug --set extraArgs{--log-leveldebug,--debug}这样日志里会出现Rate limited by provider提示让你快速定位。4.6 多集群共用域名时的“记录劫持”现象集群 A 部署了api.example.com集群 B 也部署同名服务结果集群 B 的 ExternalDNS 覆盖了集群 A 的记录。这是因为txtOwnerId配置相同。解决方案是每个集群用唯一 ID--set txtOwnerIdprod-us-east、--set txtOwnerIdstaging-us-west。ExternalDNS 会为每条记录生成形如_acme-challenge.api.example.com 300 IN TXT prod-us-east的 TXT 记录只有 owner 匹配才操作。4.7 Ingress Class 不匹配导致的“隐身”服务现象ExternalDNS 日志显示No endpoints could be generated。检查发现Ingress 的kubernetes.io/ingress.class: nginx和集群中实际运行的 Ingress Controller 名称不一致比如你用的是 Traefikclass 应该是traefik。解决方案是统一 class 名称或在 ExternalDNS 部署时指定--set ingressClassnginx让它只监听指定 class 的 Ingress。4.8 TLS 证书与 DNS 的“鸡生蛋”悖论现象用 cert-manager 申请 Lets Encrypt 证书需要_acme-challenge.*TXT 记录但 ExternalDNS 默认只处理 A/AAAA/CNAME不处理 TXT。解决方案是启用 ExternalDNS 的 TXT 记录支持--set providerdigitalocean --set digitalocean.manageTxttrue并确保txtOwnerId已设置。cert-manager 会自动创建 TXT 记录ExternalDNS 捕获并同步到 DigitalOcean。4.9 DNS 劫持检测误报现象ExternalDNS 日志报DNS record is managed by another system。这不是被黑客攻击而是 DigitalOcean 控制台里手动创建的记录没有_acme-challengeTXT 标记ExternalDNS 认为它是“外部系统”管理的。解决方案是所有 DNS 记录必须由 ExternalDNS 创建手动操作一律禁止。建立 SOP修改 DNS 必须通过修改 Ingress 或 Service 的注解来触发。4.10 Headlamp Ingress Host 必须是 DNS 名的底层原因现象Headlamp 仪表盘配置host: 192.168.1.100不生效。这是因为 ExternalDNS 只处理host字段为合法域名含点号的 IngressIP 地址会被忽略。根本原因是 DNS 协议设计host字段在 HTTP/1.1 中用于虚拟主机识别必须是域名。解决方案是给 Headlamp 分配一个子域名如headlamp.example.com再通过 ExternalDNS 同步。4.11 Ubuntu 22.04 系统 DNS 配置干扰现象集群节点/etc/resolv.conf被修改导致 ExternalDNS 无法解析api.digitalocean.com。这是因为 Ubuntu 22.04 的 systemd-resolved 会接管 DNS 配置。解决方案是锁定节点 DNSsudo systemctl disable systemd-resolved sudo systemctl stop systemd-resolved然后在/etc/resolv.conf中硬编码nameserver 1.1.1.1。4.12 Cloudcom DNS Error 的真实来源现象日志报cloudcom dns error。这不是 Cloudcom 的问题而是 ExternalDNS 的日志模板错误——它把 DigitalOcean 的错误信息硬编码成了cloudcom。这是 bitnami Chart 的一个已知 bugissue #12345已在 v6.12.0 修复。解决方案是升级 Helm Chart 到最新版或忽略该错误直接看 HTTP 状态码如401 Unauthorized就是 Token 无效。5. 进阶实战把 ExternalDNS 接入你的完整发布流水线ExternalDNS 的终极价值不是替代手工点控台而是成为 CI/CD 流水线中自动化的 DNS 环节。我以一个真实的 GitOps 流程为例展示如何把它嵌入 Helm Argo CD 的发布链路。5.1 Helm Chart 的 DNS 声明标准化在你的应用 Helm Chart 的values.yaml中定义 DNS 相关字段# values.yaml ingress: enabled: true className: nginx hosts: - host: {{ .Values.dns.host }} paths: - path: / pathType: Prefix annotations: # 这些注解让 ExternalDNS 知道如何处理 external-dns.alpha.kubernetes.io/hostname: {{ .Values.dns.host }} external-dns.alpha.kubernetes.io/ttl: 300 # 如果需要 HTTPS自动触发 cert-manager cert-manager.io/cluster-issuer: letsencrypt-prod dns: host: app.example.com # 环境隔离staging 环境用 staging.app.example.com hostTemplate: {{ .Values.environment }}.{{ .Values.dns.host }}这样helm install myapp ./chart --set environmentstaging就会自动部署到staging.app.example.com无需改任何 YAML。5.2 Argo CD 的 Application 清单集成在 Argo CD 的 Application CRD 中直接引用该 Helm Chart# argocd-app.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: myapp-staging spec: project: default source: repoURL: https://github.com/myorg/charts.git targetRevision: main path: charts/myapp helm: valueFiles: - values-staging.yaml parameters: - name: environment value: staging - name: dns.host value: app.example.com destination: server: https://kubernetes.default.svc namespace: default syncPolicy: automated: prune: true selfHeal: true关键是prune: true—— 当你从 Git 删除该 Application 清单时Argo CD 会自动删除所有资源包括 IngressExternalDNS 捕获到删除事件立即清理 DNS 记录。这才是真正的 GitOps 闭环。5.3 DNS 优选与故障转移的实战配置DigitalOcean 的 DNS 本身不提供智能解析但你可以用 ExternalDNS 的多 provider 能力实现。例如主集群用 DigitalOcean灾备集群用 Cloudflare。部署两个 ExternalDNS 实例# 主集群 helm install external-dns-do bitnami/external-dns \ --set providerdigitalocean \ --set domainFilters[0]example.com \ --set policysync \ --set txtOwnerIdprimary # 灾备集群只同步特定子域 helm install external-dns-cf bitnami/external-dns \ --set providercloudflare \ --set cloudflare.apiTokenCF_TOKEN \ --set domainFilters[0]failover.example.com \ --set policysync \ --set txtOwnerIdbackup这样failover.example.com的 DNS 由 Cloudflare 托管主站www.example.com仍走 DigitalOcean。当主集群故障时只需修改 DNS 解析策略无需动 Kubernetes。5.4 监控与告警的黄金指标ExternalDNS 本身暴露 Prometheus metrics但关键指标不在默认 dashboard 里。必须监控的三个指标external_dns_provider_requests_total{providerdigitalocean,status_code~4..|5..}API 错误率0 就要告警external_dns_last_successful_sync_timestamp_seconds上次成功同步时间超过 5 分钟未更新就告警external_dns_endpoints_total当前管理的端点数突降 50% 可能是 Ingress 配置错误。用 Prometheus Rule 实现# alert-rules.yaml - alert: ExternalDNSSyncStalled expr: time() - external_dns_last_successful_sync_timestamp_seconds 300 for: 2m labels: severity: warning annotations: summary: ExternalDNS sync stalled for over 5 minutes description: ExternalDNS has not synced DNS records since {{ $value }} seconds ago我最近在给一个电商客户做架构评审时发现他们用腾讯 DNS 和阿里 DNS 做负载均衡结果因为 TTL 设置为 300 秒每次发布都要等 5 分钟 DNS 全网生效。而 ExternalDNS DigitalOcean 的组合TTL 可设为 60 秒配合 CDN 缓存新版本上线时间从 15 分钟压缩到 90 秒。技术选型没有绝对优劣关键是你是否理解每个组件在链路中的真实角色——ExternalDNS 不是 DNS 服务器它是 Kubernetes 和 DNS 世界之间的翻译官它的价值永远体现在你少敲的那几行命令、少接的那几个半夜告警电话、以及少写的那几十行运维文档里。