K8s operator从0到1实战

Operator基础知识

Kubernetes Operator是一种用于管理和扩展Kubernetes应用程序的模式和工具。它们是一种自定义的Kubernetes控制器,可以根据特定的应用程序需求和业务逻辑扩展Kubernetes功能。

Kubernetes Operator基于Kubernetes的控制器模式,通过自定义资源定义(CRD)来描述和管理应用程序的状态。它们利用Kubernetes的控制循环(control loop)机制,监视和响应与应用程序相关的事件,并采取适当的操作来维护所需的状态。
自定义资源与内置资源关系:
K8s operator从0到1实战_第1张图片

行业内使用现状

operator基本成为应用上云,计算框架等上云的标准方案
主流的开源operator,被统一收录在开源商城,涵盖大数据、数据库、机器学习、devops等领域。目前收录了300+款框架应用,用户在k8s集群可实现开箱即用。
K8s operator从0到1实战_第2张图片
redHat维护的一个operator商城:operatorhub

CRD基本概念

crd与controller一般是配套使用,在这里简单描述一下在实际运转中,他们的流程关系。
crd定义了自定义资源的结构和资源状态信息等,自定义的内置资源一般以yaml或者json结构形式被使用。一个crd定义完毕并成功注册到k8s后,会自动生成一个独有端口号的k8s api,这个api可以被kubectl工具以命令行的形式执行、或者以k8s client的方式被调用。最终用户在使用自定义cr(自定义资源实例对象)时,能享受到调用内置资源时的便捷。

controller依靠k8s提供的控制循环机制监视资源,调用对应资源的k8s api,依据资源的状态和期望状态之间的差异采取适当的操作,在这个循环机制中涉及创建、更新或删除其他 Kubernetes 资源。

K8s operator从0到1实战_第3张图片
K8s operator从0到1实战_第4张图片

云平台上的使用现状

典型应用案例:

1. 分布式训练training-operator

以pytorch ddp流程为例:

  1. 按自定义资源结构生成master和worker的pod、service配置和数量
  2. 将master和worker环境变量添加到对应pod
  3. worker ping master service
  4. 监听master和worker的运行状态
    K8s operator从0到1实战_第5张图片

2. 工作流argo-workflow operator

定义、监听上下游任务顺序相关元数据
监听解析为pod的运行实例
容器的输出同步到自定义数据库,并负责将上下游stage数据在容器内传递
K8s operator从0到1实战_第6张图片

3. 分布式计算框架Spark on k8s operator

从spark2.3版本开始支持on k8s
只支持指定资源量、启停一个Spark Application集群,并指派作业任务到这些Executor中执行。
K8s operator从0到1实战_第7张图片
park operator对标spark on yarn生命周期和流程管理

  • application事件监控、控制、管理;
  • 自定义executor配置;
  • 任务监控;
  • 日志相关;
  • Ui;

  • K8s operator从0到1实战_第8张图片

更多应用:

  • 快速服务seldon operator
  • tensorboard应用tensorboard operator
  • 算法开发web ide notebook operator
  • 云存储缓存加速工具fluid dataset runtime operator

Kubebuilder构建operator实践

在k8s集群部署一个服务应用,默认方式是需要同时创建Deployment和Service这两个默认资源对象。通过 Pod 的 label标签将service资源对象与deployment关联,最后通过 Ingress 或者 type=NodePort 类型的 Service 来暴露服务。
这一通流程下来比较繁琐,在创建多个服务应用时尤为突出。为了降低服务创建时过多的资源对象定义,这里以名为EasyService的CRD为例,从0开发一个简化版本的服务创建流程。

开发工具包

这里推荐使用脚手架工具kubebuilder
使用脚手架工具,能生成项目模板,开发人员只需要关注核心逻辑和方法即可
安装流程

mac安装流程

brew install kubebuilder

linux安装流程

在github下载最新最新:
https://github.com/kubernetes-sigs/kubebuilder/releases
我在这个例子中使用的go version 1.18.3,为了避免麻烦直接下载v3.5.0版本

