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下的安装过程:
1 2 3 4 5 6 |
Set the release version variable $ RELEASE_VERSION=v0.10.0 # macOS $ curl -OJL https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk-${RELEASE_VERSION}-x86_64-apple-darwin # macOS $ chmod +x operator-sdk-${RELEASE_VERSION}-x86_64-apple-darwin && sudo mkdir -p /usr/local/bin/ && sudo cp operator-sdk-${RELEASE_VERSION}-x86_64-apple-darwin /usr/local/bin/operator-sdk && rm operator-sdk-${RELEASE_VERSION}-x86_64-apple-darwin |
就是下载然后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项目:
1 |
operator-sdk new demo-operator --repo=github.com/owenliang/demo-operator |
然后项目代码就有了:
1 2 3 4 5 6 7 8 9 10 |
liangdongs-MacBook-Pro:github liangdong$ ll demo-operator/ total 128 drwxr-x--- 4 liangdong staff 128 8 13 17:11 build drwxr-x--- 3 liangdong staff 96 8 13 17:11 cmd drwxr-x--- 6 liangdong staff 192 8 13 17:11 deploy -rw------- 1 liangdong staff 1513 8 13 17:11 go.mod -rw------- 1 liangdong staff 53823 8 13 17:11 go.sum drwxr-x--- 4 liangdong staff 128 8 13 17:11 pkg -rw-r--r-- 1 liangdong staff 95 8 13 17:11 tools.go drwxr-x--- 3 liangdong staff 96 8 13 17:11 version |
项目结构官方也有说明: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项目,执行:
1 |
operator-sdk add api --api-version=yuerblog.cc/v1 --kind=Demo |
意思就是创建一个CRD资源类型,叫做Demo,其apiversion是yuerblog.cc/v1。
这就对应于Deployment的这俩信息,只不过现在自定义一种新的而已:
1 2 |
apiVersion: apps/v1 kind: Deployment |
上述命令生成了pkg/apis目录,定义CRD的schema在这个文件里:pkg/apis/yuerblog/v1/demo_types.go,我们要做的是定义status和spec两个字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // DemoSpec defines the desired state of Demo // +k8s:openapi-gen=true type DemoSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html } // DemoStatus defines the observed state of Demo // +k8s:openapi-gen=true type DemoStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Demo is the Schema for the demos API // +k8s:openapi-gen=true // +kubebuilder:subresource:status type Demo struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec DemoSpec `json:"spec,omitempty"` Status DemoStatus `json:"status,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // DemoList contains a list of Demo type DemoList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Demo `json:"items"` } func init() { SchemeBuilder.Register(&Demo{}, &DemoList{}) } |
所有资源类型的YAML都遵循这样的结构,这里定义了2个schema,一个是单个Demo对象,一个是Demo列表,均注册到了框架。
注释中也提示了,如果修改了schema,要么执行generate k8s命令重新生成代码,脚手架会给我们生成必要的关于操作上述struct的代码,这个照做就行。
另外,更新完schema结构体定义后,可以执行命令来生成CRD的注册YAML,这样后续创建对象的时候,K8S可以帮助我们检查提交的YAML是否符合schema:
1 |
operator-sdk generate openapi |
这里举个栗子,比如我添加spec里有一个count字段:
1 2 3 4 5 6 |
type DemoSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html Count int `json:"count"` } |
那么generate openapi生成的CRD注册YAML就会变成下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: demos.yuerblog.cc spec: group: yuerblog.cc names: kind: Demo listKind: DemoList plural: demos singular: demo scope: Namespaced subresources: status: {} validation: openAPIV3Schema: properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' type: string metadata: type: object spec: properties: count: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html' format: int64 type: integer required: - count type: object status: type: object version: v1 versions: - name: v1 served: true storage: true |
就这样一回事。
生成controller
执行下面的命令:
1 |
operator-sdk add controller --api-version=yuerblog.cc/v1 --kind=Demo |
会生成CRD对应的controller框架代码到pkg/controller,但是调度逻辑需要我们自己填充。
监听资源
打开代码:pkg/controller/demo/demo_controller.go。
首先要声明我们监听什么类型的资源,我们看看默认它监听了啥:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller c, err := controller.New("demo-controller", mgr, controller.Options{Reconciler: r}) if err != nil { return err } // Watch for changes to primary resource Demo err = c.Watch(&source.Kind{Type: &yuerblogv1.Demo{}}, &handler.EnqueueRequestForObject{}) if err != nil { return err } // TODO(user): Modify this to be the types you create that are owned by the primary resource // Watch for changes to secondary resource Pods and requeue the owner Demo err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{ IsController: true, OwnerType: &yuerblogv1.Demo{}, }) if err != nil { return err } |
先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,其实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// Reconcile reads that state of the cluster for a Demo object and makes changes based on the state read // and what is in the Demo.Spec // TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates // a Pod as an example // Note: // The Controller will requeue the Request to be processed again if the returned error is non-nil or // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. func (r *ReconcileDemo) Reconcile(request reconcile.Request) (reconcile.Result, error) { reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) reqLogger.Info("Reconciling Demo") // Fetch the Demo instance instance := &yuerblogv1.Demo{} err := r.client.Get(context.TODO(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue return reconcile.Result{}, nil } // Error reading the object - requeue the request. return reconcile.Result{}, err } // Define a new Pod object pod := newPodForCR(instance) // Set Demo instance as the owner and controller if err := controllerutil.SetControllerReference(instance, pod, r.scheme); err != nil { return reconcile.Result{}, err } // Check if this Pod already exists found := &corev1.Pod{} err = r.client.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found) if err != nil && errors.IsNotFound(err) { reqLogger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name) err = r.client.Create(context.TODO(), pod) if err != nil { return reconcile.Result{}, err } // Pod created successfully - don't requeue return reconcile.Result{}, nil } else if err != nil { return reconcile.Result{}, err } // Pod already exists - don't requeue reqLogger.Info("Skip reconcile: Pod already exists", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name) return reconcile.Result{}, nil } |
request就是发生变动的资源信息,主要就是namespace和name。
因为我们之前监听并加入队列的一定是Demo资源,所以我们直接利用k8s客户端Get获取发生变动的Demo对象。
没找到Demo对象说明它被删除了,这次发来的事件是删除事件,仅此而已。
如果获取对象异常,可以返回一个非空的err,这样Controller框架层会把这个事件重新加入队列,后续会再次回调以便重试,可见K8S的设计理念多么简洁。
接下来就是要给Demo创建POD了,所以接下来先创建了POD结构体,然后设置了POD的owner为这个Demo对象。
以Deployment为例,给大家看一下什么是owner信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
apiVersion: v1 kind: Pod metadata: creationTimestamp: "2019-08-13T08:13:51Z" generateName: zhongce-service-cron-3-744b6f4896- labels: pod-template-hash: 744b6f4896 name: zhongce-service-cron-3-744b6f4896-q9hck namespace: default ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: true controller: true kind: ReplicaSet name: zhongce-service-cron-3-744b6f4896 uid: 620a594d-bd93-11e9-a108-525400d828ab |
在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。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
娘炮你微信挂了?
饭端给你还不行,还得喂着吃,喂着吃还不行,还得咀嚼后喂给你,这就是CDR !!!!科技发展是好,但不要不断的抽象,增加累赘!!!
不行就再加一层
大大,你后续工作粘的链接挂了,可以更新一下吗,萌新球球
真光速理解!赞