如何在Kubernetes中编写自定义控制器

3,517 阅读8分钟

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

接下来我将从一下三个方面介绍如何编写一个自定义控制器:

  1. Kubernetes的控制器模型和声明式API对象
  2. Kubernetes API编程范式
  3. 如何编写一个自定控制器

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

容器的本质是进程,因此容器里PID=1的进程是应用本身,其它的进程都是这个PID=1的进程的子进程。Pod只是一个逻辑概念,Kubernetes真正要处理的还是宿主机的Linux容器和Namespace和Cgroups。因此也可以认为Pod在扮演传统基础设施里“虚拟机”的角色,而容器,则是运行在这个虚拟机里的用户程序。Kubernetes提供一种实现Pod自动伸缩、滚动升级、回滚的机制叫控制器。

Kubernetes中提供很多控制器,如果我们查看pkg/controller目录:

ls **/pgk/controller

deployment/             job/                    podautoscaler/          
cloud/                  disruption/             namespace/              
replicaset/             serviceaccount/         volume/
cronjob/                garbagecollector/       nodelifecycle/   
replication/            statefulset/            daemon/
...

尽管以上每一个控制器负责不同资源资源的编排工作,但是它们都遵循最基本的控制循环(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.7.9
        ports:
        - containerPort: 80

简单将上述Deployment的作用就是为了确保携带 app:nginx 标签的Pod数量永远等于spec.replicass 指定的数量3。

可以将控制器控制循环的实现原理作如下归纳:

  1. Deployment控制器从ETCD中获取集群中携带特定标签的Pod数量(Pod的实际数量)
  2. Deployent Yaml文件中描述的Replicas字段的值 (Pod的期望数量)
  3. Deployment比较以上结果,确定是创建新的Pod还是删除老的Pod

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

  1. 通过一个定义好的API对象来“声明”期望的资源状态是什么样子
  2. 允许有多个API写端,以PATCH的方式对API对象进行修改,而无需关心原始YAML文件的内容
  3. 基于对API对象的增删改查在无需外接干预的情况下,完成对“实际状态”和“期望状态”的调谐过程

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

声明式API对象的编程范式

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文件中指定group: spursyyversion: v1API的组和版本号信息、也指定了CR资源类型叫做MyResource,复数是myresources、同时还声明该资源是Namespaced的对象。

然后我们就可以使用刚才定义的资源对象:

  • 资源类型指定为MyResource
  • 资源组为spursy
  • 资源的版本号为v1
apiVersion: spursy/v1
kind: MyResource
metadata:
  name: myresources.spursyy
spec:
  message: hello world
  someValue: 13

如果只定义资源对象,而不定义相应的控制,资源对象并不能发挥任何效用。接下来我们一起看看如何自定义资源控制器。

自定义控制器的原理

a. 控制器如何与APIServer通信

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

b. WorkQueue同步Informer跟控制循环(Control Loop)交互的数据

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

以上工作原理如下图(引用深入剖析Kubernetes):

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

编写自定义控制器

Operator

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

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

1. Operator SDK的工作流

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

2. 明确第一资源和第二资源

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

实践Operator SDK

  1. 安装Operator SDK

可参见官方文档github.com/operator-fr…

  1. 生成go项目框架

operator-sdk new podset-operator

  1. 添加自定义API

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

  1. 添加自定义控制器

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

  1. 修改 */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"`
}

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

  1. 最后我们需要实现控制器中自动伸缩的代码

代码修改是在控制器Reconcile的函数中/podset-operator/pkg/controller/podset/podset_controller.go

我需要明确一下逻辑:

  • PodSet或者归属于PodSet的Pod一旦发生变化都会触发reconcile函数
  • 无论是增加还是删除Pod,Reconcile函数每次都只能增删一个Pod,然后返回,等待下一次触发Reconcile函数
  • 确保归属于PodSet第一资源的Pod使用controllerutil.SetControllerReference()函数,这样当第一资源删除时,系统会自动将相应的Pod删除

以上代码实现可参见github.com/spursy/pods…

  1. 生成部署文件
  • 将Operator项目打包成镜像

operator-sdk build spursyy/podset-operator

  • 推送到docker hub

docker push spursyy/podset-operator

  • 修改operator.yaml文件

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

  1. 部署到集群中
  • 创建service account

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
  • 最后部署一个3副本的podset
echo "apiVersion: app.example.com/v1alpha1
kind: PodSet
metadata:
  name: example-podset
spec:
  replicas: 3" | oc create -f -

以上示例可参见githubgithub.com/spursy/pods…