本篇文章是「DevOps云学堂」与你共同进步的第 67篇
Kubernetes in Docker (KIND) 是一个由 Kubernetes SIG 社区维护的开源项目。该项目的目的是使用Docker提供一个简单的Kubernetes环境,主要用于Kubernetes CI测试
。
Kubernetes本身是一个容器编排平台,因此使用Docker作为其节点会产生基于容器中容器概念的架构
。这种方法的实现过程也引入了与双层容器
相关的挑战。本文重点讨论这一过程中出现的与 DNS 相关的一个具体实施问题。
KIND 的架构构建于 Docker 之上
。本文的实验设置涉及在 Ubuntu 20.04 上使用 KIND 创建一个三节点 Kubernetes 集群
。其中一个节点作为控制平面,另外两个节点作为普通工作人员。
在 Docker 环境中,需要启动三个 Docker 容器来模拟 Kubernetes 节点。这些容器使用 Docker 网络相互通信,以解决网络连接问题。整体架构如下图所示:
在此环境中,将部署三个容器。每个容器在 Docker 网络子网中都将拥有自己的 IP
,并且它们都将包含各自容器映像中包含的应用程序和库。
对于Kubernetes来说,为了实现三节点的Kubernetes集群,集群内必须有一个控制平面,提供etcd、调度器、控制器和API服务器等功能。此外,每个节点都需要安装 kubelet 和相关的容器运行时来管理容器的生命周期。该概念如下图所示:
KIND 环境结合了这两个方面。因此,整体架构如下:
Kubernetes 如何在 KIND 中工作
通过 Docker 启动的容器将安装 Containerd 来管理 Kubernetes 容器的生命周期
。同时,通过kubelet等组件与控制平面建立连接
,形成Kubernetes集群。
熟悉 Docker Compose 的读者可能知道,为了方便容器之间的通信,可以直接使用容器名称作为 DNS 目标
。这样的设计让容器无需担心IP变化。事实上,Docker 在系统中嵌入了一个 DNS 服务器
来处理这个问题,DNS 服务器的固定 IP 是 127.0.0.11
。
该 DNS 服务器的职责可分类如下:
转发到上游
DNS 服务器。在下面描述的示例中,两个名为hwchiu
和hwchiu2
的容器正在运行。使用nslookup,可以轻松解析对应的IP地址
。还可以观察到这些容器中的 /etc/hosts 文件动态指向 127.0.0.11。这意味着容器内的所有 DNS 请求都将重定向到内置的 Docker DNS 服务器。
然而,Docker 的 DNS 实现提出了一些值得探讨的问题:
为了解决这些不确定性,有必要了解 Docker DNS 的实现。通过深入了解其实施情况,我们可以为上述问题提供准确的答案。
Docker DNS 的设计非常巧妙,利用 Linux 命名空间
的概念无缝地解决了这个问题。它使所有 Docker DNS 服务器能够在主机本身(PID 命名空间内)上运行,而网络方面则在每个容器内(网络命名空间内)进行侦听
。
这种架构允许你通过127.0.0.11访问DNS服务器
,但是你在容器内找不到这个DNS服务器的进程。
此外,为了防止 Docker DNS 服务器与用户定义的 DNS 服务发生冲突,Docker DNS 避免使用端口 53,而是采用随机端口号
。
整体架构如下图所示:
但是,对于容器服务,/etc/hosts 已更改为使用 127.0.0.11 作为默认 DNS 搜索,并且通常依赖于在端口 53 上,Dockerd 依靠 iptables 来动态调整规则。这涉及修改发送到 127.0.0.11:53 的所有数据包的目标端口以处理连接问题。 因此,如果您使用 nsenter 等命令在容器内进行观察,请使用 ss 和 i ptables 命令,您将看到如下图所示的结果:
在此显示中,ss 显示在环境中,127.0.0.11 正在侦听两个端口,分别对应 TCP 和 UDP DNS 请求,进程为dockerd。iptables揭示了相关DNAT规则。 有了这些机制,来自容器的所有 DNS 请求都由 Docker DNS 处理,而不会抢占端口 53。 在这里您可以找到相关的源代码,https://github.com/moby/libnetwork/blob/67e0588f1ddfaf2faf4c8cae8b7ea2876434d91c/resolver_unix.go?WT.mc_id=AZ-MVP-5003331 它演示了Docker动态修改四个规则来满足DNAT + SNAT需求。
...
resolverIP, ipPort, _ := net.SplitHostPort(os.Args[2])
_, tcpPort, _ := net.SplitHostPort(os.Args[3])
rules := [][]string{
{"-t", "nat", "-I", outputChain, "-d", resolverIP, "-p", "udp", "--dport", dnsPort, "-j", "DNAT", "--to-destination", os.Args[2]},
{"-t", "nat", "-I", postroutingchain, "-s", resolverIP, "-p", "udp", "--sport", ipPort, "-j", "SNAT", "--to-source", ":" + dnsPort},
{"-t", "nat", "-I", outputChain, "-d", resolverIP, "-p", "tcp", "--dport", dnsPort, "-j", "DNAT", "--to-destination", os.Args[3]},
{"-t", "nat", "-I", postroutingchain, "-s", resolverIP, "-p", "tcp", "--sport", tcpPort, "-j", "SNAT", "--to-source", ":" + dnsPort},
}
...
到目前为止,您应该对 Docker DNS 有了基本的了解。接下来,让我们看看 Kubernetes 的情况。
在 Kubernetes 集群中,还有一个 DNS 服务器——从早期的 Kube-DNS 到现在的 CoreDNS
。它的功能与Docker DNS非常相似:
与 Docker DNS 类似,两者都旨在处理特定于服务的请求并在必要时进行转发
。
对于CoreDNS来说,上游DNS服务器默认是节点使用的DNS服务器,与前面提到的127.0.0.11相同。
该过程展开如下:
CoreDNS 如何处理 DNS
假设“worker2”上的 Pod 想要发出 DNS 请求。该请求被定向到 CoreDNS
。当 CoreDNS 无法解析它并尝试将其转发到上游 DNS 服务器时,它最终会转发到 127.0.0.11
,这就是问题出现的地方。
由于 127.0.0.11 是 Dockerd 特有的,因此 Docker 容器上 Containerd 管理的 CoreDNS 自然缺乏此功能
。它不具备相关的 iptables 和 Docker DNS 服务器
。
为了解决这个问题,KIND 的方法是阻止 CoreDNS 向 127.0.0.11 发送数据包。相反,CoreDNS 将它们发送到节点的 IP,然后将数据包转发到节点上的 127.0.0.11 服务
。
流程概述如下:
解决 CoreDNS 问题的想法
如前所述,CoreDNS 本身使用节点的 /etc/hosts 作为上游服务器
。这里的做法是动态修改节点的/etc/hosts
,将默认的DNS服务器从127.0.0.11改为节点自己的IP,比如示例图中的 172.18.0.2。
一旦默认请求位置发生更改,Dockerd 设置的 iptables 规则将不再适用。这就需要再次调整iptables规则
。
Dockerd 设置的初始 iptables 规则如下:
KIND 更改之前的 iptables 但是,KIND 将它们修改为以下内容(示例来自不同节点,节点 IP 为 172.18.0.1):
KIND 更改后的 iptables 现在,所有发送到 172.18.0.1:53 的数据包将被重定向到 127.0.0.11 :33501/41285,Docker DNS 位置。这还涉及到 SNAT 的更改。 从KIND源码可以观察到KIND的K8s节点每次启动都会修改iptables规则,同时也会更新默认地址在/etc/resolve.conf中。
# well-known docker embedded DNS is at 127.0.0.11:53
local docker_embedded_dns_ip='127.0.0.11'
# first we need to detect an IP to use for reaching the docker host
local docker_host_ip
docker_host_ip="$( (head -n1 <(timeout 5 getent ahostsv4 'host.docker.internal') | cut -d' ' -f1) || true)"
# if the ip doesn't exist or is a loopback address use the default gateway
if [[ -z "${docker_host_ip}" ]] || [[ $docker_host_ip =~ ^127\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
docker_host_ip=$(ip -4 route show default | cut -d' ' -f3)
fi
# patch docker's iptables rules to switch out the DNS IP
iptables-save \
| sed \
`# switch docker DNS DNAT rules to our chosen IP` \
-e "s/-d ${docker_embedded_dns_ip}/-d ${docker_host_ip}/g" \
`# we need to also apply these rules to non-local traffic (from pods)` \
-e 's/-A OUTPUT \(.*\) -j DOCKER_OUTPUT/\0\n-A PREROUTING \1 -j DOCKER_OUTPUT/' \
`# switch docker DNS SNAT rules rules to our chosen IP` \
-e "s/--to-source :53/--to-source ${docker_host_ip}:53/g"\
`# nftables incompatibility between 1.8.8 and 1.8.7 omit the --dport flag on DNAT rules` \
`# ensure --dport on DNS rules, due to https://github.com/kubernetes-sigs/kind/issues/3054` \
-e "s/p -j DNAT --to-destination ${docker_embedded_dns_ip}/p --dport 53 -j DNAT --to-destination ${docker_embedded_dns_ip}/g" \
| iptables-restore
# now we can ensure that DNS is configured to use our IP
cp /etc/resolv.conf /etc/resolv.conf.original
replaced="$(sed -e "s/${docker_embedded_dns_ip}/${docker_host_ip}/g" /etc/resolv.conf.original)"
if [[ "${KIND_DNS_SEARCH+x}" == "" ]]; then
# No DNS search set, just pass through as is
echo "$replaced" >/etc/resolv.conf
elif [[ -z "$KIND_DNS_SEARCH" ]]; then
# Empty search - remove all current search clauses
echo "$replaced" | grep -v "^search" >/etc/resolv.conf
else
# Search set - remove all current search clauses, and add the configured search
{
echo "search $KIND_DNS_SEARCH";
echo "$replaced" | grep -v "^search";
} >/etc/resolv.conf
fi
通过这些调整,CoreDNS 向节点发送 DNS 请求,然后通过 iptables 将请求转发到 Docker DNS。如果 Docker DNS 无法处理它们,请求将向上转发到主机上的初始设置,从而创建级联过程。
文章翻译 https://hwchiu.medium.com/fun-dns-facts-learned-from-the-kind-environment-241e0ea8c6d4