KubeVirt:通过CRD扩展Kubernetes实现虚拟机管理

830 阅读11分钟
原文链接: mp.weixin.qq.com

KubeVirt是什么?

KubeVirt[1]是个Kubernetes的一个插件,使其在原本调度容器之余能够并行调度传统虚拟机。它通过运用自定义资源定义(以下简称CRD)及其他Kubernetes相关功能来无缝扩展现有的集群,提供一系列可用于管理虚拟机的虚拟化API。

 

为什么要使用CRD而不是Aggregated API Server?

时光回转到2017年中期,那时工作在KubeVirt的团队面临选择的十字路口。他们必须决定使用何种方式来扩展Kubernetes——究竟该用Aggregated API Server,还是新出的CRD功能。 那时CRD缺乏相当多KubeVirt所需的相关功能,而通过创建Aggregated API Server能够提供这些所需的灵活性。不过它有一个主要缺陷——大大增加了安装及运维KubeVirt的复杂度。  此处问题的关键在于Aggregated API Server需要访问etcd来进行对象的持久化。这意味着集群管理员将不得不为KubeVirt单独部署一个etcd,这样会增加复杂度;或让KubeVirt直接访问Kubernetes本身的etcd,而这样则会引入风险。 当时团队无法接受Aggregated API Server这些问题。他们的目标并不仅仅只是完成扩展Kubernetes来管理虚拟机的任务,而是用尽量最无缝集成和最小管理代价的方式来完成这个任务。他们认为使用Aggregated API Server增加了复杂度,牺牲了安装和运维KubeVirt的用户体验。  最终KubeVirt团队选择使用CRD来实现功能,并相信Kubernetes生态圈会持续成长改进相关功能以满足KubeVirt的使用场景。他们最终得偿所愿。此时此刻,他们当初2017年做评估时CRD所存在的功能差距都已经有了解决方案,或至少解决方案正在讨论之中。

 

使用CRD来构建分层的类Kubernetes的API

KubeVirt的API是按照用户业已熟悉的Kubernetes核心API的模式进行设计的。 举例来说,在Kubernetes里用户创建的最底层的运行单元是Pod。当然Pod的确有可能包含多个容器,但逻辑上Pod是整个Kubernetes栈里最底层的单元。一个Pod代表了一份有始有终的执行任务。Pod会被调度到集群中,当工作完成后Pod也会被停止,同时意味着Pod生命周期的结束。  ReplicaSet和StatefulSet这样的工作负载控制器是构建在Pod之上的一层,分别用于管理Pod的伸缩和有状态的应用。在ReplicaSet之上还会有更高层的Deployment控制器用于实现诸如滚动更新这样的功能。 KubeVirt的API也是以如上多层次控制器的概念为中心进行设计的。KubeVirt里的虚拟机实例(VirtualMachineInstance, 以下简称VMI)对象是KubeVirt栈里的最底层单元。一个VMI代表了一份有始有终的虚拟化的执行任务,被创建出来被执行任务直到结束(此处意味着关机),与Pod的概念相同。  在VMI这层之上是VirtualMachine(以下简称VM)控制器。从VM控制器开始我们可以真正看到管理虚拟化的工作负载与容器化的工作负载之间的区别。在现有的Kubernetes功能体系之内,最能描述VM控制器行为的方法是把它和单一尺寸的StatefulSet进行比较。这是因为VM控制器能构建一个单独的有状态的虚拟机,这样的虚拟机能够在节点出错或VMI多次重启时依旧保持状态。VM对象的表现方式与我们在AWS,GCE,OpenStack或是其他类似的IaaS云平台上管理的虚拟机很类似。用户可以关闭虚拟机,然后又在随后的某个时间再重新启动这一台虚拟机。 除了VM控制器外,KubeVirt中还有VirtualMachineInstanceReplicaSet(以下简称VMIRS)控制器,用来管理VMI对象的伸缩。这个控制器与Kubernetes的ReplicaSet控制器表现行为几乎一模一样,它们两个的区别仅在于VMIRS管理VMI对象,而ReplicaSet管理Pod对象。不过这样说来,是不是如果能有方法使用Kubernetes的ReplicaSet控制器来伸缩管理VMI会更好一些? VMI,VM和VMIRS这些对象定义都是在KubeVirt的安装清单文件提交到集群中时被作为CRD定义注册进Kubernetes里的。而CRD这样的注册方式能让所有Kubernetes集群管理相关的工具(比如kubectl)能像访问其他原生Kubernetes对象的方式来访问KubeVirt的API。

