不知道你是不是也有这种感觉K8s 集群跑起来之后环境一多、服务一多最烦的反而是那些看起来特别不起眼的杂活。我这次被折磨的是内部域名解析——几十个服务走 Ingress域名全靠手工在 DNS 控制台里加解析记录业务一迭代就得跟着改加得我头都大了。客户那边催得紧我还因为漏加一条解析白白排查了半个多小时。后来把 ExternalDNS 接进来让 Ingress 里的 host 自动同步成内网 DNS 记录才算把这个活彻底自动化。这篇就把整套方案从头到尾讲清楚包括原理、部署参数、踩坑记录和排查链路适合那些正在用托管 Kubernetes 集群、被内部域名维护折磨的人。先说一下环境背景我这边用的是云厂商的托管 Kubernetes 集群Ingress 控制器是 NGINX 那套集群内部服务之间走内部域名通信。目标很简单开发或运维在集群里创建一个 Ingress声明一个内部域名DNS 记录就自动出现不需要任何人去控制台手工操作。听起来很美好实际做起来也确实做到了但中间有不少细节值得展开聊聊。1. 内部域名解析的痛点手工维护 DNS 记录有多累我先把场景还原一下。假设你有二十个内部服务每个服务都暴露在集群里前端通过 Ingress 路由到对应后端。这些服务之间的调用按照规范化要求不能直接写 IP也不能只写 Service 名而是要写完整的内部域名比如order.internal.example.local、payment.internal.example.local。域名一多问题就来了。1.1 手工流程的每一步都在埋雷没有 ExternalDNS 之前我的日常是这样的先在 Git 仓库里提交一个 Ingress YAML等待集群同步然后跑到云厂商的 DNS 控制台找到私有域手工添加一条 A 记录指向 Ingress Controller 的负载均衡地址。听起来也就三步但每步都有翻车空间。比如服务上线那天我同时提交了三个 Ingress结果只去控制台加了两条记录漏掉的那条恰好是核心链路要用的。线上调用直接报域名解析失败我查了半天最后发现不是代码问题不是网络问题而是 DNS 记录压根不存在。再比如负载均衡地址变了。Ingress Controller 如果重建LB IP 可能是新的。手工加的记录不会跟着变还是指向旧地址服务方的调用就会时通时断这种问题特别难排查因为代码没动过K8s 里看起来也一切正常。1.2 为什么不能只靠 Ingress Controller有人可能会问Ingress 里不是已经有 host 了吗难道它本身不负责解析这里要分清两件事。Ingress Controller 负责的是南北向流量路由——它接收外部请求根据 host 把请求转发给对应的 Service。但它不会去注册 DNS它只知道“如果这个域名的流量到达我这里我应该转发到哪”。也就是说Ingress 声明的是“路由规则”不是“解析记录”。域名要能被客户端解析出 IP还需要 DNS 服务器里有对应的 A 记录或 CNAME 记录。这两者是独立的中间缺一个环节域名就跑不通。1.3 内部域名的特殊要求内部域名和公网域名还不一样。公网域名有完整的 DNS 体系随便找个公共 DNS 就能查到记录内部域名通常只存在于某个云厂商的私有域服务里或者自建的 DNS 服务器里只有同一个 VPC 内的机器才能解析。这就带来一个额外的维护成本你不仅要保证记录存在还要保证它存在于正确的私有域并且 VPC 内的所有节点、所有 Pod 都要能递归解析到它。如果只是偶尔一两台机器解析不到哪怕 DNS 记录本身没问题也会让人觉得“玄学”。所以说内部域名解析的核心痛点并不是“创建一条记录”而是“让记录的生命周期和 Ingress 保持一致”。Ingress 创建记录要有Ingress 修改域名记录要跟着改Ingress 删除记录要清理。这个一致性诉求才是 ExternalDNS 真正解决的事。2. ExternalDNS 的原理与价值它到底在帮你做什么我第一次听到 ExternalDNS 这名字时以为又是一套需要啃三天的复杂组件。实际上它的设计思路非常直白就是一个运行在集群里的控制器持续盯着你的 Ingress或 Service然后调用 DNS 服务商的 API把记录同步过去。2.1 监听、比对、同步三段式ExternalDNS 的工作循环可以用三个词概括监听、比对、同步。监听指的是它通过 Kubernetes 的 Watch 机制订阅 Ingress 资源的变化。只要集群里有 Ingress 被创建、修改或删除ExternalDNS 都会在很短时间内感知到。比对比的是“当前 DNS 服务商里有什么记录”和“根据集群资源应该有什么记录”。它会拉取你指定的私有域下现有记录和 Ingress 暴露的 host 列表做对比找出需要新增的、需要更新的、需要删除的。同步就是调用 DNS 服务商的 API 执行变更。新增和更新通常没什么风险删除则要看策略后面我会专门讲。这个循环不是实时的默认会有个同步周期一般设置 1 分钟。所以从 Ingress 创建到 DNS 记录生效最坏情况大概是“1 分钟同步周期 DNS TTL 缓存时间”正常情况下几分钟内就能解析。2.2 为什么 TXT 记录这么重要ExternalDNS 有个容易被忽略但极其关键的机制它会在 DNS 里额外写一条 TXT 记录用来标记这条记录归谁管。举个例子Ingress host 是order.internal.example.localExternalDNS 除了创建 A 记录指向 LB 地址还会写一条类似external-dns.cluster-dev.order.internal.example.local的 TXT 记录内容里包含所属集群的标识和记录指纹。这样做的目的是防止多个 ExternalDNS 实例管理同一个域名时互相打架。如果两个集群的 ExternalDNS 都在操作同一个 DNS Zone而没有任何归属标记那么 A 记录就会被后写入的那个实例覆盖前面那个集群的域名就不通了。有了 TXT 归属标记每个实例只修改自己创建的记录其他实例的记录一概不动。这个机制在单集群场景下看起来多此一举但在多集群、多环境共用一个 DNS 域时它就是救命的。2.3 为什么数据源主选 IngressExternalDNS 官方支持很多数据源包括 Service、Ingress、Gateway 等。我这边实际用下来内部域名场景优先选 Ingress原因很实在域名语义本身就在 Ingress 的 host 字段里。如果我们用 Service 作为数据源每个 Service 都会生成一条记录指向 Service 的负载均衡地址。但很多内部服务并不需要直接暴露到 Ingress 层用 Service 当源会生成大量无用记录。Ingress 作为数据源时只有那些真正配置了 host 的资源才会生成 DNS 记录信息密度高维护量小。另外ExternalDNS 读取目标地址的方式也很关键。它会优先读 Ingress 的 Status 里 LoadBalancer 字段获取 Ingress Controller 暴露出来的负载均衡 IP 或域名。如果 Status 为空则需要通过注解手动指定目标地址。这一步是后续排错的高发区我会在验证章节展开。3. 环境准备托管集群、Ingress 控制器和私有 DNS 域在写配置之前先聊聊环境准备。这套方案不需要额外的中间件但有几个前提条件没满足的话ExternalDNS 装上了也白搭。3.1 基础设施清单首先你需要一个托管 Kubernetes 集群版本建议 1.20 以上。太老的版本对 Ingress v1 API 的支持不完整ExternalDNS 在读取 Ingress 资源时可能会出问题。其次集群里要部署好 Ingress Controller。我用的是 NGINX Ingress Controller它会把负载均衡器的地址写回 Ingress 的 Status 字段ExternalDNS 正是从这个字段拿到目标 IP 的。再次要有一个 DNS 服务商侧的内部域名空间。这个空间可以是云厂商的私有域服务在 VPC 内生效也可以是自建的权威 DNS 服务器。我这次用的是前者但两种模式原理一样。最后还需要一组有权限操作这个 DNS 空间的 API 凭证。权限原则是最小化只能操作这个私有域下的记录不能给它整个账号的写权限。3.2 私有 DNS Zone 的设计建议域名设计比想象中重要。不要把你所有环境塞进同一个域里比如不要把所有测试环境的服务都写成xxx.internal.example.local。一旦环境多了记录会互相干扰删除时还可能误删别的环境的记录。比较好的做法是每个环境一个子域比如devdev.internal.example.localtesttest.internal.example.localprodprod.internal.example.local如果还有多集群可以在环境底下再拆一层比如cluster-a.test.internal.example.local。这样做的好处有两个一是 ExternalDNS 的--domain-filter天然会按域名做过滤子域隔离就是天然的权限边界二是记录归属清楚出问题时看域名就能判断是哪个环境的问题。3.3 凭证与权限的最小化配置创建完私有域后我需要为 ExternalDNS 准备身份凭证。这里强烈建议不要在集群里用全局管理员密钥而是要创建一个只允许操作该私有域的专用身份。我的一般做法是把凭证放到 Kubernetes Secret 里ExternalDNS 的 Deployment 通过环境变量或挂载文件读取。这样权限是收敛的就算集群里的 Pod 被入侵也只是泄露一个受限凭证影响面可控。还有一个容易被忽略的点凭证对应的账号必须对这个私有域有完整的读写权限包括创建解析记录、修改解析记录、删除解析记录、查看记录列表。有些只读密钥能查看但写不了ExternalDNS 日志会一直报权限错误这种问题最容易在环境搭建初期浪费时间。3.4 集群内解析链路如何打通ExternalDNS 负责把记录写到 DNS 服务商但集群里的 Pod 要能解析到这个内部域名还需要解析链路畅通。在托管的 Kubernetes 集群里Pod 的 DNS 查询默认会先发给集群内的 CoreDNS再由 CoreDNS 转发给 VPC 内的 DNS 服务。只要私有域创建时关联了当前 VPCCoreDNS 的转发目标 VPC DNS 就能递归解析到私有域记录Pod 就能拿到结果。如果你的内部 DNS 不是云厂商私有域而是自建 CoreDNS 作为权威服务那需要把集群的 CoreDNS 配置成“特定域转发到特定权威服务器”。比如在 CoreDNS 的 ConfigMap 里加一段internal.example.local:53 { forward 10.10.0.53 10.10.0.54 }这样 Pod 查询xxx.internal.example.local时集群 CoreDNS 会把请求转发到权威 DNS而不是走公网递归。这个细节如果漏配置就算外部 DNS 记录已经存在Pod 依然解析不了。4. 实战配置ExternalDNS 部署参数与 Ingress 注解一次讲透环境就绪后进入正题。我先给出一份可直接落地的 Deployment 配置然后逐项拆解参数含义再说 Ingress 端要怎么做。4.1 部署 ExternalDNS 的完整 YAML假设你已经创建好了external-dns这个 Namespace并且把 DNS 凭证放进了 Secret。Deployment 大致长这样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: [networking.k8s.io] resources: [ingresses] verbs: [get, list, watch] - apiGroups: [] resources: [services, endpoints, pods, nodes] verbs: [get, list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns namespace: external-dns spec: replicas: 1 selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.14.2 args: - --sourceingress - --domain-filterdev.internal.example.local - --provider你的provider - --policyupsert-only - --registrytxt - --txt-owner-idcluster-dev - --interval1m - --log-levelinfo env: - name: DNS_API_TOKEN valueFrom: secretKeyRef: name: dns-secret key: token注意几个和实际环境强相关的点。你的provider需要替换成 ExternalDNS 支持的对应 provider 标识不同云厂商差异很大。如果你用的是自建 CoreDNS 权威模式那这里是--providercoredns同时还要配置 etcd 相关参数如果你用的是 RFC2136 协议兼容的 DNS 服务器那要配--providerrfc2136。不管哪种原理都是一样的ExternalDNS 调用服务商 API 写记录。4.2 核心启动参数逐项解析这里把我常用的参数整理成一个表方便你对着看参数作用我的建议值--sourceingress指定数据源类型必须内部域名走 Ingress--domain-filter只管理匹配该域名的记录精确到环境子域--policyupsert-only只新增或更新不删除初期建议稳定后可切 sync--registrytxt通过 TXT 记录标记所有权必须开启--txt-owner-id当前实例的唯一标识集群名环境名--interval1m监听之外的定期同步周期1m 比较平衡--log-levelinfo日志级别调试时改 debug--policy这个参数值得单独说。upsert-only是安全的它只会新增或更新记录绝不动删除操作。但这也意味着如果你删掉了某个 IngressDNS 记录不会自动清理会残留一条指向旧地址的记录。如果你的 DNS Zone 是专供这套集群使用的子域比如dev.internal.example.local下面所有记录都由 K8s 管理那我建议最终切到--policysync。sync 模式下 ExternalDNS 会定期比对期望状态和实际状态把不再被任何 Ingress 引用的记录删掉。这个模式确实好用但前提是 Zone 必须干净如果里面有手工维护的历史记录sync 可能会误删所以需要谨慎。4.3 Ingress 端需要做什么Ingress 端的配置比想象中少。大多数情况下你只需要在 Ingress 的spec.rules[].host里写清楚域名就行ExternalDNS 会自动读取apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: order-api namespace: business annotations: nginx.ingress.kubernetes.io/rewrite-target: / external-dns.alpha.kubernetes.io/ttl: 300 spec: ingressClassName: nginx rules: - host: order.dev.internal.example.local http: paths: - path: / pathType: Prefix backend: service: name: order-api port: number: 80ExternalDNS 发现这个 Ingress 后会在dev.internal.example.local域里创建order.dev.internal.example.local的 A 记录目标地址取自 Ingress Controller 的负载均衡 IP。不过有两个坑要注意。第一如果 Ingress 的 host 不在--domain-filter匹配范围内ExternalDNS 会直接忽略它不会报错只会静默跳过。第二如果 Ingress 的 Status 里还没有负载均衡地址ExternalDNS 就无法知道目标 IP记录也不会生成。所以 Ingress 创建后最好确认一下kubectl get ingress的 ADDRESS 列有没有值。还有一种更手动但灵活的方式通过注解明确指定域名和目标地址metadata: annotations: external-dns.alpha.kubernetes.io/hostname: order.dev.internal.example.local external-dns.alpha.kubernetes.io/target: 10.0.1.10用了这个注解后ExternalDNS 就不再依赖 Ingress 的 host 字段了而是直接使用你指定的值和目标。这个方式适合那些 Ingress host 比较特殊、或者目标负载均衡地址暂时无法通过 Status 暴露的场景。4.4 多环境隔离的两种方式我知道很多团队一开始会把 dev、test、prod 的 Ingress 放在同一个集群或者同一批域名空间里。这种情况下隔离做得不好后面必乱。第一种方式每个环境一个--domain-filter值。比如 dev 集群的 ExternalDNS 只管理dev.internal.example.localtest 集群只管理test.internal.example.local。这样就算两个集群的 ExternalDNS 同时运行它们也是各管各的域互不干扰。第二种方式同一个域多个--txt-owner-id。适合两个集群共享同一个 DNS Zone 的场景。每个集群的 ExternalDNS 用不同的 owner 标识通过 TXT 记录区分自己的资源但共享 Zone 的记录列表依然可能互相影响所以除非确实需要共享域否则不建议这么干。我个人的经验是优先用第一种把域先拆开后续不管是权限控制还是排错都清晰太多了。5. 验证和排错从日志、DNS 查询到控制台的完整链路配置写完之后最怕的就是“看起来没问题但记录不出来”。这一章把验证路径和常见问题整理出来你可以照这个顺序逐步排查。5.1 先看 ExternalDNS 日志ExternalDNS 跑起来后第一件事就是看日志kubectl logs -f -n external-dns deploy/external-dns正常情况下日志里会周期性输出类似“Desired change: CREATE order.dev.internal.example.local A 10.0.1.10”这样的信息。看到这个就说明 ExternalDNS 已经识别到 Ingress正准备创建记录。如果日志里长时间没有任何输出说明 ExternalDNS 没有发现你新建的 Ingress。可能的原因有三个RBAC 权限没给够导致它 Watch 不到 Ingress--sourceingress没有配置或者 Ingress 的 host 不在--domain-filter范围内。调试阶段我建议先开--log-leveldebug因为 info 级别下很多跳过原因是不打印的。比如域名不匹配时info 级别不会告诉你为什么忽略debug 级别则会明确指出。5.2 从 Pod 内部验证解析结果日志显示创建成功之后还需要确认解析链路真的通了。直接在集群里起一个临时的 Pod 去查询kubectl run dns-test --rm -it --restartNever --imagebusybox -- nslookup order.dev.internal.example.local如果返回了 LB 地址说明链路通。如果返回server cant find说明 ExternalDNS 那侧可能根本没写成功或者写到了别的域。还有一个很常见的现象在宿主机上能解析在 Pod 里解析不到。这种时候基本可以确定是 CoreDNS 转发配置的问题回到 3.4 节检查那个转发段。5.3 我整理了一版问题排查对照表现象最可能原因排查动作日志一直没有变化domain-filter 没匹配查看 Ingress host 和 filter 是否一致日志报权限错误凭证权限不足确认凭证能读写该 DNS Zone日志显示 CREATE 但 DNS 没记录同步周期未到或控制台延迟等 1-2 个周期再刷新控制台DNS 记录在但 Pod 解析不了CoreDNS 转发配置问题检查 CoreDNS ConfigMap解析到旧地址LB 地址变过记录没更新查看日志里的 target 值确认是否使用 Sync 策略记录被反复覆盖多实例共用 owner配置独立的 txt-owner-id5.4 一次记录没出现的完整排查过程我记得有一次新建了一个 Ingress等了五分钟记录都没出现。日志里反复出现“no matching record found in zone”这句话。第一次看的时候我以为是 DNS Zone ID 配错了翻来覆去检查配置都没发现问题。后来我把日志级别调到 debug才发现 ExternalDNS 解析出来的域名是Order.Dev.Internal.Example.Local而我配置的--domain-filter是小写dev.internal.example.local。DNS 域名本身不区分大小写但 ExternalDNS 在匹配 Zone 时是直接做字符串比较的大小写不一致就会匹配不上。改成小写之后下一个同步周期记录就自动出来了。这类问题在文档里几乎不会写但它真实存在一旦遇到会卡你很久所以我把这个案例拿出来说。还有一次是目标地址问题。当时 Ingress Controller 刚重建Status 里暂时没有 LB 地址ExternalDNS 日志一直提示“no targets”。我等了一会儿再刷新地址状态恢复后才正常创建记录。这种情况不用紧张ExternalDNS 会持续重试不会漏掉。6. 进阶事项多集群隔离、证书联动和运维边界基础链路通了之后还有几个问题会随着规模增长冒出来。这里把我在实际中遇到的进阶场景一并说说。6.1 多集群共用域名的竞争问题如果你的团队有多个集群而它们都往同一个 DNS Zone 写记录就要小心了。即便有 TXT ownership 机制当两个集群分别创建同一个域名的 Ingress 时ExternalDNS 依然会去写同一条 A 记录只会有一个集群的目标地址生效。这种情况往往不是配置错误而是架构设计的问题。最稳妥的办法还是回到子域隔离每个集群用独立的子域各自管理各自的记录。如果确实需要同一个域名承载流量到多个集群那应该在 Ingress Controller 这一层做统一负载均衡而不是让 ExternalDNS 同时管理同一条记录。6.2 和 cert-manager 联动实现证书自动签发域名解析自动化之后下一个自然的需求是证书自动化。内部服务走 HTTPS可以引入 cert-manager配置一个内部 CA 的 ClusterIssuer然后给 Ingress 加 TLS 配置spec: tls: - hosts: - order.dev.internal.example.local secretName: order-tlsExternalDNS 负责让域名解析到正确的 LB 地址cert-manager 负责签发证书并注入到 Ingress Controller。两者配合起来整个服务上线的流程就变成了创建 Ingress → 域名自动解析 → 证书自动签发 → 服务自动可访问。要注意的是内部 CA 的根证书需要分发给集群内所有需要访问该域名的客户端。否则应用虽然能解析到 IP但在 TLS 握手阶段会因为不信任根证书而失败。6.3 ExternalDNS 的运维边界ExternalDNS 并不是万能的记录管理器它只管自己创建并拥有所有权标记的记录。手工在 DNS 控制台里加的历史记录它不会主动删除哪怕这些记录和当前的 Ingress 冲突但如果你开了 sync 策略它也不会去清理那些没有 TXT ownership 标记的记录。所以我把这个边界概括为ExternalDNS 是一个“声明式同步器”它的期望状态来自集群里的 Ingress它只对自己的所有权记录负责。上线前最好把 Zone 里已有的多余记录清理干净否则容易混淆。6.4 个人的三条经验最后分享三点实操体会。第一域名命名一定要克制子域划分要早做不然环境一多记录根本没法看。第二不要在 production 的共享 Zone 上一上来就开sync策略先用upsert-only跑一段时间确认日志里没有任何异常删除行为之后再切。第三日志级别平时用info就行排错再切debug不然 ExternalDNS 的日志量大到根本不想看。这套方案跑到现在最大的感受就是省心。以前每次上线都要惦记着 DNS 记录加没加现在只要 Ingress 提交进集群剩下的交给 ExternalDNS 就行。自动化之后人出错的空间也小了很多至少我再没因为漏加解析记录被拉去排查问题。如果你也在被内部域名维护折磨这套链路值得花半天时间搭一遍。