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

Kubernetes 中的 Kubernetes 和 WEDOS PXE 可引导服务器场

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

借助它们,您可以在另一个 Kubernetes 中使用 Helm 在几个命令中部署一个功能齐全的 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,您会注意到它遵循了 十二要素应用程序的所有原则,并且实际上编写得非常好。因此,这意味着在另一个 Kubernetes 中将 Kubernetes 作为应用程序运行应该不是什么大问题。

在 Kubernetes 中运行 Kubernetes

现在让我们看看 Kubernetes-in-Kubernetes 项目,它为在 Kubernetes 中运行 Kubernetes 提供了现成的 Helm 图表。

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

Kubernetes is just five binaries

除了 持久性(集群的存储参数)之外,这里还描述了 Kubernetes 控制平面组件:即:etcd 集群apiservercontroller-managerscheduler。这些是非常标准的 Kubernetes 组件。有一句轻松的说法,“Kubernetes 只是五个二进制文件”。所以这里是这些二进制文件的配置所在。

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

发布部署后,您可以看到 Pod 的列表:admin-containerapiserver(两个副本)、controller-manageretcd-clusterscheduller 以及初始化集群的初始作业。最后,您将获得一个命令,该命令允许您进入管理容器的 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 项目,该项目允许您自动执行此操作。最常用的示例可以在 示例 目录中找到。其中最标准的一个名为 generic。让我们看一下 values.yaml

在这里,您可以指定传递到上游 Kubernetes-in-Kubernetes 图表中的参数。为了使您的控制平面可以从外部访问,只需在此处指定 IP 地址就足够了,但是如果您愿意,您可以在此处指定一些 DNS 名称。

在 PXE 服务器配置中,您可以指定时区。您还可以添加用于无密码登录的 SSH 密钥(但您也可以指定密码),以及在启动系统期间应应用的内核模块和参数。

接下来是 nodePools 配置,即节点本身。如果您曾经使用过 gke 的 terraform 模块,那么这个逻辑会让你想起它。在这里,您使用一组参数静态描述所有节点

  • 名称 (主机名);

  • MAC 地址 - 我们的节点有两张网卡,并且每个都可以从此处指定的任何 MAC 地址启动。

  • IP 地址,DHCP 服务器应将该地址颁发给该节点。

在此示例中,您有两个池:第一个有五个节点,第二个只有一个,第二个池还分配了两个标签。标签是描述特定节点配置的方式。例如,您可以为某些池添加特定的 DHCP 选项、用于启动的 PXE 服务器的选项(例如,此处启用了调试选项)以及一组 kubernetesLabelskubernetesTaints 选项。这是什么意思?

例如,在此配置中,您有一个只有一个节点的第二个 nodePool。该池分配了 debugfoo 标签。现在查看 kubernetesLabelsfoo 标签的选项。这意味着 m1c43 节点将使用分配的这两个标签和污点启动。一切似乎都很简单。现在 让我们在实践中尝试一下

演示

转到 示例 并将先前部署的图表更新为 Kubefarm。只需使用 generic 参数并查看 Pod。您可以看到添加了 PXE 服务器和一个额外的作业。此作业本质上是转到已部署的 Kubernetes 集群并创建一个新令牌。现在它将每 12 小时重复运行以生成一个新令牌,以便节点可以连接到您的集群。

图形表示 中,它看起来大致相同,但现在 apiserver 开始在外部公开。

在图中,IP 用绿色突出显示,可以通过它访问 PXE 服务器。目前,Kubernetes 默认不允许为 TCP 和 UDP 协议创建单个 LoadBalancer 服务,因此您必须创建两个具有相同 IP 地址的不同服务。一个用于 TFTP,第二个用于 HTTP,系统镜像通过它下载。

但是这个简单的例子并不总是足够,有时您可能需要在启动时修改逻辑。例如,这是一个目录 advanced_network,其中有一个带有简单 shell 脚本的 values 文件。让我们称其为 network.sh

这个脚本的作用就是在启动时获取环境变量,并基于这些变量生成网络配置。它会创建一个目录,并将 netplan 配置放在其中。例如,这里会创建一个 bonding 接口。基本上,这个脚本可以包含你所需的一切。它可以保存网络配置,生成系统服务,添加一些钩子,或者描述任何其他逻辑。任何可以用 bash 或 shell 语言描述的东西都可以在这里使用,并且会在启动时执行。

让我们看看如何部署它。我们传递通用值文件作为第一个参数,并传递一个附加的值文件作为第二个参数。这是一个标准的 Helm 功能。这样你也可以传递密钥,但在这种情况下,配置只是通过第二个文件扩展的。

