写在前面
这篇分享是我在公司内部给其他不太熟悉K8s的新同事做分享的文章,因为面向的基本都是硕士刚毕业对K8s不熟悉的校招生,因此内容比较浅显,不涉及太底层的东西,但是看了这篇文章以后基本能对K8s的Opeator做一个基本的了解,但是如果想要掌握一种非常专业的技术,光看几篇分享的文章肯定是不行的,因此建议大家还是尽量看官方文档
什么是 K8s 的 CRD 和 Operator?
Kubernetes 的 CRD,全称 Custom Resource Definition,是 Kubernetes 的一种资源,允许用户自定义新的资源类型。除了 CRD 本身,用户还需要提供一个 Controller,以实现自己的逻辑。CRD 允许用户基于已有的 Kubernetes 资源(例如 Deployment、Configmap 等)拓展集群能力。它是一种自定义资源的定义,用于描述用户定义的资源是什么样子。
Operator=CRD+Controller.
怎么理解这个CRD+Controller呢,就是你通过编写yaml文件,然后apply这个yaml文件的方式可以生成一个CRD,这个yaml文件要满足一定的规则,接下来我会详细讲到CRD的yaml的编写,在apply生成一个CRD以后,你可以再基于这个CRD来创建一个CR(也是apply一个yaml的方式),在CR创建成功以后你的具体业务逻辑就可以开始执行了,业务逻辑的执行是依赖于CRD对应的Controller(这里还涉及到K8s的list-watch机制等等,后续我会写一些文章尽量简单的说清楚这个list-watch机制),这个Controller会监测对应CRD,如果有基于这个CRD的CR创建,那么Controller就会去执行预先在代码中定义好的逻辑(Controller都是基于Golang实现)
CRD 怎么使用
我们以一个经典(k8s 官网的例子)的 CRD 为例
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# 名字必需与下面的 spec 字段匹配,并且格式为 '<名称的复数形式>.<组名>'
name: crontabs.stable.example.com
spec:
# 组名称,用于 REST API: /apis/<组>/<版本>
group: stable.example.com
# 列举此 CustomResourceDefinition 所支持的版本
versions:
- name: v1
# 每个版本都可以通过 served 标志来独立启用或禁止
served: true
# 其中一个且只有一个版本必需被标记为存储版本
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
# 可以是 Namespaced 或 Cluster
scope: Namespaced
names:
# 名称的复数形式,用于 URL:/apis/<组>/<版本>/<名称的复数形式>
plural: crontabs
# 名称的单数形式,作为命令行使用时和显示时的别名
singular: crontab
# kind 通常是单数形式的驼峰命名(CamelCased)形式。你的资源清单会使用这一形式。
kind: CronTab
# shortNames 允许你在命令行使用较短的字符串来匹配资源
shortNames:
- ct
现在我们将它保存到 resourcedefinition.yaml,并且执行:
kubectl apply -f resourcedefinition.yaml
这个时候 K8S 会分析 CRD 定义,检查其有效性。包括 API 版本、元数据、名称、定义规范等,如果通过的话 API Server 会将 CRD 对象存储起来,并持久化在 Etcd 中。接下来你会得到这样的反馈:
这个时候一个新的受namespace约束的 RESTful API 端点会被创建在:
/apis/stable.example.com/v1/namespaces/*/crontabs/…
这个时候就代表我们在我们的 k8s 集群中创建了一个名为 crontabs.stable.example.com 的 CRD,当我们使用
kubectl get crds
的时候可以看到下图的结果,注意:CRD 是不区分namespace的
现在我们已经成功在自己的 K8s 集群中创建了一个 CRD,现在我们可以基于这个 CRD 创建一个自定义对象 CR,现在我们创建一个 my-crontab.yaml 的文件,内容是:
apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
name: my-new-cron-object
spec:
# 这个字段的内容需要满足上面CRD的规约
cronSpec: "* * * * */5"
image: my-awesome-cron-image
apply 这个 yaml(注意:这个时候就是区分 ns 的了),接着执行就可以看到相应内容:
kubectl get ct #这个ct是crontab的short-name
到这里我们就创建了一个 CRD 和基于这个 CRD 创建一个 CR 了,那这样的 CR 有什么用呢,上面这个例子不明显,我们这里以 ArgoWorkflow 为例:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# 名字必需与下面的 spec 字段匹配,并且格式为 '<名称的复数形式>.<组名>'
name: workflowtemplates.argoproj.io
spec:
# 组名称,用于 REST API: /apis/<组>/<版本>
group: argoproj.io
names:
# kind 通常是单数形式的驼峰命名(CamelCased)形式。你的资源清单会使用这一形式。
kind: WorkflowTemplate
listKind: WorkflowTemplateList
# 名称的复数形式,用于 URL:/apis/<组>/<版本>/<名称的复数形式>
plural: workflowtemplates
shortNames:
- wftmpl
# 名称的单数形式,作为命令行使用时和显示时的别名
singular: workflowtemplate
# 这里可以是ns级别或者cluster级别
scope: Namespaced
# 列举此 CustomResourceDefinition 所支持的版本
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
type: object
x-kubernetes-map-type: atomic
x-kubernetes-preserve-unknown-fields: true
required:
- metadata
- spec
type: object
# 每个版本都可以通过 served 标志来独立启用或禁止
served: true
# 其中一个且只有一个版本必需被标记为存储版本
storage: true
现在我们将它写入 wftmpl-crd.yaml,并且执行
kubectl apply -f wftmpl-crd.yaml -n argo
我们创建了一个名为 workflowtemplates.argoproj.io 的 CRD 资源,基于这个 CRD 资源我们就可以实现 一个Workflow的工作流程,具体是通过创建一个 CR 来实现,当然,不是单独这样一个 yaml 文件就可以实现复杂的函数包构建功能。实际功能的实现是通过后面的逻辑代码来完成的,具体如何实现,接着往下看。
Operator 是怎么使用的
一个 Operator 的安装和使用有以下几个步骤:
- 创建一个 ServiceAccount,创建一个 Role,创建一个 RoleBinding 将 sa 和 Role 绑定在一起
- 通过 apply 一个 CRD 的 yaml 文件部署 CRD
- 通过apply一个CR的yaml文件安装一个 CR
- 安装 Controller。通过 apply 一个 Deployment.yaml,这个 Deployment 中定义好了 Controller 的镜像,最终 Controller 会以 Pod 的形式在 K8s 集群中运行
- 修改 CR 的内容并重新 apply
- Controller 监控到 CR 的变化,修改集群状态(即「调谐」)
针对这几个步骤,我下面有个Mysql的Operator的小例子,这里先简单介绍一下例子的流程:apply一个CRD的yaml创建Mysql的CRD、apply一个CR的yaml创建对应的CR、apply一个yaml创建一个Controller(Controller的代码是自己实现的)、修改CR的yaml中的replicas把mysql容器的数量从2变成3再重新apply,这个时候Controller监测到CR变化,开始调谐,把Mysql的pod数量从2变为3
如何自己创建一个 Operator
使用 kubebuilder
kubebuilder 是一个快速实现 kubernetes Operator 的框架,通过 kubebuilder 我们能够快速定义 CRD 资源和实现 Controller 逻辑
kubebuilder 自带两个工具:controller-runtime与controller-tools,我们可以通过controller-tools来生成大部分的代码,从而我们可以将我们的主要精力放置在 CRD 的定义和编写 controller 的 reconcile 逻辑上。reconcile 翻译成汉语是「调谐」,意思是指将资源的 status 通过一系列的操作调整至与定义的 spec 一致。
下图是一个 Controller 的调谐图示意:
这里涉及到list-watch,关于list-watch可以看看这篇文章
这里简单介绍一下一次「调谐」的逻辑:
- reflector 通过ListAndWatch方法去监听指定的 Object
- reflector 将所监听到的 event,包括对 object 的Add,Update,Delete的操作 Push 到 DeltaFIFO 这个 Queue 中
- Informer 首先会解析 event 中的 action 和 object
- Informer 将解析的 object 更新到 local store,也就是本地 cache 中的数据更新
- 然后 Informer 会执行 Controller 在初始化 Infromer 时注册的 ResourceEventHandler
- ResourceEventHandler 中注册的 callback 会将对应变化的 object 的 key 存入其初始化的一个 workQueue
- 最终 controller 会循环进行 reconcile,就是从 workQueue 不停地 pop key,然后去 local store 中取到对应的 object,然后进行处理,最终多数情况会再通过 client 去更新这个 object
如果不理解调谐的过程也不要紧,通俗一点来说就是修改了 yaml 文件的内容再重新 apply(比如把 replicas 的值从 2 改成了 3),controller 就会开始调谐,新增一个 pod(把 pod 数从 2 变成 3)
最佳实践
现在我们演示使用 kubebuilder 来创建 mysql 的 operator,期望这个 mysql-operator 可以实现 deployment 的创建和更新,以及 mysql 实例数的扩缩容。
5.1 环境准备
- 有一个 K8s 集群
- 安装 kubectl 并且可以连接到 K8s 集群上
- 安装 kubebuilder:brew install kubebuilder(windows自行百度一下)
5.2 初始化项目
kubebuilder init --domain tutorial.kubebuilder.io
kubebuilder create api --group batch --version v1 --kind Mysql # 接下来连续点击两次y
5.3 编写 CRD 资源
编写 CRD 资源的定义主要是在mysql_types.go文件中,编写 controller 逻辑主要是在mysql_controller.go文件中
我们先要编写一个 CR 的定义,写个 yaml 来描述我们的期望
定义一下 spec,MysqlSpec 这个结构体中的字段需要跟上面 batch_v1_mysql.yaml 文件中的 spec 对应(可以多不可以少)
定义一下 status
编写 Controller 的逻辑
kubebuilder 为我们在mysql_controller.go文件中预先生成了两个函数,分别是 Reconcile()和 SetupWithManager()
- 先编写 Reconcile 函数
我在代码里面把同步逻辑抽出来写成了 syncMysql,这个函数实现了:
- 查询 deployment
- 如果 deployment 不存在,则根据 mysql 的 spec 创建 deployment
- 如果 deployment 存在,则更新其 spec 使其与 mysql 的 spec 一致
- 更新 mysql 的 status
(syncMysql函数内部又调用了其他函数,代码太长这儿就不贴图了,想看的可以:https://github.com/helin0815/learncrds/tree/main/mysqsl-crd)
-
修改一些 SetUpWithManager 函数
这儿如果只是测试验证的话其实不修改也行。修改后的代码使用了 WithOptions 方法来设置控制器的选项,其中 MaxConcurrentReconciles: 3 表示控制器的最大并发调谐(reconciliation)数量为 3
-
执行make install 安装 CRD。每次修改mysql_types.go以后都要重新执行一下make install
-
执行make run运行 Controller。每次修改mysql_controller.go以后都要重新执行make run
-
通过 apply -f yaml 的方式部署 CR 到集群里面
我以前已经执行过了,所以下面执行的时候报 unchanged
-
查看 CRD 是否安装成功
-
修改 CR 的 yaml 中的内容,把 replicas 改成 2 再 apply 一下
-
现在再查看 pod 数量,就已经变成两个了
如果这里发现你的 pod 数量没变化,大概率是你忘记执行 make run 了,导致 controller 没启动,调谐没进行
- 部署到生产。到这里就已经体验了 kubebuilder 的基本功能,不过实际生产环境中 controller 一般都会运行在 kubernetes 环境内,像我这种运行在自己 macbook 的方式就不行了,现在尝试将其做成 docker 镜像.
这里有个要求,需要有个 kubernetes 可以访问的镜像仓库,例如局域网内的 Harbor,或者公共的 hub.docker.com
执行下面命令就可以将 controller 构建成镜像上传到自己的镜像仓库,以后就可以通过 image 来安装了
make docker-build docker-push IMG=test.example.com/mysql:v0.1
这就是一个K8s的Operator的简单介绍,如果有不懂的可以评论区问我(我写这些文章的大部分原因是为了加深我自己的印象,但是别人有遇到问题的话问我我也会尽力解答)