使用动态的网络回调(Webhook)做CRD校验

Kubernetes API Server的职责之一是在对象被持久化进etcd之前拦截和验证进入的请求。举例来说,如果有人想尝试用有问题的Pod定义来创建一个新Pod,Kubernetes API Server会马上发现错误并拒绝这个创建请求。这样的动作都是发生在对象被持久化进etcd之前,从而防止了有问题的Pod定义进入到集群之中。 在Kubernetes里具体执行校验的过程被称为准入控制(admission control)。在之前,想要扩展默认的Kubernetes准入控制器都还需要修改Kubernetes API Server的代码并做重新编译和部署。这意味着如果想要对传入集群的KubeVirt相关的对象做准入控制和校验,KubeVirt团队就一定要编译一个独有的API Server的二进制版本并说服用户来使用。这对于KubeVirt团队来说并不是一个可行方案。 Kubernetes 1.9版本新引入了动态准入控制功能。通过使用这个功能提供的ValidatingAdmissionWebhook,KubeVirt团队终于有办法来对相关对象做自定义校验。KubeVirt会在安装到集群中时动态地注册一个HTTPS的网络回调。而在注册完后,所有与KubeVirt的API对象相关的请求都会被Kubernetes API Server转发到KubeVirt自己的HTTPS接口上做校验处理。如果这个HTTPS接口出于任何原因拒绝了这个请求,那么相关的对象也无法被持久化进etcd中,而发送请求方也会收到包含拒绝原因的返回。 举个例子,当有人发送了一个有问题的VM对象,他会收到如下的返回:

$ 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注册到准入控制的网络回调发出的。 使用OpenAPIv3做CRD校验

除了用网络回调来做校验外,KubeVirt也提供了OpenAPIv3校验模式(OpenAPIv3 validation schema[2])。OpenAPIv3模式并不能帮我们表达很多准入控制的网络回调里提供的那些高级校验规则。 但它提供了一些简单轻便的校验能力,比如必填字段检查,最大/最小值范围控制,正则表达式验证内容等。 使用动态网络回调达成类似PodPreset的效果

Kubernetes的动态准入控制功能不仅仅能做校验,还为KubeVirt这样的应用提供了拦截和修改进入集群的请求的能力。这个功能具体是通过操作Kubernetes的MutatingAdmissionWebhook对象来完成。KubeVirt也在寻求通过使用这样的网络回调来支持VirtualMachinePreset(以下简称VMPreset)功能。 VMPreset的表现行为与PodPreset相似。就像PodPreset允许用户定义一些在Pod创建期需要自动注入的预定义值,VMPreset允许用户定义在VM创建期需要注入的参数。通过使用网络回调,KubeVirt可以拦截创建VM的请求,把VMPreset配置应用到VM定义上,并且完成了VM的校验。这些活动都在VM对象被持久化进etcd之前发生,这样一旦有任何冲突或问题,执行这个操作的用户都会第一时间得到KubeVirt提供的反馈。

 

CRD的子资源(Subresources)

