前言
该文章在之前的时候写的,operator sdk 相对来说与 kubebuilder 并没有太大区别,operator 开发大致流程也不会有太大变化。
后续计划使用新版本 kubebuilder 写一个实际场景中的小案例,并计划介绍在实际使用中对 CR 的调用常见方法。
准备
案例简介
案例来自于 《Programming-Kubernetes》文章中的 cloud native at ,用于实现一个云原生的 at 命令,
效果: 在yaml配置中 schedule
时间到达后,创建 busybox 镜像的 Pod,执行yaml配置中的 command
具体实现效果如下,通过以下格式的 yaml 进行定义
$ cat programming-kubernetes_v1alpha1_at.yaml
apiVersion programming-kubernetes.viper.run/v1alpha1 Kind At metadata name at-demo spec command echo QAQ schedule 2016-01-30T21 04 00Z
通过如下命令进行发布以及查看
$ kubectl apply -f programming-kubernetes_v1alpha1_at.yaml $ kubectl get ats
环境介绍
组件 | 版本 |
开发环境 | Linux_x86_64 |
kubernetes | v1.16.6 |
kubectl | v1.16.6 |
go | go1.14.4 linux/amd64 |
operator-sdk | v1.2.0 |
IDE | GoLand |
初始化
Go环境依赖
需要提前开启 go mod,配置 goproxy
export GO111MODULE=on export GOPROXY=https://mirrors.aliyun.com/goproxy/
项目初始化
初始化项目,新版本的 operator-sdk 使用的 --domain进行指定域名,比如本次项目我们期望的是 programming-kubernetes.viper.run, 此处domain指定好 viper.run 之后,api创建的时候只需要指定 group 为 programming-kubernetes 即可
$ mkdir /workspace/programming-kubernetes $ operator-sdk init --domain=viper.run --repo=github.com/AbyssViper/programming-kubernetes
API创建
创建 API ,各个字段的含义 apiVersion: group.domain/version
, kind 对应 yaml 配置中的 Kind, 这里我们设置 controller 为 true,同时按照 group 与 version 定义创建 controller。
$ operator-sdk create api --group programming-kubernetes --version v1alpha1 --kind At --resource=true --controller=true
运行测试
接下来进行一下发布测试,会创建对应的 CRD
$ make install $ kubectl get crds | grep ats.programming-kubernetes.viper.run
将自带的 sample yaml 发布至 Kubernetes,并且通过 kubectl get 查看发布情况
$ kubectl get ats
通过如下命令,对于已经发布的 crd 进行清理
make uninstall
编写业务逻辑
项目结构
一般情况下,对于业务逻辑,我们主要关注的是 at_types.go
与 at_controller.go
, yaml文件中对自定义资源使用的字段定义,是在 at_types.go
中定义的,相关的业务逻辑是需要在 at_controller.go
中进行编写的。
定义结构体
在 at_types.go
中进行 yaml 的映射结构定义
对于 spec 结构体,我们需要 Schedule
Command
两个字段声明
type AtSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // Schedule need UTC time format. Example: 2006-05-14T09:23:00Z Schedule string `json:"schedule,omitempty"` // Linux command will execute when be scheduled. Command string `json:"command,omitempty"` }
而对于 status 结构体,我们需要一个 Phase
字段来存储当前 At
实例所处的阶段
type AtStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // Phase set instance's status. Phase string `json:"phase,omitempty"` }
既然有了 status 对应的状态,我们需要对状态常量进行个标识,所以 at_types.go
中还需要声明如下常量,可以看到对于我们的 At
实例有 PENDING
, RUNNING
, DONE
三个状态
const ( PhasePending = "PENDING" PhaseRunning = "RUNNING" PhaseDone = "DONE" )
相关的类型结构体映射已经定义好了,接下来就是实现相关的逻辑
实现逻辑
具体的实现逻辑在控制循环中进行控制,在controllers.at_controller.go 中的 Reconcile 函数中进行实现
// +kubebuilder:rbac:groups=programming-kubernetes.viper.run,resources=ats,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=programming-kubernetes.viper.run,resources=ats/status,verbs=get;update;patch func (r *AtReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { _ = context.Background() _ = r.Log.WithValues("at", req.NamespacedName) // your logic here return ctrl.Result{}, nil }
逻辑结构
对于实现一个定时的at命令,基本实现逻辑分为以下几步:
- 控制循环中判定当前 At 实例的状态;如果实例状态为 空,代表没有发布对应的实例,重新放入队列进行循环
- 如果拿到实例,对实例
Phase
进行判断,如果Phase
为空,需要初始化添加PENDING
状态
- 根据实例的
Phase
执行不同的逻辑(具体见后续步骤)
- 不论
Phase
如何,最后把对 At 实例的在内存中的修改,提交至Kubernetes
具体逻辑架构如下:
-
特别注意 :需要注意的是如下
rbac
的控制,逻辑中涉及到对Pod
的操作,是需要利用kubebuilder
注解,添加对Pod
的操作权限,方便起见,我们如下添加对Pod
的所有操作权限
- 相关内置资源的
groups
以及resources
, 可以通过kubectl api-resources
查看
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=programming-kubernetes.viper.run,resources=ats,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=programming-kubernetes.viper.run,resources=ats/status,verbs=get;update;patch func (r *AtReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() reqLogger := r.Log.WithValues("at", req.NamespacedName) reqLogger.Info("----- Reconciling At -----") // Create At instance atInstance := &cnatv1alpha1.At{} // Try to get cloud native at instance. err := r.Get(ctx, req.NamespacedName, atInstance) if err != nil { // Request object not found. if errors.IsNotFound(err) { return ctrl.Result{}, nil } // Other error. return ctrl.Result{}, err } // If instance's phase not set (string type default ""), set PENDING default. if atInstance.Status.Phase == "" { atInstance.Status.Phase = cnatv1alpha1.PhasePending } // Switch instance phase, switch logic. switch atInstance.Status.Phase { case cnatv1alpha1.PhasePending: // Pending phase logic. case cnatv1alpha1.PhaseRunning: // Running phase logic. case cnatv1alpha1.PhaseDone: // Done phase logic. default: reqLogger.Info("Unknown instance status.") return ctrl.Result{}, nil } // Update this time reconcile status to the respective phase. if err := r.Status().Update(ctx, atInstance); err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil }
PENDDING
对于上述结构中的 case cnatv1alpha1.PhasePending
, 处理 PENDDING
阶段的逻辑:
- 从实例 Spec 中,判定 yaml 中配置的
schedule
字段 与 当前时间的比较,判定是否已经达到了需要执行command
的时机
- 如果未到达时间,以时间差值大小放入延迟队列
- 如果已经到达了执行时间,更改当前状态为
RUNNING
- 通过逻辑结构中的
r.Status().Update(ctx, atInstance)
将更改信息提交,并再次放入到队列中,等待下次循环
reqLogger.Info("Phase:", "status", cnatv1alpha1.PhasePending) // Violate field from config yaml. targetSchedule := atInstance.Spec.Schedule // Violate schedule. reqLogger.Info("Violate schedule format.", "schedule: ", targetSchedule) timeNow := time.Now().UTC() local, _ := time.LoadLocation("Asia/Shanghai") layout := "2006-01-02T15:04:05Z" s, err := time.ParseInLocation(layout, targetSchedule, local) if err != nil { reqLogger.Error(err, "Phase schedule error.") // Requeue. return ctrl.Result{}, err } diffTime := s.Sub(timeNow) reqLogger.Info("Schedule parse diff time end.", "result", diffTime) // Not this time. if diffTime > 0 { // Not this time, requeue after time diff. return ctrl.Result{RequeueAfter: diffTime}, nil } // Time now, will execute command. reqLogger.Info("Ready to phase RUNNING.", "command", atInstance.Spec.Command) // Change status, next requeue will execute case PhaseRUNNING. atInstance.Status.Phase = cnatv1alpha1.PhaseRunning
RUNNING
对于上述结构中的 case cnatv1alpha1.PhaseRunning
, 处理 RUNNING
阶段的逻辑:
- 创建以
busybox
镜像创建Pod
实例,并且打好标签 (PS: 此处可以理解为用代码级别去对 Pod 的 yaml 进行实例化)
- 通过
controllerutil.SetControllerReference
设置执行command
的Pod
归属于 At 实例,方便删除 At 实例的时候删除下属的Pod
- 创建信息定义好以后,根据定义信息查询当前
Pod
是否存在(此处是为了Pod
在执行过程中再次循环到RUNNING
逻辑),如果Pod
查询到并且处于successed
或者failed
的状态,则代表已经完成,更改 At 实例状态为DONE
- 如果上述
Pod
没有完成,继续放入队列,进行循环判定
reqLogger.Info("Phase:", "status", cnatv1alpha1.PhaseRunning) labels := map[string]string{ "app": atInstance.Name, } exePod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: atInstance.Name + "-pod", Namespace: atInstance.Namespace, Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "busybox", Image: "busybox", Command: strings.Split(atInstance.Spec.Command, " "), }, }, RestartPolicy: corev1.RestartPolicyOnFailure, }, } if err := controllerutil.SetControllerReference(atInstance, exePod, r.Scheme); err != nil { // requeue with error. return ctrl.Result{}, err } getPod := &corev1.Pod{} // Try to get exePod now. if err := r.Get(ctx, types.NamespacedName{Name: exePod.Name, Namespace: exePod.Namespace}, getPod); err != nil { if errors.IsNotFound(err) { if err := r.Create(ctx, exePod); err != nil { return ctrl.Result{}, err } reqLogger.Info("Pod create success.", "name", exePod.Name) } else { return ctrl.Result{}, err } } else if getPod.Status.Phase == corev1.PodFailed || getPod.Status.Phase == corev1.PodSucceeded { reqLogger.Info("container terminal", "reason", getPod.Status.Reason, "message", getPod.Status.Message) atInstance.Status.Phase = cnatv1alpha1.PhaseDone } else { return ctrl.Result{}, nil }
Done
对于上述结构中的 case cnatv1alpha1.PhaseDone
, 处理 Done
阶段的逻辑:
- 如果实例状态为 Done 则代表实例已经完成,直接返回
reqLogger.Info("Phase:", "status", cnatv1alpha1.PhaseDone) return ctrl.Result{}, nil
发布流程
开发过程调试发布
对于上述已经编写完成的 Operator
, 通过如下命令进行安装,并且查看对应的 crd
$ make install $ kubectl get crds | grep viper.run
发布完成后,可以通过如下命令进行运行,可以看到此时对应的控制器已经开始监听
$ make run
此时我们编辑对应的 yaml 文件进行发布, 默认在 config/samples/programming-kubernetes_v1alpha1_at.yaml
, 按照我们结构体的定义,我们编辑如下配置文件:
apiVersion programming-kubernetes.viper.run/v1alpha1 kind At metadata name at-sample spec command echo QAQ schedule 2020-11-29T16 36 25Z
编写完上述的配置文件后,我们将其发布至 Kubernetes
$ kubectl apply -f config/samples/programming-kubernetes_v1alpha1_at.yaml
此时我们运行的程序,开始进行 Reconciling
,可以看到
- 第一次
Reconciling At
, 验证了schedule
字段的时间,距离目标时间还有 51s,对应放入延迟队列
- 第二次
Reconciling At
此时已经到了执行的时间,经过验证以后,更改当前的状态从PENDING
至RUNNING
,并返回等待下次循环
- 第三次
Reconciling At
,此时实例已经为RUNNING
状态,创建对应的 Pod,并且执行响应的命令,此时执行完成,更改实例状态为DONE
对应的我们对于 Pod
进行监听,可以看到整个创建的流程以及日志记录
发布上线
对于上述开发过程中的调试,其实就是 Operator-SDK
利用 Informer
, Caching
, Workqueue
等机制(具体内容见另外一篇博客)在本地进行同步 Kubernetes
对象信息来进行调试的,上述的程序其实可以打包后部署在任何与 Kubernetes
可以通信的地方,但是一般情况下我们会封装镜像,作为 Pod
进行发布,这样不论效率还是实际使用上都是最佳的。
构建镜像
Operator-SDK
同样提供了方便的部署流程,首先我们需要构建镜像:
$ make docker-build IMG=abyssviper/operator-cnat:v0.0.1
通过上述命令,会进行对应的单元测试等,也可以使用根目录下的 Dockerfile
进行镜像构建,但是不推荐这么做
可能存在的问题:
- make配置中存在被墙的地址
- 由于
Makefile
中,其中如下setup-envest.sh
是直接使用的raw. githubusercontent.com
的,该地址是被墙掉的,我们可以更改如下地址到 gitee 的仓库,或者下载下来更改为本地的地址
- 此处我采用的是
gitee
同步github
项目,然后按照Makefile
中的对应版本,替换掉链接,当然你也可以使用该仓库-》https://gitee.com/AbyssViper/controller-runtime
- 由于
- 构建镜像过程中,
go
依赖包拉取失败 以及 镜像拉取失败
- 默认
Dockerfile
中基础镜像golang
采用的是 golang.org 的 goproxy,此处我们添加镜像环境变量即可
- 对于
Dockerfile
中第二个基础镜像是从gcr.io
拉取的,该地址被墙,之前还有中科大、微软的镜像,文章编写的时候两个地址都不可用了。所以使用了阿里云免费的海外构建绑定github仓库的方式,这个可以自寻百度
- 默认
构建镜像完成后,可以在本地查看到对应的镜像 —》 abyssviper/operator-cnat:v0.0.1
当然没必要完全使用默认的 Dockerfile
,自己按照规则重新编写 Dockerfile
也可以,这里就不赘述了,大家自行编写
推送镜像
镜像构建完成后,需要推送对应的镜像至镜像仓库,这里我直接推送到了 docker hub
,正常都会推送到 Harbor
等私服
$ make docker-push IMG=abyssviper/operator-cnat:v0.0.1
发布至Kubernetes
$ make deploy IMG=abyssviper/operator-cnat:v0.0.1
可能存在的问题:
- 镜像拉取失败
- 对于 deploy 操作中,会拉取
gcr.io/kubebuilder/kube-rbac-proxy
镜像,目前对于gcr.io
相关的镜像加速比较难找
- 可以通过利用阿里云镜像仓库海外构建或者其他方式,获取镜像
- 或者更改对应的镜像地址
- 对于 deploy 操作中,会拉取
$ vim ./config/default/manager_auth_proxy_patch.yaml
发布成功后,会创建该 API 的命名空间,比如本项目会创建命名空间 programming-kubernetes-system
, 可以通过查看对应的 deployment 以及 pod
$ kubectl get po -n programming-kubernetes-system $ kubectl get deploy -n programming-kubernetes-system
相关的日志我们可以查看 Pod
,Pod
中是存在两个 container
, 分别是 kube-rbac-proxy
, manager
, 相关的运行日志在 manager
中查看
$ kubectl logs -f -n programming-kubernetes-system programming-kubernetes-controller-manager-7888669b85-f6b69 manager
此时我们准备对应的 At
yaml配置发布至 Kubernetes
,可以看到与我们本地开发环境一样,对应的日志输出,Pod
创建并执行任务
这样一个较为完整的利用 Operator-SDK
开发 Operator
的案例就结束了