当前位置: 首页 > 工具软件 > dc-sdk > 使用案例 >

使用operator-sdk在Kubernetes中编写自定义控制器CRD

苏鸿志
2023-12-01

随着云计算技术生态的日趋完善,以及各大云计算技术厂商提供PAAS平台能力的日臻成熟,创建Kubernetes集群和在集群上部署应用变得非常容易。尽管K8s Deployment可以实现应用的滚动升级和回滚管理,但事实上应用程序的发布流程千差万别。在遵循Kubernetes的控制器模型和API编程范式前提下,从“在Kubernetes中部署代码"晋级到"使用Kubernetes编写代码”,是Kubernetes用户进阶的过程。接下来,我们将从以下三个方面介绍如何编写一个自定义控制器。

  • Kubernetes的控制器模型和声明式API对象

  • Kubernetes API编程范式

  • 如何编写一个自定义控制器


1. Kubernetes的控制器模型和声明式API对象

1.1 控制器模型

容器的本质是进程,因为容器中PID=1的进程是容器自身,其他进程都是这个PID=1进程的子进程。Pod只是一个逻辑概念,Kubernetes真正要处理的还是宿主机中构成容器的Cgroups和Namespace,因此也可认为Pod扮演了基础设施中"虚拟机"的角色,容器是运行在Pod中的"应用程序"。Kubernetes提供了一种实现Pod自动伸缩、滚动升级、回滚的机制叫控制器。Kubernetes提供了很多控制器,如果我们查看pkg/controller目录:

deployment
replicaset
replication
statefulset
job
podautoscaler
cronjob
nodelifecycle

尽管以上每一个控制器负责不同资源的编排工作,但是它们都遵循最基本的控制循环(control loop)原理,本节我们主要介绍deployment。首先,我们一起来看一个deployment的yaml文件:

apiVersion: apps/v1    
kind: Deployment     
metadata:        
  name: nginx-deployment
  labels:
    app: nginx
spec:          
  replicas: 3       
  selector:
    matchLabels:
      app: nginx
  template:        
    metadata:       
      labels:
        app: nginx
    spec:              
      containers:
      - name: nginx
        image: nginx:1.13.12
        ports:
        - containerPort: 80

简单的说,上述deployment的作用是为了确保携带 app:nginx 标签的Pod数量永远等于spec.replicas指定的数量3。我们可以把控制器控制循环的实现原理归纳如下:

  • deployment控制器从etcd中获取集群携带特定标签的Pod数量(Pod实际数量)

  • deployment yaml文件中描述的replicas字段值

  • deployment比较以上结果,确定是创建新的Pod还是删除老的Pod

1.2 声明式API对象

像上面deployment yaml文件那样,具备以下几个特点的资源对象,就是声明式API对象:

  • 通过一个定义好的API对象,"声明"期望的资源状态

  • 允许有多个API写端,以PATCH的方式对API对象进行修改,而无需关心原始YAML文件的内容

  • 在无需外界干预的情况下,基于对API对象的增删改查,完成对"实际状态"和"期望状态"的调谐

读到这里,想必你已经发现 声明式API对象控制器模型 相辅相成,声明式API对象定义出期望的资源状态,控制器模型则通过控制循环(control loop)把kubernetes内部的资源调整为声明式API对象期望的样子。因此,我们可以认为声明式API对象和控制器模型,才是Kubernetes项目编排能力"赖以生存"的核心所在。


2. 声明式API对象的编程范式

2.1 API对象的组织方式

API对象在etcd里的完整资源路径是由 Group(API组)、Version(API版本)和Resource(API资源类型)三部分组成。Kubernetes创建资源对象的流程:

  • 首先,Kubernetes读取用户提交的yaml文件

  • 然后,Kubernetes去匹配yaml文件中API对象的组

  • 再次,Kubernetes去匹配yaml文件中API对象的版本号

  • 最后,Kubernetes去匹配yaml文件中API对象的资源类型

因此,我们需要根据需求,先进行自定义资源(CRD - Custom Resource Definition),它将包括API对象组、版本号、资源类型:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
    name: myresources.spursyy
