大家好,我是乔克。
在Kubernetes中,Pod是最小的调度单元,它由各种各样的Controller管理,比如ReplicaSet Controller,Deployment Controller等。
Kubernetes内置了许多Controller,这些Controller能满足80%的业务需求,但是企业里也难免需要自定义Controller来适配自己的业务需求。
网上自定义Controller的文章很多,基本都差不多。俗话说:光说不练假把式
,本篇文章主要是自己的一个实践归档总结,如果对你有帮助,可以一键三连
!
本文主要从以下几个方面进行介绍,其中包括理论部分和具体实践部分。
Controller的实现逻辑
当我们向kube-apiserver提出创建一个Deployment
需求的时候,首先是会把这个需求存储到Etcd中,如果这时候没有Controller的话,这条数据仅仅是存在Etcd中,并没有产生实际的作用。
所以就有了Deployment Controller,它实时监听kube-apiserver中的Deployment对象,如果对象有增加、删除、修改等变化,它就会做出相应的相应处理,如下:
// pkg/controller/deployment/deployment_controller.go 121行 ..... dInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: dc.addDeployment, UpdateFunc: dc.updateDeployment, // This will enter the sync loop and no-op, because the deployment has been deleted from the store. DeleteFunc: dc.deleteDeployment, }) ......
其实现的逻辑图如下(图片来自网络):
可以看到图的上半部分都由client-go
实现了,下半部分才是我们具体需要去处理的。
client-go
主要包含Reflector
、Informer
、Indexer
三个组件。
Reflector
会List&Watch
kube-apiserver中的特定资源,然后会把变化的资源放入Delta FIFO
队列中。Informer
会从Delta FIFO
队列中拿取对象交给相应的HandleDeltas
。Indexer
会将对象存储到缓存中。
上面部分不需要我们去开发,我们主要关注下半部分。
当把数据交给Informer
的回调函数HandleDeltas
后,Distribute
会将资源对象分发到具体的处理函数,这些处理函数通过一系列判断过后,把满足需求的对象放入Workqueue
中,然后再进行后续的处理。
code-generator介绍
上一节说到我们只需要去实现具体的业务需求,这是为什么呢?主要是因为kubernetes
为我们提供了code-generator
【1】这样的代码生成器工具,可以通过它自动生成客户端访问的一些代码,比如Informer
、ClientSet
等。
code-generator
提供了以下工具为Kubernetes
中的资源生成代码:
- deepcopy-gen:生成深度拷贝方法,为每个 T 类型生成 func (t* T) DeepCopy() *T 方法,API 类型都需要实现深拷贝
- client-gen:为资源生成标准的 clientset
- informer-gen:生成 informer,提供事件机制来响应资源的事件
- lister-gen:生成 Lister**,**为 get 和 list 请求提供只读缓存层(通过 indexer 获取)
如果需要自动生成,就需要在代码中加入对应格式的配置,如下:
其中:
// +genclient
表示需要创建client// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
表示在需要实现k8s.io/apimachinery/pkg/runtime.Object
这个接口
除此还有更多的用法,可以参考Kubernetes Deep Dive: Code Generation for CustomResources
【2】进行学习。
CRD介绍
CRD
全称CustomResourceDefinition
,中文简称自定义资源
,上面说的Controller主要就是用来管理自定义的资源
。
我们可以通过下面命令来查看当前集群中使用了哪些CRD
,如下:
# kubectl get crd NAME CREATED AT ackalertrules.alert.alibabacloud.com 2021-06-15T02:19:59Z alertmanagers.monitoring.coreos.com 2019-12-12T12:50:00Z aliyunlogconfigs.log.alibabacloud.com 2019-12-02T10:15:02Z apmservers.apm.k8s.elastic.co 2020-09-14T01:52:53Z batchreleases.alicloud.com 2019-12-02T10:15:53Z beats.beat.k8s.elastic.co 2020-09-14T01:52:53Z chaosblades.chaosblade.io 2021-06-15T02:30:54Z elasticsearches.elasticsearch.k8s.elastic.co 2020-09-14T01:52:53Z enterprisesearches.enterprisesearch.k8s.elastic.co 2020-09-14T01:52:53Z globaljobs.jobs.aliyun.com 2020-04-26T14:40:53Z kibanas.kibana.k8s.elastic.co 2020-09-14T01:52:54Z prometheuses.monitoring.coreos.com 2019-12-12T12:50:01Z prometheusrules.monitoring.coreos.com 2019-12-12T12:50:02Z servicemonitors.monitoring.coreos.com 2019-12-12T12:50:03Z
但是仅仅是创建一个CRD
对象是不够的,因为它是静态
的,创建过后仅仅是保存在Etcd
中,如果需要其有意义,就需要Controller
配合。
创建CRD
的例子如下:
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: # name 必须匹配下面的spec字段:<plural>.<group> name: students.coolops.io spec: # group 名用于 REST API 中的定义:/apis/<group>/<version> group: coolops.io # 列出自定义资源的所有 API 版本 versions: - name: v1 # 版本名称,比如 v1、v1beta1 served: true # 是否开启通过 REST APIs 访问 `/apis/<group>/<version>/...` storage: true # 必须将一个且只有一个版本标记为存储版本 schema: # 定义自定义对象的声明规范 openAPIV3Schema: type: object properties: spec: type: object properties: name: type: string school: type: string scope: Namespaced # 定义作用范围:Namespaced(命名空间级别)或者 Cluster(整个集群) names: plural: students # plural 名字用于 REST API 中的定义:/apis/<group>/<version>/<plural> shortNames: # shortNames 相当于缩写形式 - stu kind: Student # kind 是 sigular 的一个驼峰形式定义,在资源清单中会使用 singular: student # singular 名称用于 CLI 操作或显示的一个别名
具体演示
本来准备根据官方的demo
【3】进行讲解,但是感觉有点敷衍,而且这类教程网上一大堆,所以就准备自己实现一个数据库管理的一个Controller。
因为是演示怎么开发Controller,所以功能不会复杂,主要的功能是:
- 创建数据库实例
- 删除数据库实例
- 更新数据库实例
开发环境说明
本次实验环境如下:
软件 | 版本 |
kubernetes | v1.22.3 |
go | 1.17.3 |
操作系统 | CentOS 7.6 |
创建CRD
CRD
是基础,Controller
主要是为CRD
服务的,所以我们要先定义好CRD
资源,便于开发。
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: databasemanagers.coolops.cn spec: group: coolops.cn versions: - name: v1alpha1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: deploymentName: type: strin replicas: type: integer minimum: 1 maximum: 10 dbtype: type: string status: type: object properties: availableReplicas: type: integer names: kind: DatabaseManager plural: databasemanagers singular: databasemanager shortNames: - dm scope: Namespaced
创建CRD,检验是否能创建成功。
# kubectl apply -f crd.yaml customresourcedefinition.apiextensions.k8s.io/databasemanagers.coolops.cn created # kubectl get crd | grep databasemanagers databasemanagers.coolops.cn 2021-11-22T02:31:29Z
自定义一个测试用例,如下:
apiVersion: coolops.cn/v1alpha1 kind: DatabaseManager metadata: name: example-mysql spec: dbtype: "mysql" deploymentName: "example-mysql" replicas: 1
创建后进行查看:
# kubectl apply -f example-mysql.yaml databasemanager.coolops.cn/example-mysql created # kubectl get dm NAME AGE example-mysql 9s
不过现在仅仅是创建了一个静态数据,并没有任何实际的应用,下面来编写Controller来管理这个CRD。
开发Controller
自动生成代码
1、创建项目目录database-manager-controller,并进行go mod 初始化
# mkdir database-manager-controller # cd database-manager-controller # go mod init
2、创建源码包目录pkg/apis/databasemanager
# mkdir pkg/apis/databasemanager -p # cd pkg/apis/databasemanager
3、在pkg/apis/databasemanager目录下创建register.go文件,并写入一下内容
package databasemanager // GroupName is the group for database manager const ( GroupName = "coolops.cn" )
4、在pkg/apis/databasemanager目录下创建v1alpha1目录,进行版本管理
# mkdir v1alpha1 # cd v1alpha1
5、在v1alpha1目录下创建doc.go文件,并写入以下内容
// +k8s:deepcopy-gen=package // +groupName=coolops.cn // Package v1alpha1 is the v1alpha1 version of the API package v1alpha1
其中// +k8s:deepcopy-gen=package
和// +groupName=coolops.cn
都是为了自动生成代码而写的配置。
6、在v1alpha1目录下创建type.go文件,并写入以下内容
package v1alpha1 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type DatabaseManager struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec DatabaseManagerSpec `json:"spec"` Status DatabaseManagerStatus `json:"status"` } // DatabaseManagerSpec 期望状态 type DatabaseManagerSpec struct { DeploymentName string `json:"deploymentName"` Replicas *int32 `json:"replicas"` Dbtype string `json:"dbtype"` } // DatabaseManagerStatus 当前状态 type DatabaseManagerStatus struct { AvailableReplicas int32 `json:"availableReplicas"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // DatabaseManagerList is a list of DatabaseManagerList resources type DatabaseManagerList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` Items []DatabaseManager `json:"items"` }
type.go
主要定义我们的资源类型。
7、在v1alpha1目录下创建register.go文件,并写入以下内容
package v1alpha1 import ( dbcontroller "database-manager-controller/pkg/apis/databasemanager" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) // SchemeGroupVersion is group version used to register these objects var SchemeGroupVersion = schema.GroupVersion{Group: dbcontroller.GroupName, Version: dbcontroller.Version} // Kind takes an unqualified kind and returns back a Group qualified GroupKind func Kind(kind string) schema.GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( // SchemeBuilder initializes a scheme builder SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme is a global function that registers this API group & version to a scheme AddToScheme = SchemeBuilder.AddToScheme ) // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &DatabaseManager{}, &DatabaseManagerList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil }
register.go
的作用是通过addKnownTypes
方法使得client
可以知道DatabaseManager
类型的API对象。
至此,自动生成代码的准备工作完成了,目前的代码目录结构如下:
# tree . . ├── artifacts │ └── database-manager │ ├── crd.yaml │ └── example-mysql.yaml ├── go.mod ├── go.sum ├── LICENSE ├── pkg │ └── apis │ └── databasemanager │ ├── register.go │ └── v1alpha1 │ ├── doc.go │ ├── register.go │ └── type.go
接下里就使用code-generator进行代码自动生成了。