让我们看看用于网络启动服务器的 configmap foo-kubernetes-ltsp,并确保 network.sh 脚本确实存在。这些命令用于在启动时配置网络。

这里你可以看到它的工作原理。机箱接口(我们使用 HPE Moonshots 1500)有节点,你可以输入 show node list 命令来获取所有节点的列表。现在你可以看到启动过程。

你还可以通过 show node macaddr all 命令获取它们的 MAC 地址。我们有一个聪明的操作员,可以自动从机箱收集 MAC 地址,并将它们传递给 DHCP 服务器。实际上,它只是为在同一个管理 Kubernetes 集群中运行的 dnsmasq-controller 创建自定义配置资源。此外,通过这个接口,你可以控制节点本身,例如打开和关闭它们。

如果你没有机会通过 iLO 进入机箱并为你的节点收集 MAC 地址列表,你可以考虑使用 catchall 集群 模式。严格来说,它只是一个具有动态 DHCP 池的集群。因此,所有未在其他集群配置中描述的节点将自动加入到此集群。

例如,你可以看到一个包含一些节点的特殊集群。它们以基于其 MAC 地址自动生成的名称加入集群。从这一点开始,你可以连接到它们并查看那里发生了什么。在这里,你可以以某种方式准备它们,例如,设置文件系统,然后将它们重新加入到另一个集群。

现在让我们尝试连接到节点终端,看看它是如何启动的。在 BIOS 之后,网络卡被配置,这里它从一个特定的 MAC 地址向 DHCP 服务器发送请求,这将它重定向到一个特定的 PXE 服务器。稍后,内核和 initrd 镜像将使用标准 HTTP 协议从服务器下载。

加载内核后,节点会下载 rootfs 镜像,并将控制权转移到 systemd。然后启动过程照常进行,之后节点加入 Kubernetes。

如果你查看 fstab,你会看到那里只有两个条目:/var/lib/docker/var/lib/kubelet,它们被挂载为 tmpfs(实际上是从 RAM)。同时,根分区被挂载为 overlayfs,因此你在此系统上所做的所有更改将在下次重启时丢失。

查看节点上的块设备,你可以看到一些 nvme 磁盘,但它尚未挂载到任何地方。还有一个 loop 设备 - 这正是从服务器下载的 rootfs 镜像。目前它位于 RAM 中,占用 653 MB,并使用 loop 选项挂载。

如果你查看 /etc/ltsp,你会找到在启动时执行的 network.sh 文件。从容器中,你可以看到正在运行的 kube-proxy 和它的 pause 容器。

详情

网络启动镜像

但是主镜像来自哪里呢?这里有一个小技巧。节点的镜像通过 Dockerfile 与服务器一起构建。 Docker 多阶段构建功能允许你在镜像构建阶段轻松添加任何软件包和内核模块。它看起来像这样

这里发生了什么?首先,我们使用一个常规的 Ubuntu 20.04 并安装我们需要的所有软件包。首先,我们安装 kernellvmsystemdssh。一般来说,你希望在最终节点上看到的所有内容都应该在这里描述。这里我们还安装了 docker 以及用于将节点加入集群的 kubeletkubeadm

然后我们执行额外的配置。在最后阶段,我们只需安装 tftpnginx(用于为客户端提供镜像)、grub(引导加载程序)。然后将前几个阶段的 root 复制到最终镜像中,并从中生成压缩镜像。也就是说,实际上,我们得到一个 docker 镜像,它同时具有服务器和我们节点的引导镜像。同时,可以通过更改 Dockerfile 轻松更新它。

Webhooks 和 API 聚合层

我想特别关注 webhooks 和聚合层的问题。一般来说,webhooks 是 Kubernetes 的一个功能,允许你响应任何资源的创建或修改。因此,你可以添加一个处理程序,以便在应用资源时,Kubernetes 必须向某个 pod 发送请求,并检查此资源的配置是否正确,或者对其进行额外的更改。

但关键是,为了使 webhooks 工作,apiserver 必须可以直接访问它正在运行的集群。如果它像我们的情况一样在单独的集群中启动,甚至与任何集群分开启动,那么 Konnectivity 服务可以在这里帮助我们。Konnectivity 是 Kubernetes 可选但官方支持的组件之一。

例如,我们以一个由四个节点组成的集群为例,每个节点都运行一个 kubelet,并且我们在外部运行其他 Kubernetes 组件:kube-apiserverkube-schedulerkube-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 能够访问集群中的所有 webhooks 和所有 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 集群。但目前我对此想法还不太确定。如果你对此事有任何想法,我将很乐意听取。