spec: 
    group: spursy
    version: v1
    names:
      kind: MyResource
      plural: myresources
    scope: Namespaced  

在上面的yaml文件中,指定API对象组和版本号信息 group: spursyversion: v1 ,也指定了CR资源类型叫做MyResource,复数是myresources、同时还声明该资源是Namespaced的对象。然后,我们就可以使用刚才定义的资源对象:

  • 资源类型指定为MyResource

  • 资源组为spursy

  • 资源的版本号为v1

apiVersion: spursy/v1
kind: MyResource
metadata:
    name: myresources.spursyy
spec: 
    message: hello world
    someValue: 13

如果只定义资源对象,而不定义相应的控制,资源对象并不能发挥任何效用。

2.2 自定义资源控制器

  • 控制器与APIServer通信

Informer是APIServer与Kubernetes互相通信的桥梁,它通过Reflector实现ListAndWatch方法来“获取”和“监听”对象实例的变化。每当APIServer接收到创建、更新和删除实例的请求,Refector都会收到“事件通知”,然后将变更的事件推送到先进先出的队列中。Informer会不断从上一队列中读取增量,然后根据增量事件的类型创建或者更新本地对象的缓存。Informer会根据事件类型触发事先定义好的ResourceEventHandler(具体为AddFunc、UpdatedFunc和DeleteFunc,分别对应API对象的“添加”、“更新”和“删除”事件),同时每隔一定的时间,Informer也会对本地的缓存进行一次强制更新。

  • WorkQueue同步Informer和控制循环(Control Loop)交互的数据

  • Controller Loop扮演Kubernetes控制器的角色,确保期望与实际的运行状态是一致的。

工作原理图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d1WscQqm-1577432281619)(quiver-image-url/261503B39AAC0CFDDA06F35DC4B07B04.png =1207x550)]

综上所述,如何使用控制器模式,与Kubernetes里API对象的“增、删、改、查”进行协作,进而完成用户业务逻辑的编写过程,这就是声明式API对象的编程范式,即"Kubernetes编程范式"。


3. Operator

知道了Kubernetes的控制器模型和API的编程范式,接下来我们将介绍如何自定义控制器。

Operator是由CoreOS开发,用来扩展Kubernetes API、特定的应用程序控制器,它可以用来创建、配置和管理复杂的有状态应用,如数据库、缓存和监控系统。接下来我们将使用Operator SDK,自定义用于控制Pod数量的特定资源。简而言之,就是实现类似Kubernetes ReplicaSet类型的资源。

在使用Operator SDK自定义资源前,我们需要明确两点:

  • Operator SDK的工作流

使用SDK创建一个新的Operator项目,接着通过添加自定义资源(CRD)定义新的资源API,然后指定使用SDK API来watch的资源,再次定义Operator的协调(reconcile)逻辑,最后使用Operator SDK构建并生成Operator部署清单文件。

  • 明确第一资源和第二资源

我们即将实现类似ReplicaSet的自定义资源,第一资源是ReplicaSet自身(明确指定运行的Docker镜像和ReplicaSet中Pod的数量)、第二资源是运行的Pod。当ReplicaSet中属性发生变化(如自定的Docker镜像,或者指定Pod副本的数量)或者Pod发生变化(如Pod的实际运行数量减少),Controller控制器通过前文讲的控制循环,一旦发现上述变化,就会通过变更Pod中镜像的版本或者伸缩Pod的数量,调谐(reconcile)集群中ReplicaSet资源的状态。

了解Operator的原理,接下来我将通过一个demo演示如何通过 Operator SDK创建资源。

注:Operator SDK:https://github.com/operator-framework/operator-sdk


4. 使用Operator SDK生成go项目框架

# operator-sdk new podset-operator

# cd podset-operator

生成的项目文件结构如下图:

podset-operator# tree

.
├── build
│   ├── Dockerfile
│   └── bin
│       ├── entrypoint
│       └── user_setup
├── cmd
│   └── manager
│       └── main.go
├── deploy
│   ├── operator.yaml
│   ├── role.yaml
│   ├── role_binding.yaml
│   └── service_account.yaml
├── go.mod
├── go.sum
├── pkg
│   ├── apis
│   │   └── apis.go
│   └── controller
│       └── controller.go
├── tools.go
└── version
    └── version.go