# 重命名
$ mv kubebuilder_linux_amd64 kubebuilder

# 赋予可执行权限
$ chmod a+x kubebuilder

# 移动可执行文件到bin路径
$ mv kubebuilder /usr/local/bin

# 为 PATH 环境变量追加 kubebuilder 二进制路径
$ export PATH=$PATH:/usr/local/bin

使用流程

example

1.切入到项目文件夹
$ cd webapp-operator/

2.初始化go modulm
$ go mod init webapp-operator

3.初始化项目模板
$ kubebuilder init --domain kubebuilder.io

4.创建api
这里我们创建一个 group 为 app, version 为 v1, kind 为 EasyService 的 api:
$ kubebuilder create api --group app --version v1 --kind EasyService

核心逻辑编写和测试

按以上流程,自动生成项目文件夹,文件夹的结构如下图所示:

.
├── Dockerfile          # 用于构建控制器镜像的 Dockerfile
├── Makefile            # 用于控制器构建及部署的 Makefile
├── PROJECT             # 勇于生成组件的 kubebuilder 元数据
├── README.md
├── api                                 # API 模板代码所在目录
│   └── v1
│       ├── easyservice_types.go       # API 类型文件, 主要关注 Spec 与 Status 结构体
│       ├── groupversion_info.go        # 此文件包含了 Group Version 的一些元信息
│       └── zz_generated.deepcopy.go    # 自动生成的 runtime.Object 实现
├── bin
│   └── manager
├── config              # 采用 Kustomize YAML 定义的配置
│   ├── certmanager/    # 证书管理相关
│   ├── crd/            # CRD 相关, 当 make install 将 apply 此目录 yaml 
│   ├── default/        # 控制器相关, 当 make deploy 将 apply 此目录 yaml
│   ├── manager/
│   ├── prometheus/     # 监控相关
│   ├── rbac/           # RBAC 权限管理
│   ├── samples/        # CR 样例
│   └── webhook/        # webhook相关
├── controllers                     # 控制器逻辑所在目录
│   ├── easyservice_controller.go  # 控制器 reconcile 逻辑实现所在文件 
│   └── suite_test.go               # 测试文件
├── cover.out
├── go.mod              # Go Mod 配置文件,记录依赖信息
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go             # 程序入口

核心逻辑编写

使用kubebuilder脚手架工具修改自定义operator,只需要修改2两处核心逻辑:

  1. api/v1/xxx_types.go的结构定义
  2. controllers/xxx_controller.go的调协、状态监控、更新等方法…

eg. 新建CR(用户自定义对象的实例)创建指定副本deployment和service,并对外暴露nodeport端口
对象结构、状态定义需要的字段:

// crd结构定义
// EasyService is the Schema for the easyservices API
type EasyService struct {
        metav1.TypeMeta   `json:",inline"`
        metav1.ObjectMeta `json:"metadata,omitempty"`

        Spec   EasyServiceSpec   `json:"spec,omitempty"`
        Status EasyServiceStatus `json:"status,omitempty"`
}

// 创建cr结构定义
// EasyServiceSpec defines the desired state of EasyService
type EasyServiceSpec struct {
        // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
        // Important: Run "make" to regenerate code after modifying this file

        // Foo is an example field of EasyService. Edit easyservice_types.go to remove/update
        // Foo string `json:"foo,omitempty"`
        Size      *int32                      `json:"size"`
        Image     string                      `json:"image"`
        Resources corev1.ResourceRequirements `json:"resources,omitempty"`
        Envs      []corev1.EnvVar             `json:"envs,omitempty"`
        Ports     []corev1.ServicePort        `json:"ports,omitempty"`
}

// 监控cr状态的内容(这里直接拿内置资源DeploymentStatus的实现)
// EasyServiceStatus defines the observed state of EasyService
type EasyServiceStatus struct {
        // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
        // Important: Run "make" to regenerate code after modifying this file
        appsv1.DeploymentStatus `json:",inline"`
}

