随着云计算技术生态的日趋完善,以及各大云计算技术厂商提供PAAS平台能力的日臻成熟,创建Kubernetes集群和在集群上部署应用变得非常容易。尽管K8s Deployment可以实现应用的滚动升级和回滚管理,但事实上应用程序的发布流程千差万别。在遵循Kubernetes的控制器模型和API编程范式前提下,从“在Kubernetes中部署代码"晋级到"使用Kubernetes编写代码”,是Kubernetes用户进阶的过程。接下来,我们将从以下三个方面介绍如何编写一个自定义控制器。
Kubernetes的控制器模型和声明式API对象
Kubernetes API编程范式
如何编写一个自定义控制器
容器的本质是进程,因为容器中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
像上面deployment yaml文件那样,具备以下几个特点的资源对象,就是声明式API对象:
通过一个定义好的API对象,"声明"期望的资源状态
允许有多个API写端,以PATCH的方式对API对象进行修改,而无需关心原始YAML文件的内容
在无需外界干预的情况下,基于对API对象的增删改查,完成对"实际状态"和"期望状态"的调谐
读到这里,想必你已经发现 声明式API对象
和 控制器模型
相辅相成,声明式API对象定义出期望的资源状态,控制器模型则通过控制循环(control loop)把kubernetes内部的资源调整为声明式API对象期望的样子。因此,我们可以认为声明式API对象和控制器模型,才是Kubernetes项目编排能力"赖以生存"的核心所在。
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: spursy
和 version: v1
,也指定了CR资源类型叫做MyResource,复数是myresources、同时还声明该资源是Namespaced的对象。然后,我们就可以使用刚才定义的资源对象:
资源类型指定为MyResource
资源组为spursy
资源的版本号为v1
apiVersion: spursy/v1
kind: MyResource
metadata:
name: myresources.spursyy
spec:
message: hello world
someValue: 13
如果只定义资源对象,而不定义相应的控制,资源对象并不能发挥任何效用。
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编程范式"。
知道了Kubernetes的控制器模型和API的编程范式,接下来我们将介绍如何自定义控制器。
Operator是由CoreOS开发,用来扩展Kubernetes API、特定的应用程序控制器,它可以用来创建、配置和管理复杂的有状态应用,如数据库、缓存和监控系统。接下来我们将使用Operator SDK,自定义用于控制Pod数量的特定资源。简而言之,就是实现类似Kubernetes ReplicaSet类型的资源。
在使用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
# 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
将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