本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。
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 版本中利用该功能。