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

Kubernetes 內部部署 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,您會注意到它遵循 The Twelve-Factor App 的所有原則,並且實際上編寫得非常好。因此,這意味著在另一個 Kubernetes 中將 Kubernetes 作為應用程式運行應該不是什麼大問題。

在 Kubernetes 中運行 Kubernetes

現在讓我們看一下 Kubernetes-in-Kubernetes 專案,它為在 Kubernetes 中運行 Kubernetes 提供了現成的 Helm chart。

以下是您可以在 values 檔案中傳遞給 Helm 的參數

Kubernetes is just five binaries

除了 persistence(叢集的儲存參數)之外,這裡還描述了 Kubernetes 控制平面組件:即:etcd 叢集apiservercontroller-managerscheduler。這些幾乎是標準的 Kubernetes 組件。有一個輕鬆的說法是「Kubernetes 只是五個二進位檔案」。因此,這裡就是這些二進位檔案的組態所在。

如果您曾經嘗試使用 kubeadm 引導叢集,那麼此組態會讓您想起它的組態。但除了 Kubernetes 實體之外,您還有一個管理員容器。事實上,它是一個容器,內部包含兩個二進位檔案:kubectlkubeadm。它們用於為上述組件產生 kubeconfig,並執行叢集的初始組態。此外,在緊急情況下,您可以隨時 exec 進入其中以檢查和管理您的叢集。

在發布 部署完成 後,您可以看到 Pod 的清單:admin-container、兩個副本中的 apiservercontroller-manageretcd-clusterscheduller 和初始化叢集的初始 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 組件的服務: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 伺服器應將其頒發給此節點。

在此範例中,您有兩個池:第一個池有五個節點,第二個池只有一個,第二個池也分配了兩個標籤。標籤是用於描述特定節點組態的方式。例如,您可以為某些池新增特定的 DHCP 選項、用於 PXE 伺服器啟動的選項(例如,此處啟用了偵錯選項)以及一組 kubernetesLabelskubernetesTaints 選項。這意味著什麼?

例如,在此組態中,您有一個節點的第二個 nodePool。該池分配了 debugfoo 標籤。現在查看 kubernetesLabelsfoo 標籤的選項。這表示 m1c43 節點將使用分配的這兩個標籤和污點啟動。一切似乎都很簡單。現在讓我們在實務中 嘗試 一下。

示範

前往 examples 並將先前部署的 chart 更新為 Kubefarm。只需使用 generic 參數並查看 Pod。您可以看到新增了一個 PXE 伺服器和一個更多 Job。此 Job 基本上會前往已部署的 Kubernetes 叢集並建立新的令牌。現在它將每 12 小時重複運行以產生新令牌,以便節點可以連線到您的叢集。

圖形表示 中,它看起來大致相同,但現在 apiserver 開始在外部公開。

在圖表中,IP 以綠色突出顯示,PXE 伺服器可以透過它訪問。目前,Kubernetes 預設不允許為 TCP 和 UDP 協定建立單一 LoadBalancer 服務,因此您必須建立兩個具有相同 IP 位址的不同服務。一個用於 TFTP,第二個用於 HTTP,系統映像透過 HTTP 下載。

但這個簡單的範例並不總是足夠,有時您可能需要在啟動時修改邏輯。例如,這裡有一個目錄 advanced_network,其中有一個帶有簡單 shell 腳本的 values 檔案。讓我們稱之為 network.sh

所有此腳本所做的事情都是在啟動時取得環境變數,並根據它們產生網路組態。它建立一個目錄並將 netplan 組態放入其中。例如,此處建立了一個綁定介面。基本上,此腳本可以包含您需要的一切。它可以包含網路組態或產生系統服務、新增一些 hook 或描述任何其他邏輯。任何可以在 bash 或 shell 語言中描述的內容都可以在這裡運作,並且它將在啟動時執行。

讓我們看看如何 部署 它。讓我們將 generic values 檔案作為第一個參數傳遞,並將另一個 values 檔案作為第二個參數傳遞。這是標準的 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 位址清單,您可以考慮使用 catchall 叢集 模式。純粹來說,它只是一個具有動態 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-proxypause 容器。

詳細資訊

網路啟動映像

但是主映像從哪裡來呢?這裡有一個小技巧。節點的映像是透過 Dockerfile 與伺服器一起建構的。Docker 多階段建構 功能讓您可以輕鬆地在映像建構階段新增任何套件和核心模組。它看起來像這樣

這裡發生了什麼事?首先,我們採用常規的 Ubuntu 20.04 並安裝我們需要的所有套件。首先,我們安裝 kernellvmsystemdssh。一般來說,您希望在最終節點上看到的所有內容都應在此處描述。在這裡,我們還安裝了 docker 以及 kubeletkubeadm,它們用於將節點加入到叢集中。

然後我們執行額外的組態。在最後階段,我們只需安裝 tftpnginx(為我們的用戶端提供映像)、grub (啟動程式)。然後將先前階段的根目錄複製到最終映像中,並從中產生壓縮映像。也就是說,實際上,我們獲得了一個 docker 映像,其中同時包含伺服器和節點的啟動映像。同時,可以透過變更 Dockerfile 輕鬆更新它。

Webhook 和 API 聚合層

我想特別關注 webhook 和聚合層的問題。一般來說,webhook 是一種 Kubernetes 功能,可讓您回應任何資源的建立或修改。因此,您可以新增一個處理程序,以便在應用資源時,Kubernetes 必須向某個 Pod 發送請求,並檢查此資源的組態是否正確,或對其進行其他變更。

但重點是,為了讓 webhook 運作,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 能夠存取叢集中的所有 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 叢集。但目前我還不太確定這個想法。如果您對此事有任何想法,我很樂意傾聽。