快速上手 K8S Operator

2023-10-18 14:24:45 浏览数 (1)

前言

如果你想要对 K8S 做二次开发或者说在原有的基础上封装一些功能让开发者更加好用,那么 Operator 的用法你可必须掌握。

什么是 Operator

我觉得 Operator 真的是 K8S 扩展设计的非常巧妙的一点,它好像一个插件系统,你有了它就好像有了 k8s 的一个扩展操作权,能扩展出各种各样的用法。那什么是 Operator 呢?这需要从 CRD 说起。

CRD

首先我们需要知道第一个概念就是 CRD(Custom Resource Define),自定义资源定义,顾名思义就是使用者可以通过 CRD 来创建自定义的资源。我们知道在 K8S 中有各种各样的资源 PodDeploymentStatefulSet… 在编写 yaml 文件的时候会指定对应的资源类型。

官方文档:Create a CustomResourceDefinition 其中有一个实际的 CustomResourceDefinition 案例

代码语言:javascript复制
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # name must match the spec fields below, and be in the form: <plural>.<group>
  name: crontabs.stable.example.com
spec:
  # group name to use for REST API: /apis/<group>/<version>
  group: stable.example.com
  # list of versions supported by this CustomResourceDefinition
  versions:
    - name: v1
      # Each version can be enabled/disabled by Served flag.
      served: true
      # One and only one version must be marked as the storage version.
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                cronSpec:
                  type: string
                image:
                  type: string
                replicas:
                  type: integer
  # either Namespaced or Cluster
  scope: Namespaced
  names:
    # plural name to be used in the URL: /apis/<group>/<version>/<plural>
    plural: crontabs
    # singular name to be used as an alias on the CLI and for display
    singular: crontab
    # kind is normally the CamelCased singular type. Your resource manifests use this.
    kind: CronTab
    # shortNames allow shorter string to match your resource on the CLI
    shortNames:
    - ct

---- 下面是具体的 object ----

apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
  name: my-new-cron-object
spec:
  cronSpec: "* * * * */5"
  image: my-awesome-cron-image

然后,有了它,你就可以像操作一个 pod 一样操作这个你定义的对象了,你还可以为它定义一些必要的属性(properties)。那么有了 CRD 之后,我们就有了一个非常强大的能力来扩展 k8s 已有的功能了。但是只有这样还是不够的。因为它仅仅定义了你所需要的资源,但是这个资源如何被操作呢?

Controller

有了资源没有人管肯定也不行,那么我们就需要一个 Controller 来控制它的行为和动作了。其实 Controller 本质是一个控制循环。我们知道,k8s 的控制模式其实是基于一个状态模型的,它将监控所有资源的状态,当现在的资源状态不满足用户定义的资源状态的时候,它就会做出调整,想办法让资源调整状态到预期值。

代码语言:javascript复制
for {
    实际状态 := 获取集群中对象 X 的实际状态(Actual State) 
    期望状态 := 获取集群中对象 X 的期望状态(Expectation State) 
    if 实际状态 == 期望状态{
        什么都不做
    } else {
        执行编排动作,将实际状态调整为期望状态
    }
}

当 Controller Manager 发现资源的实际状态和期望状态有偏差之后,会触发相应 Controller 注册的 Event Handler,让它们去根据资源本身的特点进行调整。

Operator

所以,我们可以简单的理解为 Operator = CRD Controller 也就是说自定义资源加自定义控制器就是 Operator,使用它我们不仅可以自定义我们想要的资源,还可以通过我们想要的逻辑和方式对它进行操作。

那此时你就可以想象的到它是有多万能了。比如:有了自定义资源,你可以定义你想要的各种属性,原来 deployment 只有那些属性,现在你就可以扩展各种你想要的属性了,并且你可以组合一些现有的资源。同时有了自定义控制器,你就可以任意的进行操作了,最重要的是,你能在出现各种情况(重启、异常退出等等)需要调度的时候第一时间知道,并且可以控制如何去调度,调度之后应该配置什么属性等等。

那本文下面就带你来快速制作一个 Demo 来体验一下 Operator,当然前提是你需要有一个可以操作的 k8s 环境。

使用 kubebuilder 创建 Operator

开发 Operator 并不一定要用 kubebuilder 还可以使用 https://github.com/operator-framework/operator-sdk 我更习惯用 kubebuilder 而已

安装

安装文档见:installation

代码语言:javascript复制
$ curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"  
$ chmod  x kubebuilder && mv kubebuilder /usr/local/bin/  

创建项目

代码语言:javascript复制
$ mkdir opex
$ cd opex
$ kubebuilder init --domain linkinstars.com --repo linkinstars.com/op-ex

创建 API

代码语言:javascript复制
$ kubebuilder create api --group example --version v1 --kind ExampleA
# 然后输入两次 y
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/examplea_types.go
api/v1/groupversion_info.go
internal/controller/suite_test.go
internal/controller/examplea_controller.go
...
...

此时项目结构已经创建好了,kubebuilder 也为我们创建了对应的 CRD 模板和 Controller 模板。你可以先大致浏览一下项目结构。下面我们就会开始编码的工作。

编码

