本文发表已超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已不再正确。
Kubernetes 中的 Kubernetes 与 WEDOS PXE 可启动服务器集群
当你拥有两个数据中心、数千台物理服务器、虚拟机以及为数十万个站点提供托管服务时,Kubernetes 实际上可以简化对所有这些的管理。实践证明,通过使用 Kubernetes,你不仅可以声明式地描述和管理应用程序,还可以管理基础设施本身。我在捷克最大的托管服务提供商 WEDOS Internet a.s 工作,今天我将展示我的两个项目 — Kubernetes-in-Kubernetes 和 Kubefarm。
借助它们,你只需几条命令,就可以使用 Helm 在另一个 Kubernetes 集群内部署一个完全工作的 Kubernetes 集群。如何以及为什么?
让我介绍一下我们的基础设施是如何工作的。我们所有的物理服务器可以分为两组:控制平面 (control-plane) 和 计算节点 (compute nodes)。控制平面节点通常是手动设置的,安装了稳定的操作系统,并且设计用于运行包括 Kubernetes 控制平面在内的所有集群服务。这些节点的主要任务是确保集群本身的顺利运行。计算节点默认不安任何操作系统,而是通过网络直接从控制平面节点引导操作系统镜像。它们的工作是承载工作负载。
一旦节点下载了它们的镜像,它们就可以继续工作,而无需保持与 PXE 服务器的连接。也就是说,PXE 服务器只是存储 rootfs 镜像,不包含任何其他复杂逻辑。我们的节点引导启动后,我们可以安全地重启 PXE 服务器,对它们不会发生任何关键问题。
引导启动后,我们的节点做的第一件事是加入到现有的 Kubernetes 集群,具体来说就是执行 kubeadm join 命令,以便 kube-scheduler 可以在它们上面调度一些 Pod,然后启动各种工作负载。一开始我们采用的方案是节点加入到与控制平面节点相同的集群中。
这个方案稳定运行了两年多。然而后来我们决定在其上添加容器化的 Kubernetes。现在我们可以非常轻松地直接在我们的控制平面节点上启动新的 Kubernetes 集群,这些节点现在是特殊的管理集群的成员。现在,计算节点可以直接加入到它们自己的集群中,具体取决于配置。
Kubefarm
创建这个项目的目标是让任何人只需几条命令就可以使用 Helm 部署这样的基础设施,最终得到大致相同的结果。
此时,我们放弃了单集群的想法。因为事实证明,管理多个开发团队在同一个集群中的工作不太方便。事实上,Kubernetes 从未被设计为多租户解决方案,目前它不提供足够的项目间隔离手段。因此,为每个团队运行独立的集群是个好主意。但是,集群的数量不应过多,以便于管理。也不应过少,以保持开发团队之间足够的独立性。
在那次改变之后,我们集群的可伸缩性显著改善。单位节点数量下的集群越多,故障域越小,它们的运行也就越稳定。作为额外的好处,我们获得了完全声明式描述的基础设施。因此,现在你可以像在 Kubernetes 中部署任何其他应用一样部署新的 Kubernetes 集群。
它使用 Kubernetes-in-Kubernetes 作为基础,LTSP 作为用于节点引导的 PXE 服务器,并使用 dnsmasq-controller 自动化 DHCP 服务器配置。

工作原理
现在让我们看看它的工作原理。总的来说,如果你从应用的角度来看待 Kubernetes,你会注意到它遵循 十二要素应用 的所有原则,并且实际上写得非常好。因此,这意味着在另一个 Kubernetes 中将 Kubernetes 作为应用运行不应是什么大问题。
在 Kubernetes 中运行 Kubernetes
现在我们来看看 Kubernetes-in-Kubernetes 项目,它提供了一个现成的 Helm chart,用于在 Kubernetes 中运行 Kubernetes。
这里是你可以在 values 文件中传递给 Helm 的参数

