kubebuilder扩展k8s

kubebuilder是个专门用于开发k8s的框架

k8s有很多资源如deployment,cronjob等资源,这些资源的行为则由位于controller-manager中的各个资源控制器来实现逻辑,

安装

https://github.com/kubernetes-sigs/kubebuilder/releases下载合适的二进制文件并放入path中

术语

  • GV: Api Group和Version
    • API Group 是相关API功能的集合,
    • 每个 Group 拥有一或多个Versions
  • GVK: Group Version Kind
    • 每个GV都包含很多个api 类型,称之为Kinds,不同Version同一个Kinds可能不同
  • GVR: Group Version Rsource
    • Resource 是 Kind 的对象标识,一般来Kind和Resource 是1:1 的,但是有时候存在 1:n 的关系,不过对于Operator来说都是 1:1 的关系
apiVersion: apps/v1 # 这个是 GV,G 是 apps,V 是 v1
kind: Deployment    # 这个就是 Kind
sepc:               # 加上下放的 spec 就是 Resource了

根据GVK K8s就能找到你到底要创建什么类型的资源,根据你定义的Spec创建好资源之后就成为了Resource,也就是GVR。GVK/GVR就是K8s资源的坐标,是我们创建/删除/修改/读取资源的基础

类似这样的关系/group/version/kind

示例

项目初始化

完整代码:https://github.com/NatureLR/code-example/tree/master/operator

需求背景

我们在部署服务的时候经常需要同时部署deployment和svc这样很复杂,于是自定义一个资源叫appx,让appx来创建svc和deployment

初始化文件夹

在项目文件夹下执行

kubebuilder init --repo github.com/naturelr/code-example/operator --domain naturelr.cc --skip-go-version-check

这个时候目录下会产生一些文件

├── Dockerfile # 编译docker镜像
├── Makefile # 编译部署相关的脚本,常用功能都在里面
├── PROJECT # 项目说明
├── config # 这个目录都是一些需要安装到集群的文件
│   ├── default # 默认配置
│   ├── manager # crd文件
│   ├── prometheus # 监控相关的如ServiceMonitor
│   └── rbac # rbac文件
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go

6 directories, 24 files
创建api模板

执行下面的命令,创建api,期间会问你是不是需要创建Resource和Controller,这里我们都选y

kubebuilder create api --group appx --version v1 --kind Appx

完成之后多了一些目录

.
├── Dockerfile
├── Makefile
├── PROJECT
├── api
│   └── v1 # 我们自定义的api
├── bin
│   └── controller-gen # 生成文件的程序
├── config
├── controllers
│   ├── appx_controller.go # 控制逻辑写在这
│   └── suite_test.go # 测试用例
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go

12 directories, 10 files
实现
定义字段

api/v1/application_types.go中的AppxSpec写上需要的字段

type AppxSpec 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 Appx. Edit appx_types.go to remove/update
 Image string `json:"image,omitempty"`
 Port  int    `json:"port,omitempty"`
}

然后执行make manifests generate命令生成crd文件

生成的crd文件在config/crd/bases/

实现控制器

有crd只能在k8s中定义cr但是k8s并不知道如何处理这些cr,所以我们要实现控制器来处理这些逻辑

我们需要实现的控制器逻辑在controllers/application_controller.go中的Reconcile函数中

逻辑改完之后就需要上测试了,执行make install安装crd到集群,注意他会安装到~/.kube/config这个配置文件中的集群

然后执行make run运行控制器,他会打印很多日志

  • 获取cd,拿到cr中定义的镜像和端口号
appx := &appxv1.Appx{}
if err := r.Get(ctx, req.NamespacedName, appx); err != nil {
 return ctrl.Result{}, err
}
  • 拿到信息之后需要创建对应的deployment对象和service对象,需要特别注意的是要管理创建的资源,不然删除的不会删除创建的子资源
