Kubernetes CRD多版本与升级机制

简介: 介绍 在K8s世界,CRD就像是api定义,CRD配套的Operator则是对应的api实现,在系统迭代过程中api会不断的发展,同样的,CRD也会不断的发展,v1alpha1 -> v1alpha2 -> v1beta1 -> v1 -> v2alpha2...,如何在K8s里面让用户轻易得地从低版本升级到高版本是一个十分通用的问题,而正好K8s CRD提供了引入并升

介绍

在K8s世界,CRD就像是api定义,CRD配套的Operator则是对应的api实现,在系统迭代过程中api会不断的发展,同样的,CRD也会不断的发展,v1alpha1 -> v1alpha2 -> v1beta1 -> v1 -> v2alpha2...,如何在K8s里面让用户轻易得地从低版本升级到高版本是一个十分通用的问题,而正好K8s CRD提供了引入并升级到新版本的工作流,本文将深入介绍CRD的多版本机制以及升级流程,希望对任何遇到CRD升级的同学能有一定借鉴作用。

CRD的多版本

例子

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: pizzas.restaurant.programming-kubernetes.info
spec:
  group: restaurant.programming-kubernetes.info
  names:
    kind: Pizza
    listKind: PizzaList
    plural: pizzas
    singular: pizza
  scope: Namespaced
  version: v1alpha1
  versions:
  - name: v1alpha1
    served: true
    storage: true
    schema:
      ...
  - name: v1beta1
    served: true
    storage: false
    schema:
      ...

如上图,Group为restaurant.programming-kubernetes.info,Kind为Pizza的CRD具有两个版本:

  • v1alpha1
  • v1beta1

在实际开发中,初始化CRD只有一个版本,等到需要升级的时候再新增version即可,为了说明多版本,这里直接假设v1alpha1之后新增v1beta1版本,可以看到每个版本的基本结构为:

// CustomResourceDefinitionVersion describes a version for CRD.
type CustomResourceDefinitionVersion struct {
    // name is the version name, e.g. “v1”, “v2beta1”, etc.
    // The custom resources are served under this version at `/apis/<group>/<version>/...` if `served` is true.
    Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
    Served bool `json:"served" protobuf:"varint,2,opt,name=served"`
    Storage bool `json:"storage" protobuf:"varint,3,opt,name=storage"`
    ...
    Schema *CustomResourceValidation `json:"schema,omitempty" protobuf:"bytes,4,opt,name=schema"`
    ...
}

serve version

服务版本,当某个版本v的served为true的时候,/apis/<group>/<v>路径是有效的,否则无效,通过控制served字段,我们可以很简单地控制某个版本是否可以读写,可以有多个版本的served均为true。

storage version

存储版本,当某个版本v的storage为true的时候,说明etcd里面存储的资源版本为v,只能有1个版本的storage为true。1个存储版本,多个服务版本就意味着存储版本和服务版本之间需要做转换,具体转换的机制如何,请继续往下看。

K8s APIExtensionServer工作原理

K8s apiserver相信很多同学都不陌生,所有的K8s组件都与K8s apiserver交互,只有K8s apiserver可以与etcd交互,apiserver是一个restful web server,所有kubectl/client-go客户端的Create,Update,Delete,Get...请求的本质都是发送GET/POST/PUT...restful请求到apiserver,默认情况下apiserver是只认识内置的Deployment/StatefulSet/Service这些类型的资源,为了支持CRD定义的自定义资源引入了APIExtensionServer,在apiserver启动代码里面可以看到:

// If additional API servers are added, they should be gated.
apiExtensionsConfig, err := createAPIExtensionsConfig(*kubeAPIServerConfig.GenericConfig, kubeAPIServerConfig.ExtraConfig.VersionedInformers, pluginInitializer, completedOptions.ServerRunOptions, completedOptions.MasterCount,
    serviceResolver, webhook.NewDefaultAuthenticationInfoResolverWrapper(proxyTransport, kubeAPIServerConfig.GenericConfig.EgressSelector, kubeAPIServerConfig.GenericConfig.LoopbackClientConfig))
if err != nil {
    return nil, err
}
apiExtensionsServer, err := createAPIExtensionsServer(apiExtensionsConfig, genericapiserver.NewEmptyDelegate())
if err != nil {
    return nil, err
}

