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

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,它分层在 ReplicaSets 之上,以帮助管理诸如滚动更新之类的事情。

在 KubeVirt 中,这种分层控制器的概念是我们设计的核心。KubeVirt VirtualMachineInstance (VMI) 对象是 KubeVirt 堆栈最底层的最低级别单元。与 Pod 的概念类似,VMI 代表一个在完成(关闭电源)之前执行一次的单一可终止的虚拟化工作负载。

在我们有一个名为 VirtualMachine (VM) 的工作负载控制器的 VMI 之上分层。VM 控制器是我们真正开始看到用户如何管理虚拟化工作负载与容器化工作负载之间差异的地方。在现有 Kubernetes 功能的上下文中,描述 VM 控制器行为的最佳方式是将其与大小为 1 的 StatefulSet 进行比较。这是因为 VM 控制器代表一个能够跨节点故障和多次重启其底层 VMI 持久化状态的单一有状态(不朽)虚拟机。该对象的行为方式与在 AWS、GCE、OpenStack 或任何其他类似 IaaS 云平台中管理过虚拟机的用户所熟悉的方式相同。用户可以关闭虚拟机,然后选择稍后再次启动完全相同的虚拟机。

除了 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 中,我们正在寻找使用 mutating webhook 来支持我们的 VirtualMachinePreset (VMPreset) 功能。

VMPreset 的工作方式与 PodPreset 类似。正如 PodPreset 允许用户定义应在创建时自动注入到 pod 中的值一样,VMPreset 允许用户定义应在创建时注入到 VM 中的值。通过使用 mutating 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 功能明确不支持子资源时,我们如何才能拥有 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 终结器

CRD 终结器是一种功能,允许我们在允许从持久存储中删除 CRD 对象之前提供一个预删除钩子来执行操作。在 KubeVirt 中,我们使用终结器来保证虚拟机在允许从 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 版本中落地后利用该功能。