在调协代码中主要需要实现的方法:

func (r *EasyServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 如果不存在,则创建关联资源
    // 如果存在,判断是否需要更新
    //   如果需要更新,则直接更新
    //   如果不需要更新,则正常返回
    deploy := &appsv1.Deployment{}
    if err := r.Get(ctx, req.NamespacedName, deploy); err != nil && errors.IsNotFound(err) {
       // 1. 关联 Annotations
       data, _ := json.Marshal(easyService.Spec)
       if easyService.Annotations != nil {
          easyService.Annotations["spec"] = string(data)
       } else {
          easyService.Annotations = map[string]string{"spec": string(data)}
       }
       if err := r.Client.Update(ctx, &easyService); err != nil {
          return ctrl.Result{}, err
       }
       // 创建关联资源
       // 2. 创建 Deployment
       deploy := resources.NewDeploy(&easyService)
       if err := r.Client.Create(ctx, deploy); err != nil {
          return ctrl.Result{}, err
       }
       // 3. 创建 Service
       service := resources.NewService(&easyService)
       if err := r.Create(ctx, service); err != nil {
          return ctrl.Result{}, err
       }
       return ctrl.Result{}, nil
    }
    oldspec := appv1.EasyServiceSpec{}
    if err := json.Unmarshal([]byte(easyService.Annotations["spec"]), &oldspec); err != nil {
       return ctrl.Result{}, err
    }
    // 当前规范与旧的对象不一致,则需要更新
    if !reflect.DeepEqual(easyService.Spec, oldspec) {
       // 更新关联资源
       newDeploy := resources.NewDeploy(&easyService)
       oldDeploy := &appsv1.Deployment{}
       if err := r.Get(ctx, req.NamespacedName, oldDeploy); err != nil {
          return ctrl.Result{}, err
       }
       oldDeploy.Spec = newDeploy.Spec
       if err := r.Client.Update(ctx, oldDeploy); err != nil {
          return ctrl.Result{}, err
       }
    
       newService := resources.NewService(&easyService)
       oldService := &corev1.Service{}
       if err := r.Get(ctx, req.NamespacedName, oldService); err != nil {
          return ctrl.Result{}, err
       }
       // 需要指定 ClusterIP 为之前的,不然更新会报错
       newService.Spec.ClusterIP = oldService.Spec.ClusterIP
       oldService.Spec = newService.Spec
       if err := r.Client.Update(ctx, oldService); err != nil {
          return ctrl.Result{}, err
       }
       return ctrl.Result{}, nil
    }

对象结构(API)、控制器(controller)修改完毕后,需要更新crd的定义
更新crd定义的指令:

root@dev06:/home/liuweibin/learn-kubebuilder/webapp-operator$ make manifests
/home/liuweibin/learn-kubebuilder/webapp-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

测试controller

前提条件:
登入到在集群master上操作
第一步:将CRD安装到集群

$ make install
安装完毕后,可以在集群查到crd的信息
liuweibin@dev06:~/learn-kubebuilder/webapp-operator/controllers$ sudo kubectl get crd | grep easyservice
easyservices.app.kubebuilder.io              2023-07-15T15:26:15Z

第二步:启动控制器

root@dev06:/home/liuweibin/learn-kubebuilder/webapp-operator$ make run
/home/liuweibin/learn-kubebuilder/webapp-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/home/liuweibin/learn-kubebuilder/webapp-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
controllers/easyservice_controller.go
go vet ./...
go run ./main.go
I0718 14:33:26.878953   13419 request.go:601] Waited for 1.033483673s due to client-side throttling, not priority and fairness, request: GET:https://localhost:6443/apis/serving.kserve.io/v1beta1?timeout=32s
1.6896620081329308e+09        INFO        controller-runtime.metrics        Metrics server is starting to listen        {"addr": ":8080"}
1.6896620081333506e+09        INFO        setup        starting manager
1.6896620081337626e+09        INFO        Starting server        {"kind": "health probe", "addr": "[::]:8081"}
1.689662008133777e+09        INFO        Starting server        {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"}
1.6896620081339505e+09        INFO        Starting EventSource        {"controller": "easyservice", "controllerGroup": "app.kubebuilder.io", "controllerKind": "EasyService", "source": "kind source: *v1.EasyService"}
1.689662008134056e+09        INFO        Starting Controller        {"controller": "easyservice", "controllerGroup": "app.kubebuilder.io", "controllerKind": "EasyService"}
1.689662008235348e+09        INFO        Starting workers        {"controller": "easyservice", "controllerGroup": "app.kubebuilder.io", "controllerKind": "EasyService", "worker count": 1}
控制器启动后,启动相应的事件源、开始监听事件

第三步:新建CR
新建名为easyservice-sample的自定义资源实例,创建副本数量和对应的nodeport端口
新建成功会controller会有事件变更:

1.689662008235704e+09        INFO        fetch easyservice objects        {"controller": "easyservice", "controllerGroup": "app.kubebuilder.io", "controllerKind": "EasyService", "easyService": {"name":"easyservice-sample","namespace":"default"}, "namespace": "default", "name": "easyservice-sample", "reconcileID": "31643862-0be9-4aed-b206-6759d72bbb3d", "easyservice": {"kind":"EasyService","apiVersion":"app.kubebuilder.io/v1","metadata":{"name":"easyservice-sample","namespace":"default","selfLink":"/apis/app.kubebuilder.io/v1/namespaces/default/easyservices/easyservice-sample","uid":"7f32f186-6641-46db-a2e2-413d3d678212","resourceVersion":"113468805","generation":1,"creationTimestamp":"2023-07-18T05:58:49Z","annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"app.kubebuilder.io/v1\",\"kind\":\"EasyService\",\"metadata\":{\"annotations\":{},\"name\":\"easyservice-sample\",\"namespace\":\"default\"},\"spec\":{\"image\":\"nginx:1.7.9\",\"ports\":[{\"nodePort\":31002,\"port\":80,\"targetPort\":80}],\"size\":2}}\n"},"managedFields":[{"manager":"kubectl-client-side-apply","operation":"Update","apiVersion":"app.kubebuilder.io/v1","time":"2023-07-18T05:58:49Z","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}}},"f:spec":{".":{},"f:image":{},"f:ports":{},"f:size":{}}}}]},"spec":{"size":2,"image":"nginx:1.7.9","resources":{},"ports":[{"protocol":"TCP","port":80,"targetPort":80,"nodePort":31002}]},"status":{}}}
apiVersion: app.kubebuilder.io/v1
kind: EasyService
metadata:
  name: easyservice-sample
spec:
  # TODO(user): Add fields here
  size: 2
  image: nginx:1.7.9
  ports:
    - port: 80
      targetPort: 80
      nodePort: 31002

在k8s管理端能成功看到CR创建,并启动了对应数量的内置资源实例:
K8s operator从0到1实战_第9张图片
在这里插入图片描述
在这里插入图片描述
K8s operator从0到1实战_第10张图片

打镜像和集群部署

第一步:制作推送controller镜像

$ make docker-build docker-push IMG=<some-registry>/<project-name>:tag

第二步:把controller部署到集群
建议:在正式上线时,使用git控制上线controller版本

make deploy IMG=<some-registry>/<project-name>:tag

参考项目:
https://github.com/Crazybean-lwb/webapp-operator (Kind=EasyService)

展望使用场景

在云原生场景,便捷定义流程化应用(弹性云:云资源类型不限,应用范畴:弹性服务、输出类任务…)

  1. 优化(自定义)训练框架使用流程化
  2. 批量流程化业务输出
  3. 申请带生命周期的自定义运行时资源

你可能感兴趣的:(k8s,docker,容器,kubernetes,容器,云原生)