除了持久性 (persistence)(集群的存储参数)之外,这里还描述了 Kubernetes 控制平面组件:具体来说:etcd 集群、apiserver、controller-manager 和 scheduler。这些是标准的 Kubernetes 组件。有一个轻松的说法是“Kubernetes 就是五个二进制文件”。所以这里就是这些二进制文件的配置所在。
如果你曾经尝试使用 kubeadm 引导集群,那么这个配置会让你想起它的配置。但除了 Kubernetes 实体之外,你还有一个管理容器。事实上,它是一个内部包含两个二进制文件的容器:kubectl 和 kubeadm。它们用于为上述组件生成 kubeconfig 并执行集群的初始配置。另外,在紧急情况下,你随时可以 exec 进入它来检查和管理你的集群。
发布部署完成后,你可以看到 Pod 列表:admin-container、两个副本的 apiserver、controller-manager、etcd-cluster、scheduler 以及初始化集群的初始 Job。最后你有一条命令,允许你进入管理容器的 shell,你可以用它来查看内部发生了什么。
另外,我们来看看证书。如果你曾经安装过 Kubernetes,那么你知道它有一个令人畏惧的目录 /etc/kubernetes/pki
,里面有一堆证书。对于 Kubernetes-in-Kubernetes,你使用 cert-manager 实现了证书的完全自动化管理。因此,只需在安装时将所有证书参数传递给 Helm,所有证书就会为你的集群自动生成。
查看其中一个证书,例如 apiserver,你可以看到它有一个 DNS 名称和 IP 地址列表。如果你想让这个集群可从外部访问,只需在 values 文件中描述额外的 DNS 名称并更新发布即可。这将更新证书资源,cert-manager 将重新生成证书。你将不再需要为此操心。如果 kubeadm 证书需要每年至少续订一次,这里 cert-manager 会负责自动续订它们。
现在让我们登录到管理容器并查看集群和节点。当然,目前还没有节点,因为此时你只部署了空的 Kubernetes 控制平面。但在 kube-system 命名空间中,你可以看到一些等待调度的 coredns Pod 和 ConfigMap 已经出现。也就是说,你可以得出结论,集群正在工作。
这是已部署集群的图示。你可以看到所有 Kubernetes 组件的服务:apiserver、controller-manager、etcd-cluster 和 scheduler。以及它们向其转发流量右侧的 Pod。
顺便提一下,图示是在 ArgoCD 中绘制的,这是我们用于管理集群的 GitOps 工具,酷炫的图示是其功能之一。
编排物理服务器
好的,现在你看到了我们如何部署 Kubernetes 控制平面,那工作节点呢,我们如何添加它们?正如我之前所说,我们所有的服务器都是裸金属的。我们不使用虚拟化来运行 Kubernetes,而是由我们自己编排所有物理服务器。
此外,我们非常积极地使用 Linux 网络引导功能。更重要的是,这确实是引导,而不是某种安装自动化。当节点引导时,它们只是运行一个现成的系统镜像。也就是说,要更新任何节点,我们只需重启它,它就会下载新镜像。这非常轻松、简单和方便。
为此,创建了 Kubefarm 项目,它使你能够自动化这一过程。最常用的示例可以在 examples 目录中找到。其中最标准的名为 generic。我们来看看 values.yaml
在这里你可以指定传递到上游 Kubernetes-in-Kubernetes chart 的参数。为了让你的控制平面可从外部访问,只需在这里指定 IP 地址即可,但如果你愿意,也可以指定一些 DNS 名称。
在 PXE 服务器配置中,你可以指定时区。你还可以添加 SSH 密钥进行无密码登录(但你也可以指定密码),以及系统引导期间应应用的内核模块和参数。
接下来是 nodePools 配置,即节点本身。如果你曾经使用过用于 gke 的 terraform 模块,那么这种逻辑会让你想起它。这里你使用一组参数静态描述所有节点。
名称 (Name) (hostname);
MAC 地址 (MAC-addresses) — 我们有带有两块网卡的节点,每个节点都可以从这里指定的任何 MAC 地址引导。
IP 地址 (IP-address),这是 DHCP 服务器应分配给该节点的地址。
在这个示例中,你有两个池:第一个有五个节点,第二个只有一个节点,第二个池还分配了两个标签。标签是描述特定节点配置的方式。例如,你可以为某些池添加特定的 DHCP 选项,用于引导的 PXE 服务器选项(例如这里启用了 debug 选项),以及一组 kubernetesLabels 和 kubernetesTaints 选项。这是什么意思?
例如,在这个配置中,你有一个包含一个节点的第二个 nodePool。该池分配了 debug 和 foo 标签。现在查看 kubernetesLabels 中 foo 标签的选项。这意味着 m1c43 节点将带着这两个标签和 taint 启动。一切看起来很简单。现在我们在实践中试试。
演示
转到 examples 并将之前部署的 chart 更新为 Kubefarm。只需使用 generic 参数并查看 Pod。你可以看到添加了 PXE 服务器和另一个 Job。这个 Job 实际上会连接到已部署的 Kubernetes 集群并创建一个新的 token。现在它将每 12 小时重复运行以生成新的 token,以便节点可以连接到你的集群。
在图形表示中,看起来大致相同,但现在 apiserver 开始暴露到外部。
在图示中,IP 以绿色高亮显示,可以通过它访问 PXE 服务器。目前,Kubernetes 默认情况下不允许为 TCP 和 UDP 协议创建单个 LoadBalancer Service,因此你必须创建两个具有相同 IP 地址的不同 Service。一个用于 TFTP,另一个用于 HTTP,通过它们下载系统镜像。
但这个简单的示例并非总是足够,有时你可能需要在引导时修改逻辑。例如,这里有一个目录 advanced_network,内部有一个 values 文件,其中包含一个简单的 shell 脚本。我们称之为 network.sh
。
这个脚本所做的一切就是在引导时获取环境变量,并基于它们生成网络配置。它会创建一个目录,并将 netplan 配置放入其中。例如,这里创建了一个 bonding 接口。基本上,这个脚本可以包含你所需的一切。它可以包含网络配置或生成系统服务,添加一些钩子或描述任何其他逻辑。任何可以用 bash 或 shell 语言描述的内容都可以在这里工作,并且它会在引导时执行。
让我们看看它如何部署。我们将 generic values 文件作为第一个参数传递,并将另一个 values 文件作为第二个参数。这是标准的 Helm 功能。通过这种方式,你也可以传递 secrets,但在这种情况下,配置仅仅由第二个文件进行扩展。
我们来看看用于网络引导服务器的 ConfigMap foo-kubernetes-ltsp,并确保 network.sh
脚本确实在那里。这些命令用于在引导时配置网络。
这里你可以看到它的工作原理。机箱接口(我们使用 HPE Moonshots 1500)具有节点,你可以输入 show node list
命令来获取所有节点列表。现在你可以看到引导过程。
你也可以通过 show node macaddr all
命令获取它们的 MAC 地址。我们有一个聪明的 operator,它自动从机箱收集 MAC 地址,并将它们传递给 DHCP 服务器。实际上,它只是为在同一个管理 Kubernetes 集群中运行的 dnsmasq-controller 创建自定义配置资源。此外,通过这个接口,你可以控制节点本身,例如开机和关机。
如果你没有通过 iLO 进入机箱并收集节点 MAC 地址列表的机会,你可以考虑使用catchall 集群模式。严格来说,它只是一个带有动态 DHCP 池的集群。因此,所有未在其他集群配置中描述的节点都将自动加入到此集群。
例如,你可以看到一个包含一些节点的特殊集群。它们以基于其 MAC 地址自动生成的名称加入到集群。从这一点开始,你可以连接到它们并查看那里发生了什么。在这里你可以以某种方式准备它们,例如设置文件系统,然后将它们重新加入到另一个集群。
现在我们来尝试连接到节点终端,看看它是如何引导的。在 BIOS 之后,网卡被配置,这里它从特定的 MAC 地址向 DHCP 服务器发送请求,DHCP 服务器将其重定向到特定的 PXE 服务器。随后,内核和 initrd 镜像使用标准的 HTTP 协议从服务器下载。
加载内核后,节点会下载 rootfs 镜像并将控制权转移给 systemd。然后引导过程照常进行,之后节点加入 Kubernetes 集群。
如果你查看 fstab,你会发现里面只有两个条目:/var/lib/docker 和 /var/lib/kubelet,它们被挂载为 tmpfs(实际上是来自内存)。同时,根分区被挂载为 overlayfs,因此你在这里对系统所做的所有更改将在下次重启时丢失。
查看节点上的块设备,你可以看到一些 nvme 磁盘,但它尚未被挂载到任何地方。还有一个 loop 设备——这正是从服务器下载的 rootfs 镜像。目前它位于内存中,占用 653MB,并使用 loop 选项挂载。
如果你查看 /etc/ltsp,你会找到在引导时执行的 network.sh
文件。从容器中,你可以看到正在运行的 kube-proxy
以及为其运行的 pause
容器。
详情
网络引导镜像
但是主镜像来自哪里呢?这里有一个小技巧。节点镜像通过 Dockerfile 与服务器一起构建。Docker 多阶段构建功能允许你在镜像构建阶段轻松添加任何软件包和内核模块。它看起来像这样:
这里发生了什么?首先,我们采用一个普通的 Ubuntu 20.04 并安装我们需要的所有软件包。首先,我们安装 kernel、lvm、systemd、ssh。一般来说,你希望在最终节点上看到的一切都应该在这里描述。我们还在这里安装了 docker
,以及用于将节点加入集群的 kubelet
和 kubeadm
。
然后我们进行额外的配置。在最后一个阶段,我们只需安装 tftp
和 nginx
(用于向客户端提供我们的镜像)、grub(引导加载程序)。然后将前一阶段的根文件系统复制到最终镜像中,并从其生成 squashed 镜像。也就是说,实际上我们得到一个 Docker 镜像,其中既包含服务器,也包含我们节点的引导镜像。同时,通过修改 Dockerfile,可以轻松更新它。
Webhooks 和 API 聚合层
我想特别关注 Webhook 和聚合层的问题。一般来说,Webhook 是 Kubernetes 的一个功能,允许你响应任何资源的创建或修改。因此,你可以添加一个处理程序,以便在应用资源时,Kubernetes 必须向某个 Pod 发送请求,检查该资源的配置是否正确,或对其进行额外修改。
但关键在于,为了使 Webhook 工作,apiserver 必须能够直接访问其正在运行的集群。如果它像我们的情况一样在单独的集群中启动,或者甚至与任何集群分开启动,那么 Konnectivity 服务可以在这里帮助我们。Konnectivity 是 Kubernetes 的可选但官方支持的组件之一。
以一个包含四个节点的集群为例,每个节点都运行着一个 kubelet
,而我们在外部运行着其他 Kubernetes 组件:kube-apiserver
、kube-scheduler
和 kube-controller-manager
。默认情况下,所有这些组件都直接与 apiserver 交互——这是 Kubernetes 逻辑中最广为人知的部分。但实际上,也存在反向连接。例如,当你想查看日志或运行 kubectl exec command
时,API 服务器会独立地与特定的 kubelet 建立连接。
但问题是,如果存在 Webhook,它通常作为一个标准的 Pod 在我们的集群中运行,并带有一个服务。当 apiserver 尝试访问它时,将会失败,因为它会尝试访问一个名为 webhook.namespace.svc 的集群内服务,而它本身位于该服务实际运行的集群外部。
此时,Konnectivity 来拯救我们了。Konnectivity 是一个专门为 Kubernetes 开发的精妙代理服务器。它可以作为服务器部署在 apiserver 旁边。而 Konnectivity-agent 则以多个副本直接部署在你想要访问的集群中。代理会连接到服务器并建立一个稳定的通道,使 apiserver 能够访问集群中的所有 Webhook 和所有 kubelet。因此,现在所有与集群的通信都将通过 Konnectivity-server 进行。
我们的计划
当然,我们不会止步于此。对项目感兴趣的人经常给我写信。如果有足够数量的感兴趣的人,我希望将 Kubernetes-in-Kubernetes 项目移至 Kubernetes SIGs 之下,以官方 Kubernetes Helm chart 的形式呈现。也许通过使这个项目独立,我们会聚集更大的社区。
我也在考虑将其与 Machine Controller Manager 集成,这将允许创建工作节点,不仅限于物理服务器,还可以例如使用 kubevirt 创建虚拟机并在同一个 Kubernetes 集群中运行它们。顺便说一下,它也允许在云中创建虚拟机,并在本地部署控制平面。
我还在考虑与 Cluster-API 集成的选项,以便你可以直接通过 Kubernetes 环境创建物理 Kubefarm 集群。但目前我对此想法不太确定。如果你对此事有任何想法,我很乐意倾听。