本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

Kubernetes-in-Kubernetes 和 WEDOS PXE 可引导服务器农场

当您拥有两个数据中心、数千台物理服务器、虚拟机以及托管数十万个网站时,Kubernetes 实际上可以简化所有这些事情的管理。实践证明,通过使用 Kubernetes,您不仅可以声明式地描述和管理应用程序,还可以管理基础设施本身。我为捷克最大的托管服务提供商 WEDOS Internet a.s 工作,今天我将向您展示我的两个项目——Kubernetes-in-KubernetesKubefarm

借助它们,您只需几条命令即可使用 Helm 在另一个 Kubernetes 集群中部署一个完全正常运行的 Kubernetes 集群。如何做到以及为什么?

让我向您介绍我们的基础设施是如何工作的。我们所有的物理服务器可以分为两组:控制平面计算节点。控制平面节点通常是手动设置的,安装了稳定的操作系统,旨在运行包括 Kubernetes 控制平面在内的所有集群服务。这些节点的主要任务是确保集群本身的平稳运行。计算节点默认没有安装任何操作系统,而是通过网络直接从控制平面节点启动操作系统镜像。它们的工作是承担工作负载。

Kubernetes cluster layout

一旦节点下载了镜像,它们就可以继续工作,而无需保持与 PXE 服务器的连接。也就是说,PXE 服务器只是保存 rootfs 镜像,不包含任何其他复杂的逻辑。我们的节点启动后,我们可以安全地重启 PXE 服务器,它们不会发生任何关键问题。

Kubernetes cluster after bootstrapping

启动后,我们的节点做的第一件事是加入现有的 Kubernetes 集群,即执行 kubeadm join 命令,以便 kube-scheduler 可以在它们上调度一些 Pod,然后启动各种工作负载。从一开始,我们使用的方案是节点加入用于控制平面节点的同一个集群。

Kubernetes scheduling containers to the compute nodes

这个方案稳定运行了两年多。然而,后来我们决定加入容器化的 Kubernetes。现在,我们可以非常轻松地直接在我们的控制平面节点上生成新的 Kubernetes 集群,这些节点现在是特殊管理集群的成员。现在,计算节点可以直接加入它们自己的集群——取决于配置。

Multiple clusters are running in single Kubernetes, compute nodes joined to them

Kubefarm

该项目的目标是让任何人只需几条命令,使用 Helm 就能部署这样的基础设施,并最终获得相同的结果。

此时,我们放弃了单集群的想法。因为事实证明,在同一个集群中管理多个开发团队的工作不是很方便。事实上,Kubernetes 从来没有被设计成一个多租户解决方案,目前它没有提供项目之间足够的隔离手段。因此,为每个团队运行独立的集群是一个好主意。然而,集群不应该太多,以便于管理。也不应该太少,以便开发团队之间有足够的独立性。

在那次改变之后,我们集群的可扩展性明显改善。每节点拥有的集群数量越多,故障域越小,它们的工作就越稳定。作为一个额外的好处,我们获得了完全声明式描述的基础设施。因此,现在您可以像在 Kubernetes 中部署任何其他应用程序一样部署一个新的 Kubernetes 集群。

它以 Kubernetes-in-Kubernetes 为基础,以 LTSP 作为节点启动的 PXE 服务器,并使用 dnsmasq-controller 自动化 DHCP 服务器配置。

Kubefarm

工作原理

现在让我们看看它是如何工作的。总的来说,如果你从应用程序的角度看 Kubernetes,你会发现它遵循了 Twelve-Factor App 的所有原则,而且实际上写得非常好。因此,这意味着在另一个 Kubernetes 中将 Kubernetes 作为应用程序运行应该不是什么大问题。

在 Kubernetes 中运行 Kubernetes

现在让我们来看看 Kubernetes-in-Kubernetes 项目,它提供了一个现成的 Helm Chart,用于在 Kubernetes 中运行 Kubernetes。

这是您可以在 values 文件中传递给 Helm 的参数

Kubernetes is just five binaries

除了 persistence(集群的存储参数),这里描述了 Kubernetes 控制平面组件:即 etcd clusterapiservercontroller-managerscheduler。这些都是相当标准的 Kubernetes 组件。有一个轻松的说法是“Kubernetes 只是五个二进制文件”。所以这里就是这些二进制文件的配置所在。

如果您曾经尝试使用 kubeadm 引导集群,那么这个配置会提醒您它的配置。但除了 Kubernetes 实体之外,您还有一个 admin 容器。事实上,它是一个内部包含两个二进制文件的容器:kubectlkubeadm。它们用于为上述组件生成 kubeconfig,并执行集群的初始配置。此外,在紧急情况下,您始终可以执行进入它来检查和管理您的集群。

发布 部署 后,您可以看到 Pod 列表:admin-container、两个副本的 apiservercontroller-manageretcd-clusterscheduler 和初始化集群的初始作业。最后,您会有一个命令,允许您进入 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 组件的服务:apiservercontroller-manageretcd-clusterscheduler。右侧是它们转发流量的 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 服务器添加启动选项(例如,这里启用了调试选项),以及一组 kubernetesLabelskubernetesTaints 选项。这意味着什么?

例如,在此配置中,您有第二个节点池,其中包含一个节点。该池分配了 debugfoo 标签。现在查看 kubernetesLabelsfoo 标签的选项。这意味着 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 并安装所有我们需要的软件包。首先,我们安装 kernellvmsystemdssh。总的来说,你希望在最终节点上看到的一切都应该在这里描述。我们还在这里安装了 dockerkubeletkubeadm,它们用于将节点加入集群。

然后我们执行额外的配置。在最后阶段,我们只需安装 tftpnginx(它为客户端提供我们的镜像)、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 的连接。

Kubernetes apiserver reaching kubelet

但问题是,如果我们有一个 Webhook,那么它通常作为我们集群中的标准 Pod 和服务运行。当 apiserver 试图访问它时,它会失败,因为它会尝试访问一个名为 webhook.namespace.svc 的集群内服务,而它本身位于它实际运行的集群之外。

Kubernetes apiserver can't reach webhook

而 Konnectivity 在这里就派上了用场。Konnectivity 是一个专为 Kubernetes 开发的巧妙代理服务器。它可以作为服务器部署在 apiserver 旁边。Konnectivity-agent 以多个副本直接部署在您要访问的集群中。代理与服务器建立连接,并设置一个稳定的通道,使 apiserver 能够访问集群中的所有 Webhook 和所有 kubelet。因此,现在与集群的所有通信都将通过 Konnectivity-server 进行。

Kubernetes apiserver reaching webhook via konnectivity

我们的计划

当然,我们不会止步于此。对该项目感兴趣的人经常给我写信。如果感兴趣的人足够多,我希望将 Kubernetes-in-Kubernetes 项目转移到 Kubernetes SIGs 下,以官方 Kubernetes Helm chart 的形式呈现。也许,通过使这个项目独立,我们将聚集一个更大的社区。

我还在考虑将其与 Machine Controller Manager 集成,这将允许创建工作节点,不仅是物理服务器,还可以,例如,使用 kubevirt 创建虚拟机并在同一个 Kubernetes 集群中运行它们。顺便说一下,它还允许在云中生成虚拟机,并在本地部署控制平面。

我还在考虑与 Cluster-API 集成的选项,以便您可以直接通过 Kubernetes 环境创建物理 Kubefarm 集群。但目前我对此想法并非完全确定。如果您对此事有任何想法,我将乐于倾听。