svc := &apiv1.Service{}
if err := r.Get(ctx, req.NamespacedName, svc); err != nil {
  if client.IgnoreNotFound(err) != nil { 
    return ctrl.Result{}, err// 如果有错误且不是没找到的话就直接返回错误
  }
  // 没找到就创建资源
  if svc.Name == "" {
    l.Info("创建service:", "名字", appx.Name)
    svc = &apiv1.Service{
      ObjectMeta: metav1.ObjectMeta{
        Name:      req.Name,
        Namespace: req.Namespace,
      },
        Spec: apiv1.ServiceSpec{
        Selector: map[string]string{"app": req.Name},
        Ports: []apiv1.ServicePort{{
          Port:       int32(appx.Spec.Port),
          TargetPort: intstr.FromInt(appx.Spec.Port),
        },
        },
      },
    }
    // 关联 appx和deployment
    if err := controllerutil.SetOwnerReference(appx, svc, r.Scheme); err != nil {
       return ctrl.Result{}, err
    }
    if err := r.Create(ctx, svc); err != nil {
       return ctrl.Result{}, err
    }
    l.Info("创建service成功")
  }
}
  • 如果已经有此资源,那么可能就需要更新资源了
// svc
svc.Spec.Ports = []apiv1.ServicePort{{Port: int32(appx.Spec.Port)}}
l.Info("更新service", "port", appx.Spec.Image)
if err := r.Update(ctx, svc); err != nil {
   return ctrl.Result{}, err
}
l.Info("service更新完成")

到此一个简单的crd的控制逻辑就完成了

status

上面创建的cr当查看的时候并不会显示status

  • api/v1/appx_types.go中找到AppxStatus,添加上合适的字段
// AppxStatus defines the observed state of Appx
type AppxStatus struct {
  // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
  // Important: Run "make" to regenerate code after modifying this file
  // 必须要有json tag
  Workload int32  `json:"workload"`
  Svc      string `json:"svc"`
}
  • controllers/application_controller.go中更新status字段
appx.Status.Workload = *deploy.Spec.Replicas
appx.Status.Svc = fmt.Sprintf("%d", svc.Spec.Ports[0].Port)
r.Status().Update(ctx, appx)
// 注意type要对应上字段!!!
//+kubebuilder:printcolumn:JSONPath=".status.workload",name=Workload,type=integer
//+kubebuilder:printcolumn:JSONPath=".status.svc",name=Svc,type=string
  • 同样需要重新生成crd并且要安装
event事件

evnet事件,有的时候告诉我们一些重要的信息

  • controllers/application_controller.go中增加字段
// AppxReconciler reconciles a Appx object
type AppxReconciler struct {
  client.Client
  Scheme   *runtime.Scheme
  Recorder record.EventRecorder//增加事件结构体
}
  • 调用
r.Recorder.Event(appx, apiv1.EventTypeNormal, "找到cr", appx.Name)
  • main.go中加上Recorder的初始化逻辑
if err = (&controllers.AppxReconciler{
  Client:   mgr.GetClient(),
  Scheme:   mgr.GetScheme(),
  Recorder: mgr.GetEventRecorderFor("Appx"), //+
}).SetupWithManager(mgr); err != nil {
  setupLog.Error(err, "unable to create controller", "controller", "Appx")
  os.Exit(1)
}
$ kubectl get event
LAST SEEN   TYPE     REASON   OBJECT     MESSAGE
2m55s       Normal   找到cr     appx       
4s          Normal   找到cr     appx/foo   foo  

常用命令

# 初始化
kubebuilder init --repo github.com/naturelr/code-example/operator --domain naturelr.cc --skip-go-version-check

# 创建 api
kubebuilder create api --group appx --version v1 --kind Appx

# 创建webhook
kubebuilder create webhook --group nodes --version v1 --kind Appx --defaulting --programmatic-validation

# 生成文件
make manifests generate

# 安装crd等文件
make install

# 本地调试运行
make run

参考资料

https://book.kubebuilder.io/introduction.html
https://lailin.xyz/post/operator-03-kubebuilder-tutorial.html