本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

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 叢集。但目前我對此想法並非完全確定。如果您對此事有任何想法,我將樂於傾聽。