APIExtensionServer干的事情是引入了自定义资源的request handler:

crdHandler, err := NewCustomResourceDefinitionHandler(
        versionDiscoveryHandler,
        groupDiscoveryHandler,
        s.Informers.Apiextensions().V1().CustomResourceDefinitions(),
        delegateHandler,
        c.ExtraConfig.CRDRESTOptionsGetter,
        c.GenericConfig.AdmissionControl,
        establishingController,
        c.ExtraConfig.ServiceResolver,
        c.ExtraConfig.AuthResolverWrapper,
        c.ExtraConfig.MasterCount,
        s.GenericAPIServer.Authorizer,
        c.GenericConfig.RequestTimeout,
        time.Duration(c.GenericConfig.MinRequestTimeout)*time.Second,
        apiGroupInfo.StaticOpenAPISpec,
        c.GenericConfig.MaxRequestBodyBytes,
    )
if err != nil {
    return nil, err
}
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)

这样一来任意一个不是内置类型的资源请求都会走到该handler,该handler负责自定义资源的读写,每个自定义资源的读写是由Storage对象完成:

storages[v.Name] = customresource.NewStorage(
            resource.GroupResource(),
            kind,
            schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.ListKind},
            customresource.NewStrategy(
                typer,
                crd.Spec.Scope == apiextensionsv1.NamespaceScoped,
                kind,
                validator,
                statusValidator,
                structuralSchemas,
                statusSpec,
                scaleSpec,
            ),
            crdConversionRESTOptionsGetter{
                RESTOptionsGetter:     r.restOptionsGetter,
                converter:             safeConverter,
                decoderVersion:        schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name},
                encoderVersion:        schema.GroupVersion{Group: crd.Spec.Group, Version: storageVersion},
                structuralSchemas:     structuralSchemas,
                structuralSchemaGK:    kind.GroupKind(),
                preserveUnknownFields: crd.Spec.PreserveUnknownFields,
            },
            crd.Status.AcceptedNames.Categories,
            table,
        )

可以看到crdConversionRESTOptionsGetter定义了资源读写的版本转换,具体来说当request里面的version与encoderVersion不一致时,就会进行转换:

// Perform a conversion if necessary
out, err := c.convertor.ConvertToVersion(obj, c.encodeVersion)
if err != nil {
    return err
}

转换的逻辑通常使用webhook来自定义,k8s里面有一个sample conversion webhook

func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion) (runtime.Object, error) {
    listObj, isList := in.(*unstructured.UnstructuredList)
    requestUID := uuid.NewUUID()
    desiredAPIVersion := toGV.String()
    objectsToConvert := getObjectsToConvert(in, desiredAPIVersion)
    request, response, err := createConversionReviewObjects(c.conversionReviewVersions, objectsToConvert, desiredAPIVersion, requestUID)
    if err != nil {
        return nil, err
    }
    ...
    convertedObjects, err := getConvertedObjectsFromResponse(requestUID, response)
    if err != nil {
        return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind().GroupVersionKind(), err)
    }

    if len(convertedObjects) != len(objectsToConvert) {
        return nil, fmt.Errorf("conversion webhook for %v returned %d objects, expected %d", in.GetObjectKind().GroupVersionKind(), len(convertedObjects), len(objectsToConvert))
    }

    if isList {
        // start a deepcopy of the input and fill in the converted objects from the response at the right spots.
        // The response list might be sparse because objects had the right version already.
        convertedList := listObj.DeepCopy()
        ...
        convertedList.SetAPIVersion(toGV.String())
        return convertedList, nil
    }

    if len(convertedObjects) != 1 {
        // This should not happened
        return nil, fmt.Errorf("conversion webhook for %v failed, no objects returned", in.GetObjectKind())
    }
    converted, err := getRawExtensionObject(convertedObjects[0])
    ...
    return converted, nil
}

该webhook可以在CRD里面定义:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: pizzas.restaurant.programming-kubernetes.info
spec:
  group: restaurant.programming-kubernetes.info
  names:
    kind: Pizza
    listKind: PizzaList
    plural: pizzas
    singular: pizza
  scope: Namespaced
  version: v1alpha1
  versions:
  - name: v1alpha1
    served: true
    storage: true
    ...
  - name: v1beta1
    served: true
    storage: false
    ...
  preserveUnknownFields: false
  conversion:
    strategy: Webhook
    webhookClientConfig:
      caBundle: <CA>
      service:
        namespace: pizza-crd
        name: webhook
        path: /convert/v1beta1/pizza

