Operator 版本化

Operator 版本化

Operator 的版本升级通常在考虑的是 Operator 自定义的 CRD 对象和存储数据的升级。

当 Operator 版本升级可能会需要对 CRD 结构进行升级,同时随着 Operator 开发完成,CRD 也会调整版本号为更正式的版本,这时候就要考虑对升级前的数据进行兼容,而如果 CRD 结构有调整也要考虑进行数据的迁移。

但在此之前,我们可以看一下 Kubernetes 中对于 CRD 版本的一些定义。

Kubernetes 中 CRD 的版本化

Kubernetes 中 CustomResourceDefinition 版本在 spec.versions 中进行设置,支持同时配置多个版本,字段配置如下:

CRD 声明的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
name: crontabs.example.com
spec:
group: example.com
names:
plural: crontabs
singular: crontab
kind: CronTab
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: false
deprecated: true
deprecationWarning: "example.com/v1alpha1 CronTab is deprecated; see http://example.com/v1alpha1-v1 for instructions to migrate to example.com/v1 CronTab"

schema: ...

字段说明:

  • served:是否启用,如果为 false,请求该接口会报 404;
  • storage:是否为存储版本,一个 CRD 只能有一个存储版本,其它的版本要转换成存储版本;
  • deprecated:是否为弃用版本;
  • deprecationWarning:弃用版本告警,当操作弃用版本时,API server 会返回对应告警。

当存在多个版本时,只能有一个版本设置为存储版本,其它的必需设置为 false,同时需要配置转换接口,让 API server 知道如何进行接口转换:

CRD 版本转换配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
...
spec:
...
conversion:
strategy: Webhook
webhook:
conversionReviewVersions: ["v1", "v1beta1"]
clientConfig:
service:
namespace: my-service-namespace
name: my-service-name
path: /my-path
port: 1234
caBundle: "Ci0tLS0tQk...<base64-encoded PEM bundle>...tLS0K"

理解存储版本(Stored Version)

存储版本既存储在 ETCD 中的版本,对于一个 CRD 来说,只有一个版本会存储到 ETCD 中。

而 Kubernetes API server 可以同时为多个版本提供服务,这主要是基于 conversion 接口实现的。当操作非存储版本时,API server 会调用 conversion 接口将对象进行转换再返回或存储。

kubectl 等 client 会通过 discovery API 获取资源的版本列表并决定请求哪个版本的接口。对于 CRD 来说,kubectl 会选择最新的稳定版进行请求。

如果要请求非默认版本,需要按以下方式请求:

1
2
3
kubectl get resource.version.group
# 如
kubectl get cronjobs.v1.batch.tutorial.kubebuilder.io -o yaml

转换 API 的请求响应结构

通过设置 conversionReviewVersions 版本列表,可以配置 Webhook 支持的版本转换。如果接到支持的请求,会向 API 发送 ConversionReview 对象:

转换请求体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "ConversionReview",
"request": {
# Random uid uniquely identifying this conversion call
"uid": "705ab4f5-6393-11e8-b7cc-42010a800002",

# The API group and version the objects should be converted to
"desiredAPIVersion": "example.com/v1",

# The list of objects to convert.
# May contain one or more objects, in one or more versions.
"objects": [
{
"kind": "CronTab",
"apiVersion": "example.com/v1beta1",
"metadata": {
"creationTimestamp": "2019-09-04T14:03:02Z",
"name": "local-crontab",
"namespace": "default",
"resourceVersion": "143",
"uid": "3415a7fc-162b-4300-b5da-fd6083580d66"
},
"hostPort": "localhost:1234"
}
]
}
}

转换成功的 API 需要响应如下类似数据:

转换响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "ConversionReview",
"response": {
# must match <request.uid>
"uid": "705ab4f5-6393-11e8-b7cc-42010a800002",
"result": {
"status": "Success"
},
# Objects must match the order of request.objects, and have apiVersion set to <request.desiredAPIVersion>.
# kind, metadata.uid, metadata.name, and metadata.namespace fields must not be changed by the webhook.
# metadata.labels and metadata.annotations fields may be changed by the webhook.
# All other changes to metadata fields by the webhook are ignored.
"convertedObjects": [
{
"kind": "CronTab",
"apiVersion": "example.com/v1",
"metadata": {
"creationTimestamp": "2019-09-04T14:03:02Z",
"name": "local-crontab",
"namespace": "default",
"resourceVersion": "143",
"uid": "3415a7fc-162b-4300-b5da-fd6083580d66"
},
"host": "localhost",
"port": "1234"
}
]
}
}

响应的内容要注意:

  1. uid 要和请求时一样;
  2. kindmetadata.uidmetadata.namemetadata.namespace 必需和请求时一样;
  3. metadata.labelsmetadata.annotations 可以修改;
  4. metadata 其它字段的修改会被忽略;
  5. convertedObjects 响应的对象顺序要和请求时一样,且 apiVersion 和请求的 desiredAPIVersion 一样。

简化转换方法数量

如果只有 v1 和 v2 版本,那么只需要开发两个方向的转换方法。但是,如果有 4 个,甚至是 8 个版本时,那转换版本的方法就已经是非常难以维护的了。

当前 controller-runtime 进行 conversion 时,使用的是 Hub and Spoke 模型,得以简化版本转换的维护成本。

Hub and Spoke 可以将网状转换结构转换成星型结构:

网状转换结构转换成星型结构