再拿CRD与Aggregated API Server进行比较,你还会发现CRD缺少了子资源的能力。子资源提供了额外的资源相关的功能。举例来说,kubectl logs和kubectl exec命令背后就是去读取pod/logs和pod/exec这两个子资源接口所提供的信息。 就像Kubernetes通过pod/exec子资源来提供访问Pod环境那样,KubeVirt也想使用子资源的方式来提供串口终端,VNC或者SPICE的访问虚拟机环境的能力。而且结合子资源来为虚拟机添加用户访问控制的话,则可以利用Kubernetes的RBAC能力来对此做访问控制。  所以既然当初KubeVirt团队选择了使用CRD而不是Aggregated API Server来实现自定义对象,那么他们能怎么样在CRD不支持子资源的情况又用上子资源呢? KubeVirt团队搞了个变通方案——他们实现了一个仅仅是服务于子资源的无状态Aggregated API Server。既然没有状态,那么就无需担心之前识别出的etcd访问相关的问题。这意味着KubeVirt的API实际上是CRD提供的资源和Aggregated API Server提供的无状态子资源的组合。  这对于KubeVirt来说并不是一个完美的方案。CRD和Aggregated API Server都需要在Kubernetes里注册API组名称(GroupName)。而API组名称字段基本上就是REST API路径的命名空间,因此这个机制会防止多个第三方应用在API命名上有互相冲突。由于CRD与Aggregated API Server不能共享同一个组名称,所以KubeVirt不得不注册了两个不同的组名称。一个由CRD使用,而另一个由Aggregated API Server使用。  在KubeVirt API里包含两个组名称会有一点点地不那么方便,因为这意味着子资源相关的API路径与资源相关的API路径会有差异。  如下是创建一个VMI对象的API调用:

/apis/kubevirt.io/v1alpha2/namespaces/my-namespace/virtualmachineinstances/my-vm
而通过子资源方式来访问图形化VNC的API调用是这样的:
/apis/subresources.kubevirt.io/v1alpha2/namespaces/my-namespace/virtualmachineinstances/my-vm/vnc
此处注意第一个请求使用了kubevirt.io,而第二个请求使用了subresource.kubevirt.io。KubeVirt团队也坦言他们并不喜欢这样的做法,但这是他们解决融合CRD实现和Aggregated API Server实现的方式。  值得一提的是,Kubernetes从1.10版本开始支持了两个最基础的CRD的子资源接口:/status和/scale。 这两个新接口的支持与KubeVirt交付的虚拟化功能所需要的子资源功能并没什么帮助,但至少社区里已经有关于在未来的Kubernetes里将自定义CRD子资源当做webhook暴露出来的讨论。如果这个功能能够实现,那么KubeVirt团队将会非常乐意把那个无状态的Aggregated API Server方案扔掉并迁移到这个功能上。  CRD清理器

CRD清理器让一个CRD对象从etcd中被移除前能够通过预删除钩子(pre-delete hook)来执行一些自定义操作。KubeVirt使用清理器来确保VMI对象被从etcd中移除前,对应的虚拟机已经完全被终止了。 CRD的API版本控制

Kubernetes的核心API有能力对一个对象类型支持多个版本,并在这些不同版本间做互相转换。这样一来它们就有方法把一个对象从v1alpha1版本升级到v1beta1版本乃至之后其的其他版本。  在Kubernetes的1.11版之前,CRD并不支持多版本。这意味着当KubeVirt想要把一个CRD从kubevirt.io/v1alpha1版本升级到kubevirt.io/v1beta1版本,唯一的方法是先备份CRD对象,再从集群中删除已注册的CRD,接着注册新版本的CRD进集群,然后把备份的CRD转化为新的版本,最后把转化完的版本再添加进集群中。 这样的策略实在是太不可行了。 幸好现在最新的Kubernetes 1.11版里支持了CRD多版本功能。这要感谢近期社区里的相关工作解决了这个问题。 但还是要注意这个初始的版本在能力上还很有限。尽管当前CRD可以有多版本, 但这个功能并没有提供版本间转化迁移的能力。而对于KubeVirt来说,缺少转化迁移功能会使API版本进化非常困难。但幸运的是,对版本间迁移的支持目前已经在开发之中,而KubeVirt也期待这个功能在未来Kubernetes发布时能第一时间利用上它。 相关链接:
  1. https://github.com/kubevirt/kubevirt

  2. https://kubernetes.io/docs/tasks/access-kubernetes-API/extend-api-custom-resource-definitions/#advanced-topics

原文链接:https://kubernetes.io/blog/2018/07/27/kubevirt-extending-kubernetes-with-crds-for-virtualized-workloads/