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

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 ReplicaSet 控制器幾乎相同。主要區別在於 VMIRS 管理 VMI 物件,而 ReplicaSet 管理 Pod。如果我們能想出一種方法來使用 Kubernetes ReplicaSet 控制器來擴充套件 CRD,那豈不是很好?

當 KubeVirt 安裝清單釋出到叢集時,每個 KubeVirt 物件(VMI、VM、VMIRS)都作為 CRD 註冊到 Kubernetes。透過將我們的 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 中首次推出的新動態准入控制功能,我們現在可以透過使用驗證准入 Webhook 來對 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 驗證 schema。雖然 OpenAPIv3 schema 無法讓我們表達驗證 webhook 提供的一些更高階的驗證檢查,但它確實提供了強制執行簡單驗證檢查的能力,例如必需欄位、最大/最小值的長度以及驗證值是否以符合正則表示式字串的方式格式化。

用於“PodPreset 類似”行為的動態 Webhook

Kubernetes 動態准入控制功能不僅限於驗證邏輯,它還為 KubeVirt 等應用程式提供了攔截和修改進入叢集的請求的能力。這是透過使用 **MutatingAdmissionWebhook** 物件實現的。在 KubeVirt 中,我們正在尋求使用一個變異 webhook 來支援我們的 VirtualMachinePreset (VMPreset) 功能。

VMPreset 的作用類似於 PodPreset。正如 PodPreset 允許使用者定義在建立 Pod 時自動注入到 Pod 中的值一樣,VMPreset 允許使用者定義在建立 VM 時注入到 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**,第二個請求使用 **subresource.kubevirt.io**。我們不喜歡這樣,但這是我們設法將 CRD 與無狀態聚合 API 伺服器結合用於子資源的方式。

值得注意的是,在 Kubernetes 1.10 中,以 `/status` 和 `/scale` 子資源的形式添加了一種非常基本的 CRD 子資源支援。這種支援並不能幫助我們實現我們希望子資源實現的虛擬化功能。然而,已經有人討論在未來的 Kubernetes 版本中將自定義 CRD 子資源公開為 Webhook。如果此功能落地,我們將樂於從無狀態聚合 API 伺服器的變通方案過渡到使用子資源 Webhook 功能。

CRD 終結器

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 版本中利用該功能。