
K8s CoreDNS 定制化自定义域名解析与上游转发策略DNS 是 K8s 集群的电话簿。书页折了一个角整个集群都在打错电话。一、场景痛点微服务 A 调用微服务 B通过b-service.default.svc.cluster.local访问。某天运维把 B 迁移到了另一个 K8s 集群在入口网关做了流量转发。但 A 里面的代码写死了b-service这个 Service 名——域名解析还是指向了老集群的空 Service所有调用 5 秒超时。方案有两个改代码把域名从b-service改成b-service.new-cluster.example.com——需要改 15 个微服务协调 3 个团队上线窗口排到两周后在 CoreDNS 里加一条自定义解析规则把b-service重写为外部域名 ——5 分钟改完即刻生效这就是 CoreDNS 定制化的价值。K8s 自带的 DNS 只解决集群内 Service 发现但当你的架构发展到混合云、多集群、外部服务集成时你需要 DNS 层具备路由重写、上游转发、条件解析的能力。CoreDNS 的 Corefile 插件体系就是为这些场景设计的。二、底层机制与原理剖析2.1 CoreDNS 的插件链模型插件链按顺序执行。如果kubernetes插件匹配到了 Service直接返回结果不经过rewrite。如果没匹配到继续走rewrite→forward。关键插件顺序决定了解析优先级。把rewrite放在kubernetes前面可以实现优先自定义规则没命中再走 Service 发现。2.2 Corefile 的核心配置段# CoreDNS 的标准 Corefile 结构 .:53 { # Zone 定义: . 表示处理所有域的查询 # 1. 基础插件日志、缓存、健康检查 errors # 将错误输出到标准输出 health { # 健康检查端点 :8080/health lameduck 5s # 优雅关闭前等待 5 秒 } ready # 就绪探测端点 :8181/ready # 2. 核心解析插件按优先级排序 kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa ttl 30 } # 3. 域名重写自定义规则 rewrite stop { # 规则定义... } # 4. 上游转发 forward . /etc/resolv.conf { max_concurrent 1000 } # 5. 缓存 cache 30 loop reload loadbalance }2.3 请求流转的完整路径Pod 发起 DNS 查询 b-service.default.svc.cluster.local │ ├─ Step 1: 检查 Pod 的 /etc/resolv.conf │ └─ nameserver 指向 kube-dns (CoreDNS) Service ClusterIP │ ├─ Step 2: 请求到达 CoreDNS │ └─ 匹配 Corefile 中的插件链 │ ├─ Step 3: kubernetes 插件检查 │ ├─ b-service 在 default namespace 的 Service 列表? │ │ ├─ 是 → 返回 ClusterIP 10.96.1.5 │ │ └─ 否 → fallthrough 到 rewrite 插件 │ │ ├─ Step 4: rewrite 插件处理 │ ├─ 匹配 rewrite 规则? │ │ ├─ 是 → 改写域名继续下一个插件 │ │ └─ 否 → fallthrough │ ├─ Step 5: forward 插件转发 │ └─ 转发到上游 DNS 服务器 (/etc/resolv.conf) │ └─ Step 6: 返回 IP 地址给 Pod三、生产级代码实现3.1 四大典型场景的 CoreDNS 配置# # ConfigMap: coredns-custom # K8s 1.18 推荐方式: 在 coredns ConfigMap 同级目录添加自定义配置 # --- apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: # 核心 Corefile主配置文件 Corefile: | .:53 { errors health { lameduck 5s } ready # K8s 集群内域名解析 # cluster.local 是默认的集群域名后缀 kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa ttl 30 } # 引入所有 Server Block 文件 # 每个场景一个独立的 Server Block 文件 import custom/*.server # 自定义域名重写规则 import custom/*.rewrite # 上游 DNS 转发 forward . /etc/resolv.conf { max_concurrent 1000 policy sequential # 按顺序尝试上游 DNS } cache 30 loop reload loadbalance } --- # # 场景 1: 域名重写internal DNS alias # 文件: custom/rewrite.rewrite # # 问题: 服务从 K8s 内迁移到了外部但 15 个微服务代码里写死了老域名 # 解决: DNS 层做域名重写不需要改业务代码 # # 1.1 把集群内域名重写为外部域名不带 stop 关键字继续走 forward rewrite name b-service.default.svc.cluster.local b-service.prod.example.com # 1.2 环境感知的重写根据请求来源 namespace rewrite name regex (.*)\.default\.svc\.cluster\.local {1}.production.example.com # 1.3 把老系统的域名映射到新的微服务 rewrite name legacy-api.internal.local api-gateway.default.svc.cluster.local --- # # 场景 2: 上游转发策略多上游 分区解析 # 文件: custom/forward.server # # 问题: 部分外部域名需要走特定的 DNS 服务器如公司内网 DNS # 解决: 按域名分区不同域走不同的上游 # # 2.1 公司内网域名走内网 DNS internal.example.com:53 { errors cache 30 forward . 10.0.1.53 10.0.1.54 { max_concurrent 1000 policy round_robin # 轮询上游 DNS 服务器 } } # 2.2 外部域名走阿里云公共 DNS100.100.2.136 加密 DNS external-vendor.example.com:53 { errors cache 60 forward . 100.100.2.136 100.100.2.138 { tls_servername dns.aliyun.com # TLS 加密 DNS 查询防止中间人劫持 max_concurrent 500 } } # 2.3 默认外部域名走上游 .:53 { errors cache 30 forward . 223.5.5.5 119.29.29.29 { # 阿里 腾讯公共 DNS max_concurrent 1000 policy sequential # sequential: 第一个失败才尝试第二个避免不同 DNS 返回结果不一致 } } --- # # 场景 3: 条件解析不同 namespace 走不同解析路径 # 文件: custom/conditional.rewrite # # 问题: staging 环境的服务需要调用 staging 的外部 API # 解决: 根据请求来源 namespace返回不同的解析结果 # # 3.1 staging namespace 的服务调用外部 staging API rewrite name api.external-service.example.com api.staging.example.com # 3.2 使用 response rewrite 实现视网段返回 # CoreDNS 不原生支持按来源 IP 路由通过 template 插件实现: template IN A { # 当查询 *.staging.svc.cluster.local 时 match ^(.*)\.staging\.svc\.cluster\.local\.$ # 返回固定的 staging 网关 IP answer {{ .Name }} 60 IN A 10.96.100.1 fallthrough } --- # # 场景 4: 自定义 DNS 劫持测试/调试场景 # 文件: custom/override.server # # 问题: 需要将某个外部 API 的流量劫持到本地的 mock 服务做测试 # 解决: 通过 hosts 插件返回 mock 服务的 ClusterIP # hosts { # 将第三方支付网关劫持到集群内的 mock 服务 10.96.50.10 payment-gateway.external-bank.example.com 10.96.50.10 api.payment.external-bank.example.com # 将外部分析服务劫持到内网测试实例 10.96.50.20 analytics-collector.saas-vendor.example.com fallthrough # 未匹配的域名继续走下游插件 }3.2 运维脚本DNS 解析诊断工具#!/bin/bash # # dns-diagnose.sh # K8s DNS 解析诊断脚本 # 从 Pod 内部测试 DNS 解析链路定位问题环节 # set -euo pipefail NAMESPACE${1:-default} TEST_DOMAIN${2:-b-service.default.svc.cluster.local} COREDNS_SVC_IP${3:-10.96.0.10} # kube-dns Service ClusterIP echo echo K8s DNS 诊断: ${TEST_DOMAIN} echo Namespace: ${NAMESPACE} echo # Step 1: 检查 /etc/resolv.conf 配置 echo echo [1/7] 检查 Pod DNS 配置 (/etc/resolv.conf) echo ------------------------------------------- cat /etc/resolv.conf NDOTS$(grep options /etc/resolv.conf | grep -oP ndots:\d || echo ndots:5) echo echo 注意: ${NDOTS}如果域名中的点号 ndots会先尝试 search domain 后缀 # Step 2: 使用 nslookup 解析 echo echo [2/7] nslookup 解析 (通过 /etc/resolv.conf) echo ------------------------------------------- nslookup ${TEST_DOMAIN} 21 || echo 解析失败 # Step 3: 直接查询 CoreDNS echo echo [3/7] 直接查询 CoreDNS (${COREDNS_SVC_IP}) echo ------------------------------------------- nslookup ${TEST_DOMAIN} ${COREDNS_SVC_IP} 21 || echo CoreDNS 查询失败 # Step 4: 查询上游 DNS UPSTREAM_DNS$(grep ^nameserver /etc/resolv.conf | head -1 | awk {print $2}) echo echo [4/7] 查询上游 DNS (${UPSTREAM_DNS}) echo ------------------------------------------- nslookup ${TEST_DOMAIN} ${UPSTREAM_DNS} 21 || echo 上游 DNS 查询失败 # Step 5: 绕开 search domain 做精确查询 echo echo [5/7] 精确查询 (加末尾点号绕开 search domain) echo ------------------------------------------- nslookup ${TEST_DOMAIN}. ${COREDNS_SVC_IP} 21 || echo 精确查询失败 # Step 6: 使用 dig 查看解析详情 echo echo [6/7] dig 详情 (查询时间 查询链) echo ------------------------------------------- dig ${TEST_DOMAIN} ${COREDNS_SVC_IP} noall answer stats 21 || echo dig 查询失败 # Step 7: CoreDNS metrics 检查需要从 CoreDNS Pod 或 metrics 端点 echo echo [7/7] CoreDNS 指标检查 echo ------------------------------------------- # 检查 CoreDNS 的 Prometheus metrics echo CoreDNS metrics 暴露在 :9153/metrics echo 关键指标: echo coredns_dns_requests_total - 总请求数 echo coredns_dns_responses_total - 总响应数 echo coredns_dns_request_duration_seconds - 请求延迟 echo coredns_forward_requests_total - 转发请求数 echo coredns_forward_responses_total - 转发响应数 echo coredns_cache_hits_total - 缓存命中数 echo coredns_cache_misses_total - 缓存未命中数 echo echo echo 诊断完成 echo 3.3 验证配置# 使用 test-pod 验证 DNS 解析 apiVersion: v1 kind: Pod metadata: name: dns-test namespace: default spec: containers: - name: test image: busybox:1.36 command: - sleep - 3600 resources: limits: memory: 128Mi cpu: 100m restartPolicy: Never --- # 执行验证命令 # kubectl exec -it dns-test -- nslookup b-service.default.svc.cluster.local # kubectl exec -it dns-test -- nslookup external-api.example.com # kubectl exec -it dns-test -- nslookup internal-service.corp.local3.4 Python 客户端DNS 解析监控 DNS 解析监控工具 定期探测关键域名的解析延迟异常时告警 监控维度: 1. 解析成功/失败计数 2. 解析延迟分布 (P50/P95/P99) 3. CoreDNS 上游转发延迟 4. 域名解析结果变化检测IP 漂移 import asyncio import socket import time from dataclasses import dataclass, field from collections import defaultdict dataclass class DNSProbeResult: domain: str resolved_ips: list[str] latency_ms: float success: bool error: str nameserver: str timestamp: float field(default_factorytime.time) class DNSMonitor: K8s DNS 解析监控器 监控目标: 1. 集群内 Service 域名解析 2. 外部域名解析经 CoreDNS forward 3. 跨环境域名解析 def __init__(self, nameserver: str 10.96.0.10): self.nameserver nameserver # CoreDNS ClusterIP # 关键域名列表需要持续监控 self.critical_domains [ # 集群内 Service kubernetes.default.svc.cluster.local, kube-dns.kube-system.svc.cluster.local, # 外部依赖 api.openai.com, api.github.com, # 内部服务 redis-master.default.svc.cluster.local, postgres-rw.database.svc.cluster.local, ] # 指标存储 self.metrics: dict[str, list[DNSProbeResult]] defaultdict(list) async def probe_domain(self, domain: str) - DNSProbeResult: 探测域名的 DNS 解析 使用 socket 做解析测量端到端延迟 注意: 这会使用 /etc/resolv.conf 中的 nameserver 即 CoreDNS 的 ClusterIP start time.perf_counter() try: # 使用 asyncio 的 getaddrinfo 做异步 DNS 解析 loop asyncio.get_event_loop() addrs await loop.getaddrinfo( domain, None, familysocket.AF_INET, protosocket.IPPROTO_TCP ) elapsed time.perf_counter() - start ips list(set(addr[4][0] for addr in addrs)) return DNSProbeResult( domaindomain, resolved_ipsips, latency_mselapsed * 1000, successTrue, nameserverself.nameserver ) except Exception as e: elapsed time.perf_counter() - start return DNSProbeResult( domaindomain, resolved_ips[], latency_mselapsed * 1000, successFalse, errorstr(e), nameserverself.nameserver ) async def run_probe_cycle(self) - dict[str, DNSProbeResult]: 执行一轮全量探测 tasks [self.probe_domain(d) for d in self.critical_domains] results await asyncio.gather(*tasks) # 存储结果 for result in results: self.metrics[result.domain].append(result) # 只保留最近 100 条记录 if len(self.metrics[result.domain]) 100: self.metrics[result.domain] self.metrics[result.domain][-100:] return {r.domain: r for r in results} def check_for_ip_drift(self, domain: str) - bool: 检测 DNS 解析结果的 IP 是否发生变化 IP 漂移可能是 DNS 劫持或 CoreDNS 配置错误 results self.metrics.get(domain, []) if len(results) 2: return False # 获取最近两个解析结果 latest_ips set(results[-1].resolved_ips) previous_ips set(results[-2].resolved_ips) return latest_ips ! previous_ips def report(self) - str: 生成 DNS 监控报告 lines [\n DNS 解析监控报告, * 50] for domain in self.critical_domains: results self.metrics.get(domain, []) if not results: lines.append(f\n {domain}: 无数据) continue success_count sum(1 for r in results if r.success) latencies [r.latency_ms for r in results if r.success] if latencies: latencies.sort() p50 latencies[len(latencies) // 2] p95 latencies[int(len(latencies) * 0.95)] p99 latencies[int(len(latencies) * 0.99)] else: p50 p95 p99 0 status ✅ if success_count / len(results) 0.99 else ⚠️ lines.append( f\n{status} {domain} f\n 成功率: {success_count}/{len(results)} ({success_count/len(results)*100:.1f}%) f\n 延迟: P50{p50:.1f}ms P95{p95:.1f}ms P99{p99:.1f}ms ) if self.check_for_ip_drift(domain): lines.append( ⚠️ IP 地址已发生变化请检查!) return \n.join(lines) async def main(): monitor DNSMonitor() # 持续探测每 30 秒一次 while True: await monitor.run_probe_cycle() print(monitor.report()) await asyncio.sleep(30) if __name__ __main__: asyncio.run(main())四、边界分析与架构权衡4.1 什么时候该用 CoreDNS 定制化什么时候不该用该用的场景跨集群服务迁移不改代码的情况下切换域名指向混合云 DNS 分区内网域名走内网 DNS外部域名走公共 DNS测试环境劫持把外部依赖重定向到 mock 服务DNS 层面的灰度发布按 namespace 返回不同版本的服务 IP不该用的场景作为 API 网关的替代品DNS 只能解析域名不能做协议转换、限流、认证频繁变化的动态路由DNS 有缓存TTL 内修改不生效跨地区智能路由GeoDNS 更适合CoreDNS 不原生支持按客户端 IP 路由4.2 DNS 缓存导致的问题CoreDNS 默认缓存 30 秒。如果你的服务切换了 IP 但 Pod 还在用缓存会有 30 秒的盲视期。对于滚动更新来说不成问题新旧 Pod 并存但对于紧急切换场景你需要# 关键服务的 DNS 记录使用低 TTL kubernetes cluster.local { ttl 5 # 非默认的 5 秒 TTL而不是 30 秒 } # 或者在 Service 上使用 annotation 指定 TTL metadata: annotations: external-dns.alpha.kubernetes.io/ttl: 54.3 性能影响每条 rewrite 规则都会增加约 1-5 微秒的解析时间。如果配置了 50 条 rewrite 规则P99 解析延迟可能从 1ms 涨到 5ms。建议rewrite 规则总数 20 条使用正则匹配而不是逐条精确匹配监控coredns_dns_request_duration_seconds指标4.4 安全注意事项DNS 重写可能导致中间人攻击风险如果攻击者能修改 ConfigMap可以将payment.example.com重写为攻击者控制的 IP。防护措施CoreDNS ConfigMap 的修改必须走 GitOps PR review对关键域名支付、认证使用 DNSSEC 验证启用 CoreDNS 的acl插件限制查询来源五、总结CoreDNS 定制化的核心思想是在 DNS 层解决问题而不是在应用层打补丁。DNS 是 K8s 最上层的基础设施——改一次配置所有 Pod 自动生效。相比于让 15 个微服务改代码、协调发布时间窗口DNS 层的修改是最高效的。三个最重要的配置模式rewrite域名别名、内部域名到外部域名的映射forward按域分流到不同的上游 DNS 服务器hosts/template精确控制特定域名的解析结果记住一个原则DNS 修改是全局生效的改之前先在测试环境验证改之后看 CoreDNS metrics 确认生效了。别在凌晨 3 点的生产环境直接改 Corefile——这种事干一次就够了。