Operator SDK V1.2 简单案例快速入手

前言

该文章在之前的时候写的,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


Operator SDK V1.2 简单案例快速入手

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


Operator SDK V1.2 简单案例快速入手

运行测试

接下来进行一下发布测试,会创建对应的 CRD

$ make install
$ kubectl get crds | grep ats.programming-kubernetes.viper.run


Operator SDK V1.2 简单案例快速入手

将自带的 sample yaml 发布至 Kubernetes,并且通过 kubectl get 查看发布情况

$ kubectl get ats


Operator SDK V1.2 简单案例快速入手

通过如下命令,对于已经发布的 crd 进行清理

make uninstall


Operator SDK V1.2 简单案例快速入手

编写业务逻辑

项目结构

一般情况下,对于业务逻辑,我们主要关注的是 at_types.goat_controller.go, yaml文件中对自定义资源使用的字段定义,是在 at_types.go 中定义的,相关的业务逻辑是需要在 at_controller.go中进行编写的。

Operator SDK V1.2 简单案例快速入手

定义结构体

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 设置执行 commandPod 归属于 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


Operator SDK V1.2 简单案例快速入手

发布完成后,可以通过如下命令进行运行,可以看到此时对应的控制器已经开始监听

$ make run


Operator SDK V1.2 简单案例快速入手

此时我们编辑对应的 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 此时已经到了执行的时间,经过验证以后,更改当前的状态从 PENDINGRUNNING,并返回等待下次循环
  • 第三次 Reconciling At ,此时实例已经为 RUNNING 状态,创建对应的 Pod,并且执行响应的命令,此时执行完成,更改实例状态为 DONE

Operator SDK V1.2 简单案例快速入手

对应的我们对于 Pod 进行监听,可以看到整个创建的流程以及日志记录

Operator SDK V1.2 简单案例快速入手

发布上线

对于上述开发过程中的调试,其实就是 Operator-SDK 利用 Informer , Caching , Workqueue 等机制(具体内容见另外一篇博客)在本地进行同步 Kubernetes 对象信息来进行调试的,上述的程序其实可以打包后部署在任何与 Kubernetes 可以通信的地方,但是一般情况下我们会封装镜像,作为 Pod 进行发布,这样不论效率还是实际使用上都是最佳的。

构建镜像

Operator-SDK 同样提供了方便的部署流程,首先我们需要构建镜像:

$ make docker-build IMG=abyssviper/operator-cnat:v0.0.1


通过上述命令,会进行对应的单元测试等,也可以使用根目录下的 Dockerfile 进行镜像构建,但是不推荐这么做

可能存在的问题:

  1. make配置中存在被墙的地址
    • 由于 Makefile 中,其中如下 setup-envest.sh 是直接使用的 raw. githubusercontent.com 的,该地址是被墙掉的,我们可以更改如下地址到 gitee 的仓库,或者下载下来更改为本地的地址
    • 此处我采用的是 gitee 同步 github项目,然后按照 Makefile 中的对应版本,替换掉链接,当然你也可以使用该仓库-》https://gitee.com/AbyssViper/controller-runtime

Operator SDK V1.2 简单案例快速入手

  1. 构建镜像过程中,go 依赖包拉取失败 以及 镜像拉取失败
    • 默认 Dockerfile 中基础镜像 golang 采用的是 golang.org 的 goproxy,此处我们添加镜像环境变量即可
    • 对于 Dockerfile 中第二个基础镜像是从 gcr.io 拉取的,该地址被墙,之前还有中科大、微软的镜像,文章编写的时候两个地址都不可用了。所以使用了阿里云免费的海外构建绑定github仓库的方式,这个可以自寻百度
  1. Operator SDK V1.2 简单案例快速入手

构建镜像完成后,可以在本地查看到对应的镜像 —》 abyssviper/operator-cnat:v0.0.1

Operator SDK V1.2 简单案例快速入手

当然没必要完全使用默认的 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


Operator SDK V1.2 简单案例快速入手

可能存在的问题:

  1. 镜像拉取失败
    • 对于 deploy 操作中,会拉取 gcr.io/kubebuilder/kube-rbac-proxy 镜像,目前对于 gcr.io 相关的镜像加速比较难找
    • 可以通过利用阿里云镜像仓库海外构建或者其他方式,获取镜像
    • 或者更改对应的镜像地址
$ vim ./config/default/manager_auth_proxy_patch.yaml


  1. Operator SDK V1.2 简单案例快速入手

发布成功后,会创建该 API 的命名空间,比如本项目会创建命名空间 programming-kubernetes-system , 可以通过查看对应的 deployment 以及 pod

$ kubectl get po -n programming-kubernetes-system
$ kubectl get deploy -n programming-kubernetes-system


Operator SDK V1.2 简单案例快速入手

相关的日志我们可以查看 PodPod 中是存在两个 container, 分别是 kube-rbac-proxy , manager, 相关的运行日志在 manager 中查看

$ kubectl logs -f -n programming-kubernetes-system programming-kubernetes-controller-manager-7888669b85-f6b69 manager


Operator SDK V1.2 简单案例快速入手

此时我们准备对应的 At yaml配置发布至 Kubernetes ,可以看到与我们本地开发环境一样,对应的日志输出,Pod 创建并执行任务

Operator SDK V1.2 简单案例快速入手

Operator SDK V1.2 简单案例快速入手

这样一个较为完整的利用 Operator-SDK 开发 Operator 的案例就结束了


上一篇:中国电信发布低时延光网络白皮书


下一篇:冬季实战营第三期:MySQL数据库进阶实战