为刚才生成代码添加自定义API。

# operator-sdk add api --api-version=app.example.com/v1alpha1 --kind=PodSet

INFO[0000] Generating api version app.example.com/v1alpha1 for kind PodSet.
INFO[0000] Created pkg/apis/app/group.go
INFO[0019] Created pkg/apis/app/v1alpha1/podset_types.go
INFO[0019] Created pkg/apis/addtoscheme_app_v1alpha1.go
INFO[0019] Created pkg/apis/app/v1alpha1/register.go
INFO[0019] Created pkg/apis/app/v1alpha1/doc.go
INFO[0019] Created deploy/crds/app.example.com_v1alpha1_podset_cr.yaml
INFO[0025] Created deploy/crds/app.example.com_podsets_crd.yaml
INFO[0025] Running deepcopy code-generation for Custom Resource group versions: [app:[v1alpha1], ]
INFO[0035] Code-generation complete.
INFO[0035] Running CRD generation for Custom Resource group versions: [app:[v1alpha1], ]
INFO[0036] Created deploy/crds/app.example.com_podsets_crd.yaml
INFO[0036] CRD generation complete.
INFO[0036] API generation complete.

添加自定义控制器。

# operator-sdk add controller --api-version=app.example.com/v1alpha1 --kind=PodSet

INFO[0000] Generating controller version app.example.com/v1alpha1 for kind PodSet.
INFO[0000] Created pkg/controller/podset/podset_controller.go
INFO[0000] Created pkg/controller/add_podset.go
INFO[0000] Controller generation complete.

修改podset-operator/pkg/apis/app/v1alpha1/podset_types.go文件中的PodSetSpec和PodSetStatus

type PodSetSpec struct {
    Replicas int32 `json:"replicas"`
}

type PodSetStatus struct {
    Replicas int32 `json:"replicas"`
    PodNames []string `json:"podNames"`
}

需要特别注意:我们一旦对OperatorSDK生成的框架做任何修改,都需要执行 operator-sdk generate k8s,重新生成相应的 podset-operator/pkg/apis/app/v1alpha1/zz_generated.deepcopy.go 文件

# cd podset-operator

# sudo operator-sdk generate k8s

INFO[0000] Running deepcopy code-generation for Custom Resource group versions: [app:[v1alpha1], ]
INFO[0010] Code-generation complete.

PodSet或者归属于PodSet的Pod一旦发生变化都会触发reconcile函数。无论是增加还是删除Pod,Reconcile函数每次都只能增删一个Pod,然后返回,等待下一次触发Reconcile函数。确保归属于PodSet第一资源的Pod使用controllerutil.SetControllerReference()函数,这样当第一资源删除时,系统会自动把相应的Pod删除。控制器的实现逻辑可参见:https://github.com/spursy/podset-operator/blob/master/pkg/controller/podset/podset_controller.go#L86


5. 使用Operator SDK部署文件

将Operator项目打包成镜像

# cd podset-operator

# sudo operator-sdk build spursyy/podset-operator

推送到docker hub

# docker push spursyy/podset-operator

修改operator.yaml文件

# cd podset-operator

# sudo sed -i "" 's|REPLACE_IMAGE|spursyy/podset-operator|g' deploy/operator.yaml

把上一步生成的文件部署到集群中,创建service account

# kubectl create -f deploy/service_account.yaml

为service account做RBAC认证

# kubectl create -f deploy/role.yaml

# kubectl create -f deploy/role_binding.yaml

部署CRD和Operator文件

# kubectl create -f deploy/crds/app_v1alpha1_podset_crd.yaml

# kubectl create -f deploy/operator.yaml

以上我们定义了CRD,并把Operator SDK生成的控制器代码部署到集群后,我们就可以实现在集群中部署自定义的资源。

echo "apiVersion: app.example.com/v1alpha1
kind: PodSet
metadata:
  name: example-podset
spec:
  replicas: 3" | oc create -f -

完整代码:https://github.com/spursy/podset-operator

 类似资料: