K8S – 光速理解operator

这篇博客是我花了半天时间研究k8s operator的一些成果,分享给大家。

什么是operator

要理解operator先得理解Helm和CRD。

Helm(说实话我也没学过,学不动了)

大家应该听过helm,它是一个命令行工具,帮你同时编排多个K8S资源(比如:Deployment+Service+Ingress这样的经典组合),统一发布到K8S。

它有一个优势就是模板化,比如:所有的PHP应用除了image不同之外的其他部署结构都相同,那么就可以共用一套Helm模板,通过传参image参数来替换YAML细节即可。

Helm发布的底层原理就是调用K8S的apiserver,逐个YAML推送给K8S,和我们手动去做没多少差别,所以其弊端就是缺乏对资源的全生命期监控

具体一点就是,如果我们Helm发布了一个POD,然后我们在K8S中误删了POD,那么Helm是不会替我们重建POD的,这是Helm的主要问题所在。

CRD

全拼CustomResourceDefinitions,也就是自定义K8S资源类型。

内置的资源类型包含POD、Deployment、Configmap等等,我们可以通过CRD机制注册新的资源类型到K8S中。

实际底层就是通过apiserver接口,在etcd中注册一种新的资源类型,此后就可以创建对应的资源对象了,就像我们为不同应用创建不同的Deployment对象一样。

仅仅注册资源与创建资源对象通常是没有价值的,重要的是需要实现CRD背后的功能。比如,Deployment的功能是生成一定数量的POD并监控它们的状态。

所以CRD需要配套实现Controller,相信大家也听过Deployment Controller这些内置的Controller。Controller是需要CRD配套开发的程序,它通过apiserver监听相应类型的资源对象事件,例如:创建、删除、更新等等,然后做出相应的动作,比如Deployment创建/更新时需要对POD进行更新操作等。

operator

通过了解helm和CRD,我们就知道helm没法管理资源的完整生命期,它就是推送YAML就拍拍屁股走人了;而只有CRD才能持续的监听K8S资源对象的变化事件,进行全生命期的监控响应,高可靠的完成部署交付。

operator是开发CRD的一个脚手架项目,目的是帮我们实现CRD,那么一定就有一个疑问,为什么要实现CRD呢?我举2个例子:

  • 假设公司的PHP项目都是deployment+service+ingress的统一玩法,那么我们就可以定义一个CRD叫做php-deployment,创建一个这样的资源对象,就可以全自动的拉起deployment、service、ingress,并且全程监控它们的生命期,这还不够强大嘛。
  • 比如我们想给研发提供一键创建mysql的服务,我们就可以实现一个CRD,它可以帮我们创建stateful的mysql实例,挂载PVC持久卷,并且发一封邮件通知我们mysql POD的启停事件,这就很强大了嘛。

我们知道CRD要做的就是监听相应类型的K8S资源变化,并做出响应动作。实现这一块的K8S相关代码和概念比较庞杂吧,所以operator对相关的开发模型进行了抽象封装,提供脚手架生成代码,这样我们的精神负担就小多了。

operator+helm

operator项目还有一个玩法,其实是实现了一个helm in k8s的效果。

简单的说,operator实现了一套针对Helm的通用CRD controller,我们可以直接把提前写好的helm编排配置随着CRD controller一起打包成docker镜像,然后把它部署到K8S里运行。

此后,我们只要创建CRD对应的资源对象,在YAML的spec里填写helm的模板参数,那么CRD controller就会把值填充到helm模板里,然后按照helm的方式发布到K8S集群里。

所以如果原先是通过helm发布若干内置的K8S资源的话,完全可以0成本的迁移到operator框架,借助operator提供的helm通用CRD controller,可以实现带生命期管理的helm效果。

因为原理比较简单,具体的大家参考这篇博客即可:https://bestsamina.github.io/posts/2019-02-04-first-operator-sdk-helm/

基于go实现的operator

刚才提到的operator+helm属于0开发的一种operator方式,只适用于简单资源的helm编排。

如果我们要在CRD controller里实现一些复杂的调度逻辑,比如:mysql POD挂了发一封邮件之类的事情,那么就得用Go去实现CRD controller的核心调度逻辑了。

之前我没有用operator脚手架写过一个简单的CRD controller,因此这次体验operator精神负担会小一些。再者,operator官方上手文档还是很友好的,执行下来非常顺利,所以下面就按照官方手册的方式顺一下整个过程。

官方教程:https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md

安装operator-sdk

  • 先安装golang 12+,如果你是golang 11记得导出一下gomod那个环境变量。
  • 配置goproxy:https://goproxy.io,否则代码下载很慢。

官方文档:https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md

operator-sdk是脚手架的命令行工具,应该选择下载二进制那种方式,我在MAC下的安装过程:

就是下载然后mv到/usr标准路径下,此后就可以直接用operator-sdk命令了。

生成CRD项目

既然要自定义CRD,那么operator-sdk会帮我们做下面几个事情:

  • 生成CRD YAML:因为自定义资源类型,所以这个YAML是用来向apiserver注册的,稍后会看到。
  • 生成CRD Schema的基本结构:我们都知道Deployment YAML需要配置哪些资源,因此自定义的资源类型也需要定义所需字段。operator-sdk会帮我们生成基本的结构体定义(Go代码),具体其他字段需要我们自己去Go里定义。
  • 生成CRD Controller的基本结构:包含main.go的一个完整程序骨架,我们后续要填充的就是一个事件处理函数,每当CR对象(该类型的资源对象)有变化的时候就会回调,我们就可以写逻辑做出响应。
  • 生成Controller的deployment YAML:最终CRD Controller要运行在K8S内监听资源变化并做出响应,所以它自身需要编译好之后打包成docker镜像,通过deployment部署到K8S中。

假设我的项目叫做demo-operator,并且托管在github.com/owenliang/demo-operator上,那么通过如下命令可以生成demo-operator项目:

然后项目代码就有了:

项目结构官方也有说明:https://github.com/operator-framework/operator-sdk/blob/master/doc/project_layout.md

  • build:dockerfile,打包controller用的。
  • cmd:controller的程序入口main.go
  • deploy:把controller跑到k8s里的所需yaml,比如:service account、deployment。
  • go.mod:包管理基于gomod,当前包地址是opeartor-sdk new的时候–repo指定的。
  • pkg:controller的核心代码,包括schema的定义与调度的核心逻辑,当前我们仅仅生成了程序骨架,还没生成具体CRD的代码。

生成schema

进入demo-operator项目,执行:

意思就是创建一个CRD资源类型,叫做Demo,其apiversion是yuerblog.cc/v1。

这就对应于Deployment的这俩信息,只不过现在自定义一种新的而已:

上述命令生成了pkg/apis目录,定义CRD的schema在这个文件里:pkg/apis/yuerblog/v1/demo_types.go,我们要做的是定义status和spec两个字段:

所有资源类型的YAML都遵循这样的结构,这里定义了2个schema,一个是单个Demo对象,一个是Demo列表,均注册到了框架。

注释中也提示了,如果修改了schema,要么执行generate k8s命令重新生成代码,脚手架会给我们生成必要的关于操作上述struct的代码,这个照做就行。

另外,更新完schema结构体定义后,可以执行命令来生成CRD的注册YAML,这样后续创建对象的时候,K8S可以帮助我们检查提交的YAML是否符合schema:

这里举个栗子,比如我添加spec里有一个count字段:

那么generate openapi生成的CRD注册YAML就会变成下面这样:

就这样一回事。

生成controller

执行下面的命令:

会生成CRD对应的controller框架代码到pkg/controller,但是调度逻辑需要我们自己填充。

监听资源

打开代码:pkg/controller/demo/demo_controller.go。

首先要声明我们监听什么类型的资源,我们看看默认它监听了啥:

先Watch了一下我们注册的Demo类型资源,然后又Watch了一下内置的Pod资源。

监听Demo可以理解,因为是我们的CRD类型,得到Demo资源变化后会将其enqueue放入事件队列。

监听Pod其实与这个CRD想实现的功能有关,脚手架生成的默认代码实现了一个这样的CRD:创建一个busybox的POD,就像一个replicas=1的deployment一样的维护它。

接下来watch pod的时候可以看出代码有区别,它的第二个参数填了一个OwnerType表示应该将创建POD的owner对象enqueue,而不是将POD自身入队列。

其实这个CRD就是Demo资源创建了POD资源,所以Demo是POD的owner,这个owner关系实际会体现在POD的YAML中,这个后续会看到。

总的来说,监听的是Demo和POD资源,如果是POD资源变化了,那么会根据owner信息找到所属的Demo资源,最终enqueue队列的都是Demo,这属于K8S的设计哲学,会让调度逻辑变得很简单。

调度逻辑

核心调度函数就是Reconcile,其实现如下:

request就是发生变动的资源信息,主要就是namespace和name。

因为我们之前监听并加入队列的一定是Demo资源,所以我们直接利用k8s客户端Get获取发生变动的Demo对象。

没找到Demo对象说明它被删除了,这次发来的事件是删除事件,仅此而已。

如果获取对象异常,可以返回一个非空的err,这样Controller框架层会把这个事件重新加入队列,后续会再次回调以便重试,可见K8S的设计理念多么简洁。

接下来就是要给Demo创建POD了,所以接下来先创建了POD结构体,然后设置了POD的owner为这个Demo对象。

以Deployment为例,给大家看一下什么是owner信息:

在metadata中有一个ownerReferences字段,指向了其属于某个replicaset,我们的CRD是同样的道理。

接下来先Get了一下POD,如果不存在就创建,不再赘述。

后续工作

接下来就是把CRD的YAML注册到K8S,然后把写好的operator编译部署到K8S。

因为文章篇幅原因就不演示了,各位从该文档位置继续向后阅读即可:https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md#build-and-run-the-operator

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~