Kubebuilder实现mysql-operator

前言

kubebuilder是一个快速实现kubernetes Operator的框架,通过kubebuilder我们能够快速定义CRD资源和实现controller逻辑。我们可以简单地将operator理解为:operator=crd+controller

kubebuilder自身携带了两个工具:controller-runtime与controller-tools,我们可以通过controller-tools来生成大部分的代码,从而我们可以将我们的主要精力放置在CRD的定义和编写controller的reconcile逻辑上。所谓的reconcile,即调谐,是指将资源的status通过一系列的操作调整至与定义的spec一致。

从整体来看,通过kubebuilder来实现operator大致可分为以下步骤:

  1. 创建工作目录,初始化项目
  2. 创建API,填充字段
  3. 创建Controller,编写核心调谐逻辑(Reconcile)
  4. 创建Webhook,实现接口(可选)
  5. 验证测试
  6. 发布到集群中

最佳实践

我们以创建一个mysql的operator来作为最佳实践的例子。我们期望这个mysql-operator能够实现deployment创建和更新,以及mysql实例数的扩缩容。

环境准备

组件检查

确保实践环境中拥有以下组件:

  • go version v1.15+.
  • docker version 17.03+.
  • kubectl version v1.11.3+.
  • kustomize v3.1.0+
  • 能够访问 Kubernetes v1.11.3+ 集群

安装kubebuilder

curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
chmod +x kubebuilder && mv kubebuilder /usr/local/bin/

创建项目

初始化项目

kubebuilder init --domain tutorial.kubebuilder.io

创建kubebuilder项目之前必须要先有go project,否则会报错。
创建完成之后,我们可以看到此时项目中多了许多的代码,其中:

  • go.mod: 我们的项目的 Go mod 配置文件,记录依赖库信息。
  • Makefile: 用于控制器构建和部署的 Makefile 文件
  • PROJECT: 用于生成组件的 Kubebuilder 元数据

创建API

kubebuilder create api --group batch --version v1 --kind Mysql  
Create Resource [y/n]
y
Create Controller [y/n]
y

编写CRD资源

编写CRD资源的定义主要是在mysql_types.go文件中,编写controller逻辑主要是在mysql_controller.go文件中。

我们首先编写CRD的定义。我们简单地用一个yaml文件来描述我们所期望的定义:

apiVersion: batch.tutorial.kubebuilder.io/v1
kind: Mysql
metadata:
  name: mysql-sample
spec:
  replicas: 1
  image: flyer103/mysql:5.7
  command: [ "/bin/bash", "-ce", "tail -f /dev/null" ]

我们需要在mysql_types.go文件中定义我们的spec和status。

定义spec

// MysqlSpec defines the desired state of Mysql
type MysqlSpec struct {
   // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
   // Important: Run "make" to regenerate code after modifying this file
   Name    string   `json:"name,omitempty"`
   Image   string   `json:"image,omitempty"`
   Command []string `json:"command,omitempty" protobuf:"bytes,3,rep,name=command"`
   // +optional
   //+kubebuilder:default:=1
   //+kubebuilder:validation:Minimum:=1
   Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"`
}

定义status

我们在这里针对code重新定义了string类型。

type MySqlCode string

const (
   SuccessCode    MySqlCode = "success"
   FailedCode     MySqlCode = "failed"
)

// MysqlStatus defines the observed state of Mysql
type MysqlStatus struct {
   // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
   // Important: Run "make" to regenerate code after modifying this file
   Code          MySqlCode `json:"code,omitempty"`
   Replicas      int32     `json:"replicas"`
   ReadyReplicas int32     `json:"readyReplicas"`
}

添加kubectl展示参数标记

//+kubebuilder:object:root 这样的以//+开头的注释在kubebuilder中被称为元数据标记,这些标记的作用就是告诉controller-tools生成额外的信息。

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.labelSelector
//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.code",description="The phase of game."
//+kubebuilder:printcolumn:name="DESIRED",type="integer",JSONPath=".spec.replicas",description="The desired number of pods."
//+kubebuilder:printcolumn:name="CURRENT",type="integer",JSONPath=".status.replicas",description="The number of currently all pods."
//+kubebuilder:printcolumn:name="READY",type="integer",JSONPath=".status.readyReplicas",description="The number of pods ready."

编写controller逻辑

kubebuilder为我们在mysql_controller.go文件中预先生成了两个函数,分别是Reconcile()和SetupWithManager()。关于这两个函数的区别,我们可以简单地理解为将 Reconcile 添加到 manager 中,这样当 manager 启动时它就会被启动。

编写Reconcile函数

kubebuilder自动为我们生成了Reconcile函数,我们只需编写它的业务逻辑即可。我们希望能够通过mysql-operator来管理具体的deployment,因此我们具体需要调谐的逻辑其实是deployment的spec和status的一致。
在Reconcile函数中,我们主要实现以下逻辑:

  1. 通过查询获取mysql对象
  2. 如果mysql处于删除状态,则跳过
  3. 对mysql进行同步操作
func (r *MysqlReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   defer utilruntime.HandleCrash()

   logger := log.FromContext(ctx)
   logger.Info("revice reconcile event", "name", req.String())

   // 获取mysql对象
   logger.Info("get mysql object", "name", req.String())
   mysql := &batchv1.Mysql{}
   if err := r.Get(ctx, req.NamespacedName, mysql); err != nil {
      return ctrl.Result{}, client.IgnoreNotFound(err)
   }

   // 如果mysql在删除,则跳过
   if mysql.DeletionTimestamp != nil {
      logger.Info("mysql in deleting", "name", req.String())
      return ctrl.Result{}, nil
   }

   // 同步资源状态
   logger.Info("begin to sync mysql", "name", req.String())
   if err := r.syncMysql(ctx, mysql); err != nil {
      logger.Error(err, "failed to sync mysql", "name", req.String())
      return ctrl.Result{}, err
   }

   return ctrl.Result{}, nil
}

