本文已超過一年。較舊的文章可能包含過時的內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
KubeVirt:使用 CRD 擴展 Kubernetes 以實現虛擬化工作負載
什麼是 KubeVirt?
KubeVirt 是一個 Kubernetes 外掛程式,讓使用者能夠將傳統虛擬機器工作負載與容器工作負載並排排程。透過使用自訂資源定義 (CRD) 和其他 Kubernetes 功能,KubeVirt 無縫擴展現有的 Kubernetes 叢集,以提供一組虛擬化 API,可用於管理虛擬機器。
為何使用 CRD 而非聚合 API 伺服器?
回到 2017 年年中,我們這些在 KubeVirt 上工作的人正處於十字路口。我們必須決定是否使用聚合 API 伺服器擴展 Kubernetes,還是使用新的自訂資源定義 (CRD) 功能。
當時,CRD 缺乏我們交付功能集所需的大部分功能。建立我們自己的聚合 API 伺服器的能力給了我們所需的所有彈性,但它有一個主要的缺陷。聚合 API 伺服器顯著增加了安裝和操作 KubeVirt 的複雜性。
對我們來說,問題的癥結在於聚合 API 伺服器需要存取 etcd 以進行物件持久化。這意味著叢集管理員必須接受 KubeVirt 需要單獨的 etcd 部署(這增加了複雜性),或者向 KubeVirt 提供對 Kubernetes etcd 儲存的共用存取權限(這引入了風險)。
我們不認同這種權衡。我們的目標不僅僅是擴展 Kubernetes 以運行虛擬化工作負載,而是以最無縫和輕鬆的方式做到這一點。我們認為,聚合 API 伺服器增加的複雜性犧牲了與安裝和操作 KubeVirt 相關的使用者體驗。
最終,我們選擇使用 CRD,並相信 Kubernetes 生態系統會與我們一同成長,以滿足我們的使用案例的需求。我們的押注是正確的。在這一點上,已經有解決方案到位,或者正在討論解決方案,以解決我們在 2017 年評估 CRD 與聚合 API 伺服器時遇到的每個功能差距。
使用 CRD 建構分層「類 Kubernetes」API
我們將 KubeVirt 的 API 設計為遵循使用者在 Kubernetes 核心 API 中已經熟悉的相同模式。
例如,在 Kubernetes 中,使用者建立以執行工作的最低層級單位是 Pod。是的,Pod 確實有多個容器,但在邏輯上,Pod 是堆疊底部的單位。Pod 代表一個會死亡的工作負載。Pod 被排程,最終 Pod 的工作負載終止,這就是 Pod 生命週期的結束。
工作負載控制器(例如 ReplicaSet 和 StatefulSet)分層在 Pod 抽象之上,以協助管理橫向擴展和具狀態應用程式。從那裡,我們有一個更高等級的控制器稱為 Deployment,它分層在 ReplicaSet 之上,以協助管理滾動更新等事項。
在 KubeVirt 中,這種分層控制器的概念是我們設計的核心。KubeVirt VirtualMachineInstance (VMI) 物件是 KubeVirt 堆疊最底層的最低層級單位。與 Pod 的概念類似,VMI 代表一個單一的會死亡的虛擬化工作負載,它執行一次直到完成(關機)。
在 VMI 之上分層的是一個稱為 VirtualMachine (VM) 的工作負載控制器。VM 控制器是我們真正開始看到使用者管理虛擬化工作負載與容器化工作負載之間差異的地方。在現有 Kubernetes 功能的上下文中,描述 VM 控制器行為的最佳方式是將其與大小為一的 StatefulSet 進行比較。這是因為 VM 控制器代表一個單一的具狀態(永生)虛擬機器,能夠在節點故障和底層 VMI 的多次重新啟動之間持久化狀態。此物件的行為方式與在 AWS、GCE、OpenStack 或任何其他類似 IaaS 雲端平台中管理虛擬機器的使用者熟悉的方式相同。使用者可以關閉 VM,然後選擇稍後重新啟動完全相同的 VM。
除了 VM 之外,我們還有一個 VirtualMachineInstanceReplicaSet (VMIRS) 工作負載控制器,它管理相同 VMI 物件的橫向擴展。此控制器的行為幾乎與 Kubernetes ReplicSet 控制器相同。主要區別在於 VMIRS 管理 VMI 物件,而 ReplicaSet 管理 Pod。如果我們可以想出一個方法來使用 Kubernetes ReplicaSet 控制器來擴展 CRD,那不是很好嗎?
當 KubeVirt 安裝資訊清單發佈到叢集時,每個 KubeVirt 物件(VMI、VM、VMIRS)都會在 Kubernetes 中註冊為 CRD。透過將我們的 API 註冊為 Kubernetes 的 CRD,所有與管理 Kubernetes 叢集相關的工具(如 kubectl)都可以存取 KubeVirt API,就像它們是原生 Kubernetes 物件一樣。
用於 API 驗證的動態 Webhook
Kubernetes API 伺服器的職責之一是在允許將物件持久化到 etcd 之前攔截和驗證請求。例如,如果有人嘗試使用格式錯誤的 Pod 規格建立 Pod,Kubernetes API 伺服器會立即捕獲錯誤並拒絕 POST 請求。這一切都發生在物件持久化到 etcd 之前,防止格式錯誤的 Pod 規格進入叢集。
此驗證發生在稱為准入控制的過程中。直到最近,如果不更改程式碼並編譯/部署全新的 Kubernetes API 伺服器,就不可能擴展預設的 Kubernetes 准入控制器。這意味著,如果我們想在 KubeVirt 的 CRD 物件發佈到叢集時對其執行准入控制,我們就必須建構我們自己的 Kubernetes API 伺服器版本,並說服我們的使用者改用它。這對我們來說不是一個可行的解決方案。
使用新的動態准入控制功能(該功能首次在 Kubernetes 1.9 中推出),我們現在有一條透過使用ValidatingAdmissionWebhook對 KubeVirt API 執行自訂驗證的路徑。此功能允許 KubeVirt 在 KubeVirt 安裝時向 Kubernetes 動態註冊 HTTPS webhook。在註冊自訂 webhook 後,所有與 KubeVirt API 物件相關的請求都會從 Kubernetes API 伺服器轉發到我們的 HTTPS 端點進行驗證。如果我們的端點因任何原因拒絕請求,則物件將不會持久化到 etcd,並且用戶端會收到我們的回應,其中概述了拒絕的原因。
例如,如果有人發佈格式錯誤的 VirtualMachine 物件,他們將收到一個錯誤,指出問題所在。
$ kubectl create -f my-vm.yaml
Error from server: error when creating "my-vm.yaml": admission webhook "virtualmachine-validator.kubevirt.io" denied the request: spec.template.spec.domain.devices.disks[0].volumeName 'registryvolume' not found.
在上面的範例輸出中,該錯誤回應直接來自 KubeVirt 的准入控制 webhook。
CRD OpenAPIv3 驗證
除了驗證 webhook 之外,KubeVirt 還使用在叢集中註冊 CRD 時提供OpenAPIv3 驗證架構的能力。雖然 OpenAPIv3 架構不允許我們表達驗證 webhook 提供的一些更進階的驗證檢查,但它確實提供了強制執行簡單驗證檢查的能力,這些檢查涉及必填欄位、最大/最小值長度,以及驗證值是否以符合正則表達式字串的方式格式化。
用於「PodPreset 類」行為的動態 Webhook
Kubernetes 動態准入控制功能不僅限於驗證邏輯,它還為像 KubeVirt 這樣的應用程式提供了攔截和變更請求的能力,因為它們進入叢集。這是透過使用 MutatingAdmissionWebhook 物件實現的。在 KubeVirt 中,我們正在尋找使用變更 webhook 來支援我們的 VirtualMachinePreset (VMPreset) 功能。
VMPreset 的作用方式與 PodPreset 類似。就像 PodPreset 允許使用者定義在建立時應自動注入到 Pod 中的值一樣,VMPreset 允許使用者定義在建立時應注入到 VM 中的值。透過使用變更 webhook,KubeVirt 可以攔截建立 VM 的請求,將 VMPreset 應用於 VM 規格,然後驗證結果 VM 物件。這一切都發生在 VM 物件持久化到 etcd 之前,這允許 KubeVirt 在提出請求時立即通知使用者任何衝突。
CRD 的子資源
在比較使用 CRD 與聚合 API 伺服器時,CRD 缺乏的功能之一是支援子資源的能力。子資源用於提供額外的資源功能。例如,pod/logs
和 pod/exec
子資源端點在幕後用於提供 kubectl logs
和 kubectl exec
命令功能。
就像 Kubernetes 使用 pod/exec
子資源來提供對 Pod 環境的存取權限一樣,在 KubeVirt 中,我們希望子資源提供對虛擬機器的序列主控台、VNC 和 SPICE 存取權限。透過透過子資源新增虛擬機器訪客存取權限,我們可以利用 RBAC 來提供對這些功能的存取控制。
因此,鑑於 KubeVirt 團隊決定使用 CRD 而不是聚合 API 伺服器來支援自訂資源,當 CRD 功能明確不支援子資源時,我們如何才能擁有 CRD 的子資源?
我們為此限制建立了一個解決方案,方法是實作一個無狀態聚合 API 伺服器,該伺服器僅存在以服務子資源請求。由於沒有狀態,我們不必擔心我們之前識別出的任何關於存取 etcd 的問題。這意味著 KubeVirt API 實際上是透過 CRD 資源和用於無狀態子資源的聚合 API 伺服器的組合來支援的。
這對我們來說並不是一個完美的解決方案。聚合 API 伺服器和 CRD 都要求我們向 Kubernetes 註冊 API GroupName。此 API GroupName 欄位本質上對 API 的 REST 路徑進行命名空間化,以防止其他第三方應用程式之間的 API 命名衝突。由於 CRD 和聚合 API 伺服器不能共享相同的 GroupName,我們必須註冊兩個單獨的 GroupName。一個由我們的 CRD 使用,另一個由聚合 API 伺服器用於子資源請求。
在我們的 API 中有兩個 GroupName 有點不方便,因為這意味著用於服務 KubeVirt 子資源請求的端點的 REST 路徑與資源的基準路徑略有不同。
例如,建立 VMI 物件的端點如下所示。
/apis/kubevirt.io/v1alpha2/namespaces/my-namespace/virtualmachineinstances/my-vm
但是,用於存取圖形 VNC 的子資源端點看起來像這樣。
/apis/subresources.kubevirt.io/v1alpha2/namespaces/my-namespace/virtualmachineinstances/my-vm/vnc
請注意,第一個請求使用 kubevirt.io,第二個請求使用 subresources.kubevirt.io。我們不喜歡這樣,但這就是我們如何將 CRD 與用於子資源的無狀態聚合 API 伺服器結合在一起的方式。
值得注意的是,在 Kubernetes 1.10 中,以 /status
和 /scale
子資源的形式新增了非常基本的 CRD 子資源支援。此支援無助於我們交付我們想要的用於子資源的虛擬化功能。但是,已經有關於在未來的 Kubernetes 版本中將自訂 CRD 子資源公開為 webhook 的討論。如果此功能落地,我們將很樂意從我們的無狀態聚合 API 伺服器解決方案過渡到使用子資源 webhook 功能。
CRD Finalizers
CRD finalizer 是一項功能,讓我們可以提供預先刪除掛鉤,以便在允許從持久儲存中移除 CRD 物件之前執行動作。在 KubeVirt 中,我們使用 finalizer 來保證虛擬機器已完全終止,然後才允許從 etcd 中移除相應的 VMI 物件。
CRD 的 API 版本控制
Kubernetes 核心 API 能夠支援單一物件類型的多個版本,並在這些版本之間執行轉換。這為 Kubernetes 核心 API 提供了一條將物件的 v1alpha1
版本提升到 v1beta1
版本等的路徑。
在 Kubernetes 1.11 之前,CRD 不支援多個版本。這意味著當我們想要將 CRD 從 kubevirt.io/v1alpha1
進展到 kubevirt.io/v1beta1
時,唯一可用的路徑是備份我們的 CRD 物件,從 Kubernetes 中刪除註冊的 CRD,註冊具有更新版本的新 CRD,將備份的 CRD 物件轉換為新版本,最後將遷移的 CRD 物件發佈回叢集。
該策略對我們來說並不是一個完全可行的選項。
幸運的是,由於最近在 Kubernetes 中糾正此問題的工作,最新的 Kubernetes v1.11 現在支援具有多個版本的 CRD。但請注意,此初始多版本支援是有限的。雖然 CRD 現在可以有多個版本,但該功能目前不包含在版本之間執行轉換的路徑。在 KubeVirt 中,缺乏轉換使我們難以在版本進展時發展我們的 API。幸運的是,對版本之間轉換的支援正在進行中,我們期待在該功能在未來的 Kubernetes 版本中落地後利用該功能。