本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。
Kubernetes-in-Kubernetes 和 WEDOS PXE 可引导服务器农场
当您拥有两个数据中心、数千台物理服务器、虚拟机以及托管数十万个网站时,Kubernetes 实际上可以简化所有这些事情的管理。实践证明,通过使用 Kubernetes,您不仅可以声明式地描述和管理应用程序,还可以管理基础设施本身。我为捷克最大的托管服务提供商 WEDOS Internet a.s 工作,今天我将向您展示我的两个项目——Kubernetes-in-Kubernetes 和 Kubefarm。
借助它们,您只需几条命令即可使用 Helm 在另一个 Kubernetes 集群中部署一个完全正常运行的 Kubernetes 集群。如何做到以及为什么?
让我向您介绍我们的基础设施是如何工作的。我们所有的物理服务器可以分为两组:控制平面和计算节点。控制平面节点通常是手动设置的,安装了稳定的操作系统,旨在运行包括 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,你会发现它遵循了 Twelve-Factor App 的所有原则,而且实际上写得非常好。因此,这意味着在另一个 Kubernetes 中将 Kubernetes 作为应用程序运行应该不是什么大问题。
在 Kubernetes 中运行 Kubernetes
现在让我们来看看 Kubernetes-in-Kubernetes 项目,它提供了一个现成的 Helm Chart,用于在 Kubernetes 中运行 Kubernetes。
这是您可以在 values 文件中传递给 Helm 的参数