我们将具体的同步逻辑抽象为syncMysql函数,我们希望该函数能够实现以下逻辑:

  1. 查询deployment
  2. 如果deployment不存在,则根据mysql的spec创建deployment
  3. 如果deployment存在,则更新其spec使其与mysql的spec一致
  4. 更新mysql的status
const (
   mysqlLabelName = "tutorial.kubebuilder.io/mysql"
)

func (r *MysqlReconciler) syncMysql(ctx context.Context, obj *batchv1.Mysql) error {

   logger := log.FromContext(ctx)
   mysql := obj.DeepCopy()

   name := types.NamespacedName{
      Namespace: mysql.Namespace,
      Name:      mysql.Name,
   }
   // 构造owner
   owner := []metav1.OwnerReference{
      {
         APIVersion:         mysql.APIVersion,
         Kind:               mysql.Kind,
         Name:               mysql.Name,
         Controller:         pointer.BoolPtr(true),
         BlockOwnerDeletion: pointer.BoolPtr(true),
         UID:                mysql.UID,
      },
   }

   labels := map[string]string{
      mysqlLabelName: mysql.Name,
   }

   meta := metav1.ObjectMeta{
      Name:            mysql.Name,
      Namespace:       mysql.Namespace,
      Labels:          labels,
      OwnerReferences: owner,
   }

   deploy := &appsv1.Deployment{}
   if err := r.Get(ctx, name, deploy); err != nil {
      logger.Info("get deployment success")
      if !errors.IsNotFound(err) {
         return err
      }
      deploy = &appsv1.Deployment{
         ObjectMeta: meta,
         Spec:       getDeploymentSpec(mysql, labels),
      }
      if err := r.Create(ctx, deploy); err != nil {
         return nil
      }
      logger.Info("create Deployment success", "name", name.String())
   } else {
      want := getDeploymentSpec(mysql, labels)
      get := getSpecFromDeployment(deploy)
      if !reflect.DeepEqual(want, get) {
         new := deploy.DeepCopy()
         new.Spec = want
         if err := r.Update(ctx, new); err != nil {
            return err
         }
         logger.Info("update deployment success", "name", name.String())
      }
   }

   if *mysql.Spec.Replicas == deploy.Status.ReadyReplicas {
      mysql.Status.Code = batchv1.SuccessCode
   } else {
      mysql.Status.Code = batchv1.FailedCode
   }

   r.Client.Status().Update(ctx, mysql)

   return nil
}
func getDeploymentSpec(mysql *batchv1.Mysql, labels map[string]string) appsv1.DeploymentSpec {
   return appsv1.DeploymentSpec{
      Replicas: mysql.Spec.Replicas,
      Selector: metav1.SetAsLabelSelector(labels),
      Template: corev1.PodTemplateSpec{
         ObjectMeta: metav1.ObjectMeta{
            Labels: labels,
         },
         Spec: corev1.PodSpec{
            Containers: []corev1.Container{
               {
                  Name:    "main",
                  Image:   mysql.Spec.Image,
                  Command: mysql.Spec.Command,
               },
            },
         },
      },
   }
}

func getSpecFromDeployment(deploy *appsv1.Deployment) appsv1.DeploymentSpec {
   container := deploy.Spec.Template.Spec.Containers[0]
   return appsv1.DeploymentSpec{
      Replicas: deploy.Spec.Replicas,
      Selector: deploy.Spec.Selector,
      Template: corev1.PodTemplateSpec{
         ObjectMeta: metav1.ObjectMeta{
            Labels: deploy.Spec.Template.Labels,
         },
         Spec: corev1.PodSpec{
            Containers: []corev1.Container{
               {
                  Name:    container.Name,
                  Image:   container.Image,
                  Command: container.Command,
               },
            },
         },
      },
   }
}

编写SetUpWithManager函数

// SetupWithManager sets up the controller with the Manager.
func (r *MysqlReconciler) SetupWithManager(mgr ctrl.Manager) error {
   ctrl.NewControllerManagedBy(mgr).
      WithOptions(controller.Options{
         MaxConcurrentReconciles: 3,
      }).
      For(&batchv1.Mysql{}).
      Owns(&appsv1.Deployment{}).
      Complete(r)

   return nil
}

添加rbac标记

//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch

部署验证

安装CRD

每次修改mysql_types.go文件之后,都需要重新执行以下命令

make install

本地运行operator

每次修改mysql_controller.go文件之后,都需要重新执行以下命令

make run

部署CRD到集群中

部署我们在前边定义好的yaml文件都本地集群当中

➜ kubectl apply -f batch_v1_mysql.yaml
mysql.batch.tutorial.kubebuilder.io/mysql-sample created
查看是否创建成功
➜ kubectl get mysql
NAME           PHASE     AGE
mysql-sample   success   19s

➜ kubectl get deployment
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
mysql-sample   1/1     1            1           27s

➜ kubectl get pod | grep mysql-sample
mysql-sample-6dd55bb569-pgzhz   1/1     Running   0               31s

验证扩容

➜ kubectl scale mysqls.batch.tutorial.kubebuilder.io mysql-sample --replicas 2
mysql.batch.tutorial.kubebuilder.io/mysql-sample scaled

版权声明:本文为weixin_43915303原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。