首先明确一下我们的目标,我们的目标是创建一个 CRD 和 Controller 来体验一下 Operator。我们这次创建的 CRD 扮演一个监察的角色,当整个集群中出现带有指定名称的标签(Label)的对象时,监察就会改变自己的状态,变成监控中。

修改 CRD 的定义

修改 api/v1/examplea_types.go 文件

代码语言:javascript复制
package v1

import (
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ExampleASpec defines the desired state of ExampleA
type ExampleASpec struct {
  GroupName string `json:"groupName,omitempty"`
}

// ExampleAStatus defines the observed state of ExampleA
type ExampleAStatus struct {
  UnderControl bool `json:"underControl,omitempty"`
}

// kubebuilder:object:root=true
// kubebuilder:subresource:status

// ExampleA is the Schema for the examplea API
type ExampleA struct {
  metav1.TypeMeta   `json:",inline"`
  metav1.ObjectMeta `json:"metadata,omitempty"`

  Spec   ExampleASpec   `json:"spec,omitempty"`
  Status ExampleAStatus `json:"status,omitempty"`
}

// kubebuilder:object:root=true

// ExampleAList contains a list of ExampleA
type ExampleAList struct {
  metav1.TypeMeta `json:",inline"`
  metav1.ListMeta `json:"metadata,omitempty"`
  Items           []ExampleA `json:"items"`
}

func init() {
  SchemeBuilder.Register(&ExampleA{}, &ExampleAList{})
}

可以看到这里我们主要是定义了 ExampleASpec,也就是我们常常在 yaml 文件中写的 spec 属性,其中我们添加了 GroupName 也就是一个组名。

修改 Controller 的定义

修改 internal/controller/examplea_controller.go

代码语言:javascript复制
package controller

import (
  "context"
  corev1 "k8s.io/api/core/v1"
  "k8s.io/apimachinery/pkg/runtime"
  "k8s.io/apimachinery/pkg/types"
  examplev1 "linkinstars.com/op-ex/api/v1"
  ctrl "sigs.k8s.io/controller-runtime"
  "sigs.k8s.io/controller-runtime/pkg/client"
  "sigs.k8s.io/controller-runtime/pkg/handler"
  "sigs.k8s.io/controller-runtime/pkg/log"
  "sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// ExampleAReconciler reconciles a ExampleA object
type ExampleAReconciler struct {
  client.Client
  Scheme *runtime.Scheme
}

// kubebuilder:rbac:groups=example.linkinstars.com,resources=examplea,verbs=get;list;watch;create;update;patch;delete
// kubebuilder:rbac:groups=example.linkinstars.com,resources=examplea/status,verbs=get;update;patch
// kubebuilder:rbac:groups=example.linkinstars.com,resources=examplea/finalizers,verbs=update

// Reconcile 
func (r *ExampleAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  logger := log.FromContext(ctx)
  logger.Info("开始调用Reconcile方法")

  var exp examplev1.ExampleA
  if err := r.Get(ctx, req.NamespacedName, &exp); err != nil {
    logger.Error(err, "未找到对应的CRD资源")
    return ctrl.Result{}, client.IgnoreNotFound(err)
  }

  exp.Status.UnderControl = false

  var podList corev1.PodList
  if err := r.List(ctx, &podList); err != nil {
    logger.Error(err, "无法获取pod列表")
  } else {
    for _, item := range podList.Items {
      if item.GetLabels()["group"] == exp.Spec.GroupName {
        logger.Info("找到对应的pod资源", "name", item.GetName())
        exp.Status.UnderControl = true
      }
    }
  }

  if err := r.Status().Update(ctx, &exp); err != nil {
    logger.Error(err, "无法更新CRD资源状态")
    return ctrl.Result{}, err
  }
  logger.Info("已更新CRD资源状态", "status", exp.Status.UnderControl)
  return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *ExampleAReconciler) SetupWithManager(mgr ctrl.Manager) error {
  return ctrl.NewControllerManagedBy(mgr).
    For(&examplev1.ExampleA{}).
    Watches(
      &corev1.Pod{},
      handler.EnqueueRequestsFromMapFunc(r.podChangeHandler),
    ).
    Complete(r)
}

func (r *ExampleAReconciler) podChangeHandler(ctx context.Context, obj client.Object) []reconcile.Request {
  logger := log.FromContext(ctx)

  var req []reconcile.Request
  var list examplev1.ExampleAList
  if err := r.Client.List(ctx, &list); err != nil {
    logger.Error(err, "无法获取到资源")
  } else {
    for _, item := range list.Items {
      if item.Spec.GroupName == obj.GetLabels()["group"] {
        req = append(req, reconcile.Request{
          NamespacedName: types.NamespacedName{Name: item.Name, Namespace: item.Namespace},
        })
      }
    }
  }
  return req
}

核心逻辑非常简单,就是遍历所有的 pod,如果发现 label 中带有对应 groupName 的 pod 就修改当前 crd 的 UnderControl 状态为 true

代码语言:javascript复制
if item.GetLabels()["group"] == exp.Spec.GroupName {
    logger.Info("找到对应的pod资源", "name", item.GetName())
    exp.Status.UnderControl = true
}

其中有几个要点