除了 persistence(集群的存储参数),这里描述了 Kubernetes 控制平面组件:即 etcd cluster、apiserver、controller-manager 和 scheduler。这些都是相当标准的 Kubernetes 组件。有一个轻松的说法是“Kubernetes 只是五个二进制文件”。所以这里就是这些二进制文件的配置所在。
如果您曾经尝试使用 kubeadm 引导集群,那么这个配置会提醒您它的配置。但除了 Kubernetes 实体之外,您还有一个 admin 容器。事实上,它是一个内部包含两个二进制文件的容器:kubectl 和 kubeadm。它们用于为上述组件生成 kubeconfig,并执行集群的初始配置。此外,在紧急情况下,您始终可以执行进入它来检查和管理您的集群。
发布 部署 后,您可以看到 Pod 列表:admin-container、两个副本的 apiserver、controller-manager、etcd-cluster、scheduler 和初始化集群的初始作业。最后,您会有一个命令,允许您进入 admin 容器的 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 模块,那么这个逻辑会提醒您它。在这里,您静态地描述了所有带有参数集的节点
名称(主机名);
MAC 地址——我们有带有两块网卡的节点,每块网卡都可以从这里指定的任何 MAC 地址启动。
IP 地址,DHCP 服务器应该分配给此节点的 IP 地址。
在这个例子中,你有两个池:第一个有五个节点,第二个只有一个,第二个池还分配了两个标签。标签是描述特定节点配置的方式。例如,你可以为某些池添加特定的 DHCP 选项,为 PXE 服务器添加启动选项(例如,这里启用了调试选项),以及一组 kubernetesLabels 和 kubernetesTaints 选项。这意味着什么?
例如,在此配置中,您有第二个节点池,其中包含一个节点。该池分配了 debug 和 foo 标签。现在查看 kubernetesLabels 中 foo 标签的选项。这意味着 m1c43 节点将使用这些两个标签和污点启动。一切看起来都很简单。现在让我们在实践中尝试一下。
演示
转到 examples 并将先前部署的 chart 更新到 Kubefarm。只需使用 generic 参数并查看 Pod。您可以看到添加了一个 PXE 服务器和一个额外的作业。此作业本质上是进入已部署的 Kubernetes 集群并创建一个新令牌。现在它将每 12 小时重复运行以生成一个新令牌,以便节点可以连接到您的集群。
在 图形表示 中,它看起来大致相同,但现在 apiserver 开始对外暴露。
在图中,IP 以绿色突出显示,PXE 服务器可以通过它访问。目前,Kubernetes 默认不允许为 TCP 和 UDP 协议创建单个 LoadBalancer 服务,因此您必须创建两个具有相同 IP 地址的不同服务。一个用于 TFTP,第二个用于 HTTP,通过它下载系统镜像。
但这个简单的例子并不总是足够的,有时您可能需要修改启动逻辑。例如,这里有一个目录 advanced_network,其中包含一个带有一个简单 shell 脚本的 值文件。我们称之为 network.sh
这个脚本所做的只是在启动时获取环境变量,并根据它们生成网络配置。它会创建一个目录并将 netplan 配置放在里面。例如,这里创建了一个绑定接口。基本上,这个脚本可以包含您需要的一切。它可以包含网络配置或生成系统服务,添加一些钩子或描述任何其他逻辑。任何可以用 bash 或 shell 语言描述的东西都可以在这里工作,并且会在启动时执行。
让我们看看它是如何部署的。我们将通用值文件作为第一个参数,并将一个额外的值文件作为第二个参数传递。这是一个标准的 Helm 功能。通过这种方式,您也可以传递秘密,但在本例中,配置只是由第二个文件扩展。
让我们看看 netboot 服务器的 ConfigMap foo-kubernetes-ltsp,并确保 network.sh
脚本确实存在。这些命令用于在启动时配置网络。
在这里,您可以看到它实际上是如何工作的。机箱接口(我们使用 HPE Moonshots 1500)拥有节点,您可以输入 show node list
命令来获取所有节点的列表。现在您可以看到启动过程。
您还可以通过 show node macaddr all
命令获取它们的 MAC 地址。我们有一个聪明的操作员,它会自动从机箱收集 MAC 地址并将其传递给 DHCP 服务器。实际上,它只是为在同一个管理 Kubernetes 集群中运行的 dnsmasq-controller 创建自定义配置资源。此外,通过此接口,您可以控制节点本身,例如打开和关闭它们。
如果您无法通过 iLO 进入机箱并收集节点的 MAC 地址列表,可以考虑使用全捕获集群模式。严格来说,它只是一个具有动态 DHCP 池的集群。因此,所有未在其他集群配置中描述的节点都将自动加入此集群。
例如,您可以看到一个带有某些节点的特殊集群。它们以基于 MAC 地址自动生成的名称加入集群。从这一点开始,您可以连接到它们并查看那里发生的事情。在这里,您可以以某种方式准备它们,例如设置文件系统,然后将它们重新加入另一个集群。
现在让我们尝试连接到节点终端,看看它是如何启动的。BIOS 之后,网卡被配置,它从一个特定的 MAC 地址向 DHCP 服务器发送请求,DHCP 服务器将其重定向到特定的 PXE 服务器。随后,内核和 initrd 镜像使用标准的 HTTP 协议从服务器下载。
加载内核后,节点下载 rootfs 镜像并将控制权移交给 systemd。然后启动过程照常进行,之后节点加入 Kubernetes。
如果你查看 fstab,你会看到只有两个条目:/var/lib/docker 和 /var/lib/kubelet,它们被挂载为 tmpfs(实际上是从 RAM)。同时,根分区被挂载为 overlayfs,所以你在系统上做的所有更改都将在下次重启时丢失。
查看节点上的块设备,您可以看到一些 nvme 磁盘,但它尚未挂载到任何地方。还有一个循环设备——这是从服务器下载的确切 rootfs 镜像。目前它位于 RAM 中,占用 653 MB,并使用 loop 选项挂载。
如果你查看 /etc/ltsp,你会找到在启动时执行的 network.sh
文件。从容器中,你可以看到正在运行的 kube-proxy
和它的 pause
容器。
详情
网络启动镜像
但主镜像从何而来?这里有一个小技巧。节点的镜像通过 Dockerfile 与服务器一起构建。Docker 多阶段构建 功能允许您在镜像构建阶段轻松添加任何软件包和内核模块。它看起来像这样
这里发生了什么?首先,我们使用普通的 Ubuntu 20.04 并安装所有我们需要的软件包。首先,我们安装 kernel、lvm、systemd、ssh。总的来说,你希望在最终节点上看到的一切都应该在这里描述。我们还在这里安装了 docker
、kubelet
和 kubeadm
,它们用于将节点加入集群。
然后我们执行额外的配置。在最后阶段,我们只需安装 tftp
和 nginx
(它为客户端提供我们的镜像)、grub(引导加载程序)。然后将前一阶段的根目录复制到最终镜像中,并从中生成压缩镜像。也就是说,实际上,我们得到了一个 docker 镜像,它同时包含服务器和我们节点的引导镜像。同时,可以通过更改 Dockerfile 轻松更新。
Webhook 和 API 聚合层
我想特别关注 Webhook 和聚合层的问题。一般来说,Webhook 是 Kubernetes 的一个功能,它允许您响应任何资源的创建或修改。因此,您可以添加一个处理程序,以便当资源被应用时,Kubernetes 必须向某个 Pod 发送请求并检查此资源的配置是否正确,或者对其进行额外的更改。
但关键是,为了让 Webhook 正常工作,API 服务器必须能够直接访问它所运行的集群。如果它像我们的情况一样,在单独的集群中启动,甚至与任何集群分开启动,那么 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 集群。但目前我对此想法并非完全确定。如果您对此事有任何想法,我将乐于倾听。