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

KubeVirt:使用 CRD 扩展 Kubernetes 以支持虚拟化工作负载

KubeVirt 是什么?

KubeVirt 是一个 Kubernetes 插件,它使用户能够将传统的虚拟机工作负载与容器工作负载一起调度。通过使用Custom Resource Definitions (CRDs) 和其他 Kubernetes 功能,KubeVirt 无缝扩展了现有的 Kubernetes 集群,提供了一套可用于管理虚拟机的虚拟化 API。

为什么选择 CRD 而不是 Aggregated API Server?

回到 2017 年年中,我们在 KubeVirt 上工作的团队正处于十字路口。我们必须决定是使用 Aggregated API Server 来扩展 Kubernetes,还是利用新的 Custom Resource Definitions (CRDs) 功能。

当时,CRDs 缺乏我们交付功能集所需的许多功能。创建我们自己的 Aggregated API Server 提供了我们所需的所有灵活性,但它有一个主要缺陷。Aggregated API Server 显著增加了 KubeVirt 安装和操作的复杂性。

对我们来说,问题的核心在于 Aggregated API Server 需要访问 etcd 来进行对象持久化。这意味着集群管理员必须要么接受 KubeVirt 需要独立的 etcd 部署(这增加了复杂性),要么允许 KubeVirt 共享访问 Kubernetes 的 etcd 存储(这引入了风险)。

我们不接受这种权衡。我们的目标不仅仅是扩展 Kubernetes 以运行虚拟化工作负载,而是以最无缝、最轻松的方式做到。我们认为 Aggregated API Server 带来的额外复杂性牺牲了 KubeVirt 安装和操作用户体验的一部分。

最终,我们选择使用 CRD,并相信 Kubernetes 生态系统会与我们一起发展,以满足我们用例的需求。我们的选择是正确的。目前,已经有现有的解决方案或正在讨论的解决方案解决了我们在 2017 年评估 CRD 与 Aggregated API Server 时遇到的所有功能差距。

使用 CRD 构建分层的“Kubernetes 式”API

我们设计 KubeVirt 的 API 以遵循用户在 Kubernetes 核心 API 中已熟悉的模式。

例如,在 Kubernetes 中,用户创建来执行工作的最低级别单元是 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)都作为 CRD 注册到 Kubernetes。通过将我们的 API 作为 CRD 注册到 Kubernetes,所有用于管理 Kubernetes 集群的工具(如 kubectl)都可以访问 KubeVirt API,就像它们是原生的 Kubernetes 对象一样。

API 验证的动态 Webhook

Kubernetes API Server 的职责之一是在允许对象持久化到 etcd 之前拦截和验证请求。例如,如果有人尝试使用格式错误的 Pod 规范创建 Pod,Kubernetes API Server 会立即捕获错误并拒绝 POST 请求。所有这些都在对象持久化到 etcd 之前发生,从而阻止格式错误的 Pod 规范进入集群。

这种验证发生在称为准入控制(admission control)的过程中。直到最近,在不修改代码和编译/部署全新的 Kubernetes API Server 的情况下,无法扩展默认的 Kubernetes 准入控制器。这意味着如果我们想在 KubeVirt 的 CRD 对象被提交到集群时对其进行准入控制,我们就必须构建我们自己的 Kubernetes API Server 版本,并说服用户使用它。这对我们来说不是一个可行的解决方案。

利用首次出现在 Kubernetes 1.9 中的新的动态准入控制功能,我们现在可以通过使用 ValidatingAdmissionWebhook 对 KubeVirt API 执行自定义验证。此功能允许 KubeVirt 在安装时动态向 Kubernetes 注册一个 HTTPS webhook。注册自定义 webhook 后,所有与 KubeVirt API 对象相关的请求都将从 Kubernetes API Server 转发到我们的 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/logspod/exec 子资源端点在幕后用于提供 kubectl logskubectl exec 命令功能。

就像 Kubernetes 使用 pod/exec 子资源来提供对 Pod 环境的访问一样,在 KubeVirt 中,我们希望子资源能够提供对虚拟机的串行控制台、VNC 和 SPICE 访问。通过子资源添加虚拟机访客访问,我们可以利用 RBAC 为这些功能提供访问控制。

那么,既然 KubeVirt 团队决定使用 CRD 而不是聚合 API 服务器来支持自定义资源,那么在 CRD 功能明确(expiclity)不支持子资源的情况下,我们如何才能拥有 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 子资源作为 webhooks 暴露出来,已经进行了一些讨论。如果此功能落地,我们将乐意放弃我们的无状态聚合 API 服务器 workaround,转而使用子资源 webhook 功能。

CRD Finalizers

一个 CRD finalizer 是一个功能,它允许我们提供一个预删除 hook,以便在允许从持久存储中移除 CRD 对象之前执行操作。在 KubeVirt 中,我们使用 finalizers 来确保虚拟机在相应的 VMI 对象被从 etcd 中移除之前已完全终止。

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