如何在 Kubernetes 与 OpenVPN sidecar 之间划分 DNS
sidecar Pod 需要两个名称世界。把一个 loopback resolver 指向集群与隧道,并停止依赖被手工修改的 resolv.conf。
运行 OpenVPN sidecar 的 Pod 在名称解析上会出现可识别的故障:集群名称挂起,远端名称能应答直到 Pod 重启,而你改过的 resolver 文件又恢复原样。这一连串现象没有一个是隧道故障。一个 Pod 只有一个 resolver 文件却有两个名称世界,而该文件只能按顺序列出服务器,无法表达区域。持久的解法是在 Pod 内运行一个拥有划分权的 resolver,这样文件保持简单,也没有任何策略有理由重写它。
你做的修改为何总是消失
这个文件属于 kubelet,不属于你。在 dnsPolicy: ClusterFirst 下,kubelet 生成它指向集群 DNS,并在容器每次启动时重新生成。你通过 kubectl exec 所做的修改都活在容器的可写层里,所以重启、驱逐或 rollout 都会把它丢掉。Pod 规格仍然带着同一条策略,因此同一个文件又回来了。
设置 dnsPolicy: None 会停止重新生成,同时也去掉集群解析。你得到自己掌控的文件,却失去了需要的名称。这个取舍正是整个问题所在:resolv.conf 不懂区域,只懂按顺序尝试的服务器,因此无法把 cluster.local 送到一个地址、把私有区域送到另一个地址。
在 Pod 内跑一个 resolver,监听 loopback
把 Pod 指向 loopback 上的单个名称服务器,再让共享 Pod 网络命名空间的 CoreDNS 容器在 53 端口应答。将策略设为 None,给 Pod 这个唯一的地址,并把 DNS 容器加在 VPN 和应用旁边:
spec:
dnsPolicy: None
dnsConfig:
nameservers:
- 127.0.0.1
options:
- name: ndots
value: "5"
containers:
- name: dns
image: coredns/coredns
args: ["-conf", "/etc/coredns/Corefile"]
volumeMounts:
- {name: corefile, mountPath: /etc/coredns, readOnly: true}
- name: vpn
image: your-openvpn-client-image
- name: app
image: your-app-image
volumes:
- name: corefile
configMap:
name: split-dns-corefile三个容器共享同一个网络命名空间,因此应用容器内的 127.0.0.1:53 无需 Service、无需 port-forward、也不必绕经集群网络就能到达 CoreDNS。从 ConfigMap 挂载 Corefile 让它成为有版本管理的输入,而不是就地编辑的文件。生成的 resolv.conf 之后只包含一个名称服务器,别无他物,这是一个可以推理的状态。
这里有两个细节会咬人。官方镜像以 root 身份绑定 53 端口,某些集群策略要求你明确写出这一点。Pod 里的每个容器现在都依赖那一个容器的运行,所以要为一个无害名称加上 readiness 探针,并让应用能容忍启动失败。
为每个私有区域提供自己的 upstream
Corefile 是一个区域列表,每个区域指明能为它应答的服务器。一个块覆盖隧道区域,兜底块放在最后,接收所有没有被显式块认领的名称。兜底块必须指向真实的 upstream,绝不能指回 Pod:.:53 { forward . 10.100.0.10 } 会复用集群 resolver,因为它早已把公共名称转发给节点自己的 resolver。Pod 里其他任何东西都不需要知道名称属于哪个网络。
.:53 {
errors
cache 30
forward . 10.100.0.10
}
svc.acme.internal:53 {
errors
cache 30
forward . 192.168.240.1
}从 kube-system 中的 kube-dns Service 读出集群地址,而不要从文章里照抄,因为 Service 的 CIDR 各不相同。隧道地址来自 OpenVPN 服务器下发的 dhcp-option DNS 值,或来自该区域的权威服务器。如果你的集群 resolver 不解析公共名称,就把节点的 resolver 地址放进兜底块,并且要在节点上读取,而不是在 Pod 内。
不要把兜底块指向 /etc/resolv.conf,它是那个诱人的一行配置,也正是这套设置里的陷阱。在 dnsPolicy: None 下,Pod 自己的文件里只有唯一名称服务器 127.0.0.1,所以那个目标就是 CoreDNS 本身,每个不匹配的查询都会重试到超时。名为 svc.acme.internal 的块匹配该名称及其下所有名称,覆盖 db.svc.acme.internal 并排除 svc.acme.internal.example.net。结尾的点只是形式,因为区域名称会被规范化为完全限定形式;后缀边界才决定查询落在哪里。
判断转发是否应归集群 resolver
如果命名空间中的每个工作负载都需要隧道区域,那么在 kube-system 下的 CoreDNS ConfigMap 里加一个转发块,比为每个 Pod 配 sidecar 更省。这个改动是集群级的:需要管理员权限,并重启所有人都依赖的部署。把它只限制在那一个区域,绝不要指向根。某个工作负载的隧道地址于是成为该区域所有 Pod 的 resolver,而隧道垮掉的那天整个集群都会失去这个名称。
sidecar 把爆炸半径留在一个 Pod 内,当只有少数服务需要远端网络时,这是更好的默认值。无论选哪种,都记下哪个 resolver 拥有该区域,因为下一位排查超时的人一定会问。
连接仍然失败时却能解析的名称
解析是隧道必须承载的两件事之一;另一件是回程路径,而 Pod 网络把它藏得很好。如果 OpenVPN 服务器本身以 Pod 方式运行,你加在那个容器里的规则并不管控经由节点外出的流量。这个故障很窄:只有同一节点上运行该服务器的 Pod 拿到了应答,其余全部超时。
用 hostNetwork: true 运行服务器会把它放回节点的命名空间,那里的 FORWARD 规则和源地址转换才会生效。离开时要做客户端池转换,否则没有任何远端节点有回到 10.8.0.6 这类隧道地址的路由。再次动 Corefile 之前,请检查两个方向。只带去程查询却不带应答的隧道,产生的超时与从未收到查询的 resolver 完全相同。
从新的 Pod 验证,而不是从你修好的那个会话
显式询问两个区域,然后在 Pod 被替换后重复查询。只存在于运行中容器里的配置不算配置:
kubectl exec -it deploy/app -- dig @127.0.0.1 kubernetes.default.svc.cluster.local +short
kubectl exec -it deploy/app -- dig @127.0.0.1 db.svc.acme.internal +short
kubectl delete pod -l app=your-app
kubectl wait --for=condition=Ready pod -l app=your-app --timeout=90s
kubectl exec -it deploy/app -- dig @127.0.0.1 db.svc.acme.internal +short如果第一组应答而替换后的那组不应答,说明 Corefile 正确而 Pod 规格不对。如果两者都应答但应用仍连不上,就停止调试 DNS,改从容器内部用 nc -vz db.svc.acme.internal 5432 测试 TCP 路径。把 dig 的输出留在工单里,因为正是它把名称问题与路由问题区分开。