Operator 版本化
Operator 的版本升级通常在考虑的是 Operator 自定义的 CRD 对象和存储数据的升级。
当 Operator 版本升级可能会需要对 CRD 结构进行升级,同时随着 Operator 开发完成,CRD 也会调整版本号为更正式的版本,这时候就要考虑对升级前的数据进行兼容,而如果 CRD 结构有调整也要考虑进行数据的迁移。
但在此之前,我们可以看一下 Kubernetes 中对于 CRD 版本的一些定义。
Kubernetes 中 CRD 的版本化
Kubernetes 中 CustomResourceDefinition
版本在 spec.versions
中进行设置,支持同时配置多个版本,字段配置如下:
1 | apiVersion: apiextensions.k8s.io/v1 |
字段说明:
served
:是否启用,如果为 false,请求该接口会报 404;storage
:是否为存储版本,一个 CRD 只能有一个存储版本,其它的版本要转换成存储版本;deprecated
:是否为弃用版本;deprecationWarning
:弃用版本告警,当操作弃用版本时,API server 会返回对应告警。
当存在多个版本时,只能有一个版本设置为存储版本,其它的必需设置为 false
,同时需要配置转换接口,让 API server 知道如何进行接口转换:
1 | apiVersion: apiextensions.k8s.io/v1 |
理解存储版本(Stored Version)
存储版本既存储在 ETCD 中的版本,对于一个 CRD 来说,只有一个版本会存储到 ETCD 中。
而 Kubernetes API server 可以同时为多个版本提供服务,这主要是基于 conversion 接口实现的。当操作非存储版本时,API server 会调用 conversion 接口将对象进行转换再返回或存储。
kubectl 等 client 会通过 discovery API 获取资源的版本列表并决定请求哪个版本的接口。对于 CRD 来说,kubectl 会选择最新的稳定版进行请求。
如果要请求非默认版本,需要按以下方式请求:
1 | kubectl get resource.version.group |
转换 API 的请求响应结构
通过设置 conversionReviewVersions
版本列表,可以配置 Webhook 支持的版本转换。如果接到支持的请求,会向 API 发送 ConversionReview
对象:
1 | { |
转换成功的 API 需要响应如下类似数据:
1 | { |
响应的内容要注意:
uid
要和请求时一样;kind
、metadata.uid
、metadata.name
和metadata.namespace
必需和请求时一样;metadata.labels
和metadata.annotations
可以修改;metadata
其它字段的修改会被忽略;convertedObjects
响应的对象顺序要和请求时一样,且apiVersion
和请求的desiredAPIVersion
一样。
简化转换方法数量
如果只有 v1 和 v2 版本,那么只需要开发两个方向的转换方法。但是,如果有 4 个,甚至是 8 个版本时,那转换版本的方法就已经是非常难以维护的了。
当前 controller-runtime 进行 conversion 时,使用的是 Hub and Spoke 模型,得以简化版本转换的维护成本。
Hub and Spoke 可以将网状转换结构转换成星型结构:
将一个版本指定成 Hub 版本,当其它非 Hub 版本间转换时,需要先转换成 Hub 版本,再转换成其它版本。
这样可以减少我们需要定义的转换函数数量,并且 Kubernetes 内部的实现也是这样的。
Operator 版本迭代
Operator 是基于 controller-runtime 进行开发的,使用 kubebuilder 的工具可以快速生成上面的 Hub and Spoke 方法。
首先在之前的 Demo 工程中,创建新的接口版本:
1 | operator-sdk create api --version v1beta2 --kind DemoApplication |
我们计划选择 v1beta2 版本作为 Hub 版本,所以在生成的 types 中增加注释:
1 | // +kubebuilder:storageversion |
表示该结构是存储版本:
1 | // +kubebuilder:object:root=true |
执行 make
命令生成代码和 manifests 配置。
接着执行下面命令生成 webhook 相关代码:
1 | operator-sdk create webhook --version v1beta2 --kind DemoApplication --conversion |
--version
:在哪个版本下生成--conversion
:创建 conversion 代码
执行结果如下:
可以看到如下提示:
1 | You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types. |
我们修改 api/v1beta2/demoapplication_types.go
,增加一行代码:
1 | func (r *DemoApplication) Hub() {} |
修改 api/v1beta1/demoapplication_types.go
增加 Spoke 相关实现:
1 | func (a *DemoApplication) ConvertTo(dst conversion.Hub) error { |
使用以下命令生成部署 YAML 文件进行调试,看哪些配置需要调整:
1 | make build-installer |
修改 manifest 配置
启用 webhook
开启 webhook 功能需要调整以下配置:
- 启用
config/crd/kustomization.yaml
文件里的patches/webhook_in_<kind>.yaml
;注入 webhook 配置到 CRD 文件中,默认调用
operater-sdk create webhook
时这个会自动添加并启用 - 启用
config/default/kustomization.yaml
里的../webhook
和manager_webhook_patch.yaml
注入证书到 controller,不启用会报
serving-certs/tls.crt: no such file
而无法启动 - 注释
config/webhook/kustomization.yaml
里的- manifests.yaml
配置。manifests.yaml
配置在启用 Admission WebHook 的时候才会生成,不注释的话生成installer.yaml
会报错。
单单启用 webhook 还是不可用的。Operator 使用的自签名证书并不被 API server 所信任,需要使用一个叫 cert-manager 的组件给我们的应用颁发证书。
cert-manager 组件会注入 CA 到 API server 中,所以 API server 请求 conversion webhook 时就不再报证书错误。
启用 cert-manager
一般集群里都已经安装好了这个组件,如果没安装可以执行以下命令直接安装:
1 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml |
默认情况下,certmanager 的配置被禁用了,我们需要到 manifests 里手动打开:
- 启用
config/crd/kustomization.yaml
文件里的patches/cainjection_in_<kind>.yaml
; - 启用
config/default/kustomization.yaml
文件里的./certmanager
目录(创建证书和 CA); - 启用
config/default/kustomization.yaml
文件里CERTMANAGER
块下的所有变量(注入 CRD、)。
注意,如果报以下错,说明错误地启用了 admission webhooks 的 CA 注入:
1 | Error: no resource matches strategic merge patch "MutatingWebhookConfiguration.v1.admissionregistration.k8s.io/mutating-webhook-configuration.[noNs]": no matches for Id MutatingWebhookConfiguration.v1.admissionregistration.k8s.io/mutating-webhook-configuration.[noNs]; failed to find unique target for patch MutatingWebhookConfiguration.v1.admissionregistration.k8s.io/mutating-webhook-configuration.[noNs] |
因为在 webhook 生成时是没启用 admission webhooks 代码生成的,所以并没有对应的 webhook 配置用于 patch,就会报找不到错误。
虽然没有生成 admission webhooks 配置,但是注入的开关和模板代码是提前生成了。我们把下面配置注释掉既可:
1 | - path: webhookcainjection_patch.yaml |
部署测试
1 | export IMG=registry-c.cmft.com/cmhk-grd-paas-portal/demo-app:5 |
旧数据的迁移
当部署新版本的 CRD 后,在集群中一般会同时存在多个版本,而只能有一个版本是存储版本。
对于前面的示例来说,我们可以看到,存在 v1beta1
和 v1beta2
两个版本,而 v1beta2
是存储版本:
1 | apiVersion: apiextensions.k8s.io/v1 |
那部署新版本后,旧数据怎样了呢?
所以我们可以通过以下命令看到 CRD 同时存在两个存储版本在使用:
1 | kubectl get crd demoapplications.paas.cmft.com -ojson | jq .status.storedVersions |
为什么要迁移
前面提到,当前 controller-runtime 使用的 Hub and Spoke 模型来管理各版本 CRD 的转换。
当 CRD 的结构随版本变化而变化时,维护转换函数的成本会变得越来越大。旧版本应该逐渐弃用,然后移除。所以应该像 Kubernetes 一样,当弃用或移除接口时,自动将旧数据迁移到接的存储版本中。
迁移方法
目前 Kubernetes 建议两种迁移方案。
使用 Storage Version Migrator 工具
Storage Version Migrator 由两个控制器组成:
trigger controller
:每 10 分钟调用 discovery 接口获取一次,检测默认存储版本是否变化,如果有变化就给对应 Kind 创建 StorageVersionMigration;migration controller
:负责处理 StorageVersionMigration,当有新的 kind 需要迁移时,migration controller
会将对象全部读取出来再原样写回 API server,触发 API server 使用最新的存储版本进行保存。
Storage Version Migrator 在 Kubernetes 1.30 中是 alpha 状态,小于这个版本的需要手动安装。
本地构建:
1 | make all-images |
执行以下命令部署到集群:
1 | export REGISTRY=registry-c.cmft.com/cmhk-grd-paas-portal |
手动迁移
手动迁移的动作和 Storage Version Migrator 差不多,这里假定将 v1beta1
升级到 v1
:
- CRD 将新版本设置为存储版本,此时
status.storedVersions
为v1beta1
和v1
; - 写一个脚本,读取所有的并直接写回 API server,强制以新的存储版本重新保存;
- 迁移完成后,手动从
status.storedVersions
中删除v1beta1
。
总结
当开发新版本时,如果涉及字段变化需要开发 conversion webhook 接口提供给 API server 调用进行转换。如果字段无变化,API server 无需基于 conversion webhook 就可以自动完成转换。
controller-runtime 使用 Hub and Spoke 的模型实现接口转换,只需要实现 Spoke 和 Hub 间的转换方法既可,Spoke 间的转换会经过 Hub 版本进行转换。
启用 conversion webhook 接口需要同时启用 cert-manager 组件,否则 API server 调用时会报证书错误。
修改存储版本后,旧数据不会自动进行迁移,只有在数据更新的时候才会重新以新的存储版本保存。
统一迁移旧数据可以使用 Storage Version Migrator 工具或写个脚本实现。只需要将数据重新写回 API server 就可以完成迁移。