关于golang:kubebuilder-入门实践

44次阅读

共计 7546 个字符,预计需要花费 19 分钟才能阅读完成。

本文实际的代码示例请参考: kube-falcon, 如果感觉有所帮忙, 请肯定 Star, 多谢 ^_^

下载与装置

咱们能够到 kubebuilder 在 github 上的 releases 页面, 下载反对以后零碎的二进制包;

  • kubebuilder releases 下载页面

以 mac 零碎为例, 具体的装置步骤如下:

# 解压安装包
$ tar -zxvf kubebuilder_2.3.1_darwin_amd64.tar.gz

# 本地创立装置目录
$ sudo mkdir /usr/local/kubebuilder/

# 将解压文件挪动到装置目录
$ sudo mv kubebuilder_2.3.1_darwin_amd64/* /usr/local/kubebuilder/

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

创立一个我的项目

# 创立我的项目目录
$ mkdir /src/kube-falcon && cd /src/kube-falcon

# 应用 go mod 初始化我的项目
$ go mod init kube-falcon

# 应用 kubebuilder 脚手架初始化我的项目目录构造
$ kubebuilder init --domain kubebuilder.io

创立一个 API

这里咱们创立一个 group 为 app, version 为 v1, kind 为 DeployObject 的 api:

# 创立 API
$ kubebuilder create api --group app --version v1 --kind DeployObject

执行后, 我的项目中的 api/ controllers/ 会别离生成对应的文件;

目录构造

当咱们应用 kubebuilder 脚手架初始化了一个我的项目目录, 并创立了一个 API 后, 能够大抵看到如下构造:

.
├── Dockerfile          # 用于构建控制器镜像的 Dockerfile
├── Makefile            # 用于控制器构建及部署的 Makefile
├── PROJECT             # 敢于生成组件的 kubebuilder 元数据
├── README.md
├── api                                 # API 模板代码所在目录
│   └── v1
│       ├── deployobject_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                     # 控制器逻辑所在目录
│   ├── deployobject_controller.go  # 控制器 reconcile 逻辑实现所在文件 
│   └── suite_test.go               # 测试文件
├── cover.out
├── go.mod              # Go Mod 配置文件, 记录依赖信息
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go             # 程序入口

开发阶段

咱们应用 kubebuilder 的目标是为了扩大 Kubernetes API, 实现咱们的自定义需要;

换言之, 咱们是要利用申明式 API 的形式, 扩大及构建属于咱们本人的自定义资源(CRD);

那么什么是申明式 API 呢?

其实艰深的讲就是 ” 告知解决者, 你的最终需要是什么, 而不是告知解决者, 他该怎么做 ”;

即咱们定义一个 K8S 对象的终态(Custom Resource), 让解决者自行处理实现, 其中的处理过程不须要关怀, 这里的解决者就是控制器(Controller);

所以对于开发者来讲, 实现自定义资源的扩大, 须要进行两个步骤:

  1. 编写 CRD 并将其部署至 K8S 集群中;
  2. 编写 Controller 并将其部署至 K8S 集群中;

以上提及的这两个步骤, 在 kubebuilder 的开发流程中都有所对应;

1. 编写 CRD

编写 CRD, 实质上是设计及定义申明式 API 的属性字段;

这外面着重关注${ProjectName}/api/${Version}/${Kind}_types.go 这个文件;

在这个文件中, 咱们着重关注 ${Kind}Spec${Kind}Status 这两个构造体;

这个两个构造体别离代表着一个 k8s 自定义资源所必须的两个局部 specstatus;

咱们须要将设计好的属性字段增加到所提及的上述构造体中,
而后在此之后执行 make 将会更新 config/crd/ 内的 yaml 定义;

如下是实例展现:

$ vi api/v1/deployobject_types.go
// DeployObjectSpec defines the desired state of DeployObject
type DeployObjectSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // +kubebuilder:validation:Minimum=0

    // Replicas is pod replica num
    Replicas *int32 `json:"replicas"`

    // +kubebuilder:validation:MinLength=0

    // Image is container image address
    Image string `json:"image"`

    // Resources describes the compute resource requirements
    // +optional
    Resources corev1.ResourceRequirements `json:"resources,omitempty"`

    // EnvVar represents an environment variable present in a Container.
    // +optional
    Env []corev1.EnvVar `json:"env,omitempty"`

    // ServicePort contains information on service's port.
    Ports []corev1.ServicePort `json:"ports"`}

// DeployObjectStatus defines the observed state of DeployObject
type DeployObjectStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // DeploymentStatus is the most recently observed status of the Deployment.
    appsv1.DeploymentStatus `json:",inline"`
}

2. 编写 Controller

咱们晓得, Controller 的工作原理其实就是将自定义资源 (CR) 定义的预期状态与以后资源的理论状态进行比对, 若无差别则略过, 若有差别则须要将理论状态更新为预期的状态值;

这个比对更新的过程在 Controller 中被称为调谐(Reconcile);

而咱们编写 Controller 的次要逻辑, 大部分都集中在这个调谐的逻辑上;

在生成的脚手架目录中, 咱们次要关注 ${ProjectName}/controllers/${Kind}_controller.go 这个文件;

再此文件中, 咱们要关注 Reconcile() 这个办法, 此办法中的实现即是调谐的整个逻辑;

这里展现的是一段示例的调谐逻辑,
次要性能实现了通过一个自定义资源 (CR) 来治理 Deployment 以及 Service 的逻辑:

$ vi controllers/deployobject_controller.go
func (r *DeployObjectReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {ctx := context.Background()
    log := r.Log.WithValues("deployobject", req.NamespacedName)

    // 1. 获取 NS.DeployObject 实例
    var deployObject apiv1.DeployObject
    if err := r.Get(ctx, req.NamespacedName, &deployObject); err != nil {log.Info("unable to fetch DeployObject")
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 2. DeployObject 实例处于删除状态, 则退出
    if deployObject.DeletionTimestamp != nil {log.Info("DeployObject/%s is deleting", req.Name)
        return ctrl.Result{}, nil}

    // 3. 检测 NS 下是否有已创立的关联 Deployment
    var deployment appsv1.Deployment
    if err := r.Get(ctx, req.NamespacedName, &deployment); err != nil && apierrors.IsNotFound(err) {
        // create deployment
        deployment := newDeployment(&deployObject)
        if err := r.Create(ctx, deployment); err != nil {log.Error(err, "create deployment failed")
            return ctrl.Result{}, err}

        // create service
        service := newService(&deployObject)
        if err := r.Create(ctx, service); err != nil {log.Error(err, "create service failed")
            return ctrl.Result{}, err}

        // update DeployObject spec annotation
        setDeployObjectSpecAnnotation(&deployObject)
        if err := r.Update(ctx, &deployObject); err != nil {log.Error(err, "update deployobject failed")
            return ctrl.Result{}, err}
        return ctrl.Result{}, nil}

    // 若 deployment 已存在, 则取出 DeployObject.Annotation 中之前的 DeployObject.Spec, 与以后 DeployObject.Spec 比照
    var prevSpec apiv1.DeployObjectSpec
    if err := json.Unmarshal([]byte(deployObject.Annotations["spec"]), &prevSpec); err != nil {log.Error(err, "parse spec annotation failed")
        return ctrl.Result{}, err}

    if !reflect.DeepEqual(deployObject.Spec, prevSpec) {
        // 与上一次的 Spec 有差别, 则须要更新 deployment & service
        newDeploy := newDeployment(&deployObject)
        var curDeploy appsv1.Deployment
        if err := r.Get(ctx, req.NamespacedName, &curDeploy); err != nil {log.Error(err, "get current deployment failed")
            return ctrl.Result{}, err}
        curDeploy.Spec = newDeploy.Spec
        if err := r.Update(ctx, &curDeploy); err != nil {log.Error(err, "update current deployment failed")
            return ctrl.Result{}, err}

        newService := newService(&deployObject)
        var curService corev1.Service
        if err := r.Get(ctx, req.NamespacedName, &curService); err != nil {log.Error(err, "get service failed")
            return ctrl.Result{}, err}
        clusterIP := curService.Spec.ClusterIP
        curService.Spec = newService.Spec
        curService.Spec.ClusterIP = clusterIP
        if err := r.Update(ctx, &curService); err != nil {log.Error(err, "update service failed")
            return ctrl.Result{}, err}

        // update DeployObject spec annotation
        setDeployObjectSpecAnnotation(&deployObject)
        if err := r.Update(ctx, &deployObject); err != nil {log.Error(err, "update deployobject failed")
            return ctrl.Result{}, err}
    } else {
        // 新旧 DeployObject.Spec 雷同, 但 DeployObject 与 Deployment & Service 的设置有不同, 则需纠正 Deployment & Service 的设置
        // 比照 replicas / image / port/
        newDeploy := newDeployment(&deployObject)
        var curDeploy appsv1.Deployment
        if err := r.Get(ctx, req.NamespacedName, &curDeploy); err != nil {log.Error(err, "same spec, get current deployment failed")
            return ctrl.Result{}, err}

        newService := newService(&deployObject)
        var curService corev1.Service
        if err := r.Get(ctx, req.NamespacedName, &curService); err != nil {log.Error(err, "same spec, get current service failed")
            return ctrl.Result{}, err}

        if (*deployObject.Spec.Replicas != *curDeploy.Spec.Replicas) ||
            (deployObject.Spec.Image != curDeploy.Spec.Template.Spec.Containers[0].Image) {log.Info("same spec, update current deployment ...")
            curDeploy.Spec = newDeploy.Spec
            if err := r.Update(ctx, &curDeploy); err != nil {log.Error(err, "same spec, update current deployment failed")
                return ctrl.Result{}, err}
        }
        if !reflect.DeepEqual(deployObject.Spec.Ports, curService.Spec.Ports) {log.Info("same spec, update current service ...")
            clusterIP := curService.Spec.ClusterIP
            curService.Spec = newService.Spec
            curService.Spec.ClusterIP = clusterIP
            if err := r.Update(ctx, &curService); err != nil {log.Error(err, "same spec, update service deployment failed")
                return ctrl.Result{}, err}
        }
    }

    return ctrl.Result{}, nil}

测试运行

执行如下命令将 config/crd 中 CRD 部署到 k8s 集群中:

$ make install

本地运行 controller:

$ make run

装置自定义资源(CR):

$ kubectl apply -f config/samples/

构建及部署

构建 docker 镜像及将镜像推送镜像仓库:

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

应用 docker 镜像, 部署 controller 到 k8s 集群:

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

卸载 CRDs:

$ make uninstall

参考:

  • https://book.kubebuilder.io/
  • https://cloudnative.to/kubebu…
  • https://juejin.cn/post/684490…

正文完
 0