将一个版本指定成 Hub 版本,当其它非 Hub 版本间转换时,需要先转换成 Hub 版本,再转换成其它版本。

Hub and Spoke

这样可以减少我们需要定义的转换函数数量,并且 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
2
3
4
5
6
7
8
9
10
11
12
// +kubebuilder:object:root=true  
// +kubebuilder:subresource:status
// +kubebuilder:storageversion

// DemoApplication is the Schema for the demoapplications API
type DemoApplication struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec DemoApplicationSpec `json:"spec,omitempty"`
Status DemoApplicationStatus `json:"status,omitempty"`
}

执行 make 命令生成代码和 manifests 配置。

接着执行下面命令生成 webhook 相关代码:

生成 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,增加一行代码:

在新存储版本实现 conversion.Hub 接口
1
func (r *DemoApplication) Hub() {}

修改 api/v1beta1/demoapplication_types.go 增加 Spoke 相关实现:

在旧版本实现转换到 Hub 的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (a *DemoApplication) ConvertTo(dst conversion.Hub) error {
v1beta2App := dst.(*v1beta2.DemoApplication)
v1beta2App.ObjectMeta = a.ObjectMeta
v1beta2App.Spec.Image = a.Spec.Image
v1beta2App.Spec.Size = a.Spec.Replica
v1beta2App.Spec.Ports = a.Spec.Ports

return nil
}

func (a *DemoApplication) ConvertFrom(src conversion.Hub) error {
v1beta2App := src.(*v1beta2.DemoApplication)
a.ObjectMeta = v1beta2App.ObjectMeta
a.Spec.Image = v1beta2App.Spec.Image
a.Spec.Replica = v1beta2App.Spec.Size
a.Spec.Ports = v1beta2App.Spec.Ports

return nil
}

使用以下命令生成部署 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 里的 ../webhookmanager_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

一般集群里都已经安装好了这个组件,如果没安装可以执行以下命令直接安装:

安装 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
2
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]
make: *** [build-installer] Error 1

因为在 webhook 生成时是没启用 admission webhooks 代码生成的,所以并没有对应的 webhook 配置用于 patch,就会报找不到错误。

虽然没有生成 admission webhooks 配置,但是注入的开关和模板代码是提前生成了。我们把下面配置注释掉既可:

1
- path: webhookcainjection_patch.yaml

部署测试

部署测试
1
2
export IMG=registry-c.cmft.com/cmhk-grd-paas-portal/demo-app:5
make docker-build deploy

旧数据的迁移

当部署新版本的 CRD 后,在集群中一般会同时存在多个版本,而只能有一个版本是存储版本。

对于前面的示例来说,我们可以看到,存在 v1beta1v1beta2 两个版本,而 v1beta2 是存储版本:

CRD 最新配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: demoapplications.paas.cmft.com
spec:
versions:
- name: v1beta1
served: true
storage: false
subresources:
status: {}
- name: v1beta2
served: true
storage: true

那部署新版本后,旧数据怎样了呢?

Tips

部署新版本后,旧数据的存储不会变化,只有对数据进行了操作才会以新的存储版本进行重新保存。

所以我们可以通过以下命令看到 CRD 同时存在两个存储版本在使用:

查看当前在使用的存储版本
1
2
3
4
5
$ kubectl get crd demoapplications.paas.cmft.com -ojson | jq .status.storedVersions
[
"v1beta1",
"v1beta2"
]

为什么要迁移

前面提到,当前 controller-runtime 使用的 Hub and Spoke 模型来管理各版本 CRD 的转换。

当 CRD 的结构随版本变化而变化时,维护转换函数的成本会变得越来越大。旧版本应该逐渐弃用,然后移除。所以应该像 Kubernetes 一样,当弃用或移除接口时,自动将旧数据迁移到接的存储版本中。

迁移方法

目前 Kubernetes 建议两种迁移方案。

使用 Storage Version Migrator 工具

Storage Version Migrator 由两个控制器组成:

  1. trigger controller:每 10 分钟调用 discovery 接口获取一次,检测默认存储版本是否变化,如果有变化就给对应 Kind 创建 StorageVersionMigration;
  2. migration controller:负责处理 StorageVersionMigration,当有新的 kind 需要迁移时,migration controller 会将对象全部读取出来再原样写回 API server,触发 API server 使用最新的存储版本进行保存。

Storage Version Migrator 在 Kubernetes 1.30 中是 alpha 状态,小于这个版本的需要手动安装。

本地构建:

构建 SVM 镜像
1
2
3
make all-images
# 如果不远程部署可以不推
make push-all

执行以下命令部署到集群:

部署 SVM 到集群
1
2
3
export REGISTRY=registry-c.cmft.com/cmhk-grd-paas-portal
make local-manifests
pushd manifests.local && kubectl apply -k ./ && popd

手动迁移

手动迁移的动作和 Storage Version Migrator 差不多,这里假定将 v1beta1 升级到 v1

  1. CRD 将新版本设置为存储版本,此时 status.storedVersionsv1beta1v1
  2. 写一个脚本,读取所有的并直接写回 API server,强制以新的存储版本重新保存;
  3. 迁移完成后,手动从 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 就可以完成迁移。

引用

  1. Versions in CustomResourceDefinitions
  2. Tutorial: Multi-Version API
作者

Jakes Lee

发布于

2025-06-23

更新于

2025-06-23

许可协议

评论