整体的流程看起来就是:

image.png

一句话总结,当写自定义资源时,该资源会持久化为storage version,如果请求版本与storage version不一致会做转换;当读自定义资源时,如果请求版本与storage version不同,也会做转换。注意,如果storage version变了,底层etcd里面的资源版本不会自动改变,只有重新写才会改变。

CRD的版本升级机制

增加新版本

  • spec.versions里面新增版本,并将其served置为true,storage置为false;
  • 选定转换策略并部署conversion webhook;
  • 配置spec.conversion.webhookClientConfig到conversion webhook;

当新版增加之后,新老两个版本都可以并行使用,对用户没有任何影响,要达到这样的状态意味着你的conversion需要做v1alpha1 -> v1beta1以及v1beta1 -> v1alpha1的双向支持,如果有N个版本,转换的可能性为N * (N-1),因此我建议尽量同时支持最多不超过3个版本,与此同时,如果版本没有对外开放,可以只做v1alpha1 -> v1beta1一个方向的转化,一把迁移过来。

迁移到最新的存储版本

  • spec.versions里面将新版的storage置为true,老版本的storage置为false;
  • 写一个migrator,将所有资源读1遍写1遍,这样自动写到最新的storage version了;
  • 将旧版本从status.storedVersions去除;

下线旧版本

  • 确保所有的客户端都升级到新版本,可以通过审计日志确定;
  • spec.versions将旧版本的served置为false,这一步可以几个小时甚至数天,有问题可以置为true回滚;
  • 确保存储版本已经升级到最新;
  • spec.versions删除旧版本;
  • 下掉conversion hook;

参考

文档

书籍

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
2月前
|
Kubernetes Linux 测试技术
|
2月前
|
Kubernetes Linux 开发工具
centos7通过kubeadm安装k8s 1.27.1版本
centos7通过kubeadm安装k8s 1.27.1版本
|
3月前
|
Kubernetes 负载均衡 前端开发
二进制部署Kubernetes 1.23.15版本高可用集群实战
使用二进制文件部署Kubernetes 1.23.15版本高可用集群的详细教程,涵盖了从环境准备到网络插件部署的完整流程。
130 2
二进制部署Kubernetes 1.23.15版本高可用集群实战
|
2月前
|
消息中间件 Java Kafka
Kafka ACK机制详解!
本文深入剖析了Kafka的ACK机制,涵盖其原理、源码分析及应用场景,并探讨了acks=0、acks=1和acks=all三种级别的优缺点。文中还介绍了ISR(同步副本)的工作原理及其维护机制,帮助读者理解如何在性能与可靠性之间找到最佳平衡。适合希望深入了解Kafka消息传递机制的开发者阅读。
246 0
|
3月前
|
存储 Kubernetes Ubuntu
Ubuntu 22.04LTS版本二进制部署K8S 1.30+版本
这篇文章详细介绍了在Ubuntu 22.04 LTS系统上使用VMware Fusion虚拟化软件部署Kubernetes 1.30+版本的完整过程,包括环境准备、安装containerd、配置etcd、生成证书、部署高可用组件、启动Kubernetes核心组件以及网络插件的部署和故障排查。
236 4
|
4月前
|
Kubernetes 容器 Perl
【Azure K8S】AKS升级 Kubernetes version 失败问题的分析与解决
【Azure K8S】AKS升级 Kubernetes version 失败问题的分析与解决
|
4月前
|
Kubernetes 监控 Perl
在k8S中,自动扩容机制是什么?
在k8S中,自动扩容机制是什么?
|
4月前
|
Kubernetes 监控 API
在K8S中,RS资源如何实现升级和回滚?
在K8S中,RS资源如何实现升级和回滚?
|
4月前
|
Kubernetes API 开发工具
在K8S中,Deployment的升级过程是什么?
在K8S中,Deployment的升级过程是什么?
|
4月前
|
存储 网络安全 API
【Azure Service Bus】 Service Bus如何确保消息发送成功,发送端是否有Ack机制 
【Azure Service Bus】 Service Bus如何确保消息发送成功,发送端是否有Ack机制 

热门文章

最新文章

推荐镜像

更多