介绍

Kubebuilder是一个由controller-runtime支持的出色SDK,它可以帮助您轻松快速地在 Go 中编写Kubernetes operator,方法是处理多种忙碌的事情,例如以组织良好的方式引导大量样板代码,设置有用的 Makefile make,目标是构建、运行和部署operator、构建 CRD、设置相关的 Dockefile、RBAC、涉及部署operator的多个 YAML 等等。
Kubebuilder 是一个使用 CRDs 构建 K8s API 的 SDK,主要是:

  • 提供脚手架工具初始化 CRDs 工程,自动生成 boilerplate 代码和配置;
  • 提供代码库封装底层的 K8s go-client;

方便用户从零开始开发 CRDs,Controllers 和 Admission Webhooks 来扩展 K8s。

为了编写自定义的operatro,需要进行如下:

  1. 安装kubebuilder
  2. 使用kubebuilder进行项目创建
  3. 编写operator代码
  4. 编译打包镜像上传到镜像仓库。

原理

img

基础概念

GVKs&GVRs

GVK = GroupVersionKind,GVR = GroupVersionResource。

  • API Group & Versions(GV)

API Group 是相关 API 功能的集合,每个 Group 拥有一或多个 Versions,用于接口的演进。

  • Kinds & Resources(GVR)

每个 GV 都包含多个 API 类型,称为 Kinds,在不同的 Versions 之间同一个 Kind 定义可能不同, Resource 是 Kind 的对象标识(resource type),一般来说 Kinds 和 Resources 是 1:1 的,比如 pods Resource 对应 Pod Kind,但是有时候相同的 Kind 可能对应多个 Resources,比如 Scale Kind 可能对应很多 Resources:deployments/scale,replicasets/scale,对于 CRD 来说,只会是 1:1 的关系。

每一个 GVK 都关联着一个 package 中给定的 root Go type,比如 apps/v1/Deployment 就关联着 K8s 源码里面 k8s.io/api/apps/v1 package 中的 Deployment struct,我们提交的各类资源定义 YAML 文件都需要写:

apiVersion:这个就是 GV 。kind:这个就是 K。

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

Scheme

每一组 Controllers 都需要一个 Scheme,提供了 Kinds 与对应 Go types 的映射,也就是说给定 Go type 就知道他的 GVK,给定 GVK 就知道他的 Go type,比如说我们给定一个 Scheme: “tutotial.kubebuilder.io/api/v1”.CronJob{} 这个 Go type 映射到 batch.tutotial.kubebuilder.io/v1 的 CronJob GVK,那么从 Api Server 获取到下面的 JSON:

{
    "kind": "CronJob",
    "apiVersion": "batch.tutorial.kubebuilder.io/v1",
    ...
}

就能构造出对应的 Go type了,通过这个 Go type 也能正确地获取 GVR 的一些信息,控制器可以通过该 Go type 获取到期望状态以及其他辅助信息进行调谐逻辑。

Manager

Kubebuilder 的核心组件,具有 3 个职责:

  • 负责运行所有的 Controllers;
  • 初始化共享 caches,包含 listAndWatch 功能;
  • 初始化 clients 用于与 Api Server 通信。

Cache

Kubebuilder 的核心组件,负责在 Controller 进程里面根据 Scheme 同步 Api Server 中所有该 Controller 关心 GVKs 的 GVRs,其核心是 GVK -> Informer 的映射,Informer 会负责监听对应 GVK 的 GVRs 的创建/删除/更新操作,以触发 Controller 的 Reconcile 逻辑。

Controller

Kubebuidler 为我们生成的脚手架文件,我们只需要实现 Reconcile 方法即可。

Client

在实现 Controller 的时候不可避免地需要对某些资源类型进行创建/删除/更新,就是通过该 Clients 实现的,其中查询功能实际查询是本地的 Cache,写操作直接访问 Api Server。

Index

由于 Controller 经常要对 Cache 进行查询,Kubebuilder 提供 Index utility 给 Cache 加索引提升查询效率。

Finalizer

在一般情况下,如果资源被删除之后,我们虽然能够被触发删除事件,但是这个时候从 Cache 里面无法读取任何被删除对象的信息,这样一来,导致很多垃圾清理工作因为信息不足无法进行,K8s 的 Finalizer 字段用于处理这种情况。在 K8s 中,只要对象 ObjectMeta 里面的 Finalizers 不为空,对该对象的 delete 操作就会转变为 update 操作,具体说就是 update deletionTimestamp 字段,其意义就是告诉 K8s 的 GC“在deletionTimestamp 这个时刻之后,只要 Finalizers 为空,就立马删除掉该对象”。

所以一般的使用姿势就是在创建对象时把 Finalizers 设置好(任意 string),然后处理 DeletionTimestamp 不为空的 update 操作(实际是 delete),根据 Finalizers 的值执行完所有的 pre-delete hook(此时可以在 Cache 里面读取到被删除对象的任何信息)之后将 Finalizers 置为空即可。

OwnerReference

K8s GC 在删除一个对象时,任何 ownerReference 是该对象的对象都会被清除,与此同时,Kubebuidler 支持所有对象的变更都会触发 Owner 对象 controller 的 Reconcile 方法。

实现

1、构造新的scheme,
2、解析命令行参数,获取或默认metric和健康检查端口。
3、实例化 manager,参数 config
3.1) 向 manager 添加 scheme
3.2) 向 manager 添加 controller,该 controller 包含一个 reconciler 结构体,我们需要在 reconciler 结构体实现逻辑处理
4、向manager添加healthz和readyz探测。
5、启动 manager.start()

镜像仓库

注册仓库

登录https://registry.hub.docker.com/进行注册,如用户名为mospany, 密码为自定义。

使用镜像

登录仓库

$ docker login index.docker.io
Username: mospany
Password:
Login Succeeded

或直接 docker login默认登录docker hub。

上传镜像

$ docker tag loggen:latest mospany/loggen:latest
$ docker push mospany/loggen:latest

img

参见:https://hub.docker.com/repository/docker/mospany/loggen

下载镜像

$ docker pull mospany/loggen:latest
latest: Pulling from mospany/loggen
Digest: sha256:0cdeece36f8a003dd6b9c463cc73dad93479deabec08c1def033e72ec9818539
Status: Image is up to date for mospany/loggen:latest
docker.io/mospany/loggen:latest

项目

创建项目

mkdir guestbook
cd guestbook
go mod init guestbook
kubebuilder init --domain xiaohongshu.org --owner "luxiu"
kubebuilder create api --group redis  --version v1 --kind RedisCluster

关键截图如下:
img

修改文件

1)修改Dockerfile的gcr.io镜像为其他可访问镜像(如golang:1.18)

为了防止出现“failed to solve with frontend dockerfile.v0: failed to create LLB definition: failed to do request: Head “https://gcr.io/v2/distroless/static/manifests/nonroot“: Service Unavailable”错误,需修改Dockerfile的gcr.io镜像为其他可访问镜像(如golang:1.18)

2)修改Dockerfile添加代理

为了防止go mod download时不至于超时连不上,需在Run go mod download行上面添加

ENV GOPROXY="https://goproxy.cn"

否则会出现如下错误:

3.469 go: cloud.google.com/go@v0.81.0: Get “https://proxy.golang.org/cloud.google.com/go/@v/v0.81.0.mod“: malformed HTTP response “\x00\x00\x12\x04…\x00\x00\x01”

**切记切记**: 先修改好再编译,否则一直出现上面错误(当时找了半天)

# Build the manager binary
  FROM golang:1.18 as builder
  ENV GOPROXY="https://goproxy.cn"

  WORKDIR /workspace
  # Copy the Go Modules manifests
  COPY go.mod go.mod
  COPY go.sum go.sum
  # cache deps before building and copying source so that we don't need to re-download as much
  # and so that source changes don't invalidate our downloaded layer
  RUN go mod download

  # Copy the go source
  COPY main.go main.go
  COPY api/ api/
  COPY controllers/ controllers/

  # Build
  RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go

  # Use distroless as minimal base image to package the manager binary
  # Refer to https://github.com/GoogleContainerTools/distroless for more details
  #FROM gcr.io/distroless/static:nonroot
  FROM centos:latest
  WORKDIR /
  COPY --from=builder /workspace/manager .
  USER 65532:65532

  ENTRYPOINT ["/manager"]

必须在Dockerfile里面设置代理”ENV GOPROXY=”

img

3)修改Makefile中的crd中的配置
给kubectl加上所要连接的集群, 如本机为–context docker-desktop。可通过如下命令获得:

kubectl config get-contexts
kubectl cluster-info
kubectl config view

img

对应的Makefile修改如下:

.PHONY: install
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
  $(KUSTOMIZE) build config/crd | kubectl --context docker-desktop  apply -f -

.PHONY: uninstall
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
  $(KUSTOMIZE) build config/crd | kubectl --context docker-desktop  delete --ignore-not-found=$(ignore-not-found) -f -

.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
  cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
  $(KUSTOMIZE) build config/default | kubectl --context docker-desktop  apply -f -

.PHONY: undeploy
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
  $(KUSTOMIZE) build config/default | kubectl --context docker-desktop  delete --ignore-not-found=$(ignore-not-found) -f -

编译

make help

make help

img

make build

编译并在bin/下生成目标可执行程序。

img

make install

安装crd到目标集群,这一步可能受github网络影响自动下载kustomize慢需要多试几次或隔天再试。
img

make docker-build

img
可以在刚生成的镜像列表中生成镜像。
img

make docker-push

1)先增加要上传镜像的tag

$ docker tag controller:latest docker.io/mospany/controller:latest

2)make docker-push

$ make docker-push IMG=docker.io/mospany/controller:latest

img

  1. 查看docker hub上传效果
    img

运行

先在mac上安装k8s集群,详见:Mac系统安装k8s集群

本地运行

要想在本地运行 controller,只需要执行下面的命令,你将看到 controller 启动和运行时输出:

make run

img

部署到k8s集群中运行

make deploy IMG=docker.io/mospany/controller:v1.0

img

查看日志

kubectl logs -n guestbook-system guestbook-controller-manager-7c67b5bd6c-gm5qs

img

创建CR

该创建自定义资源对象CR了,如原生中的rc/deployment等对象

# mosp @ mospdeMacBook-Pro in ~/work/pingan/arch/mysrc/guestbook [21:14:17]
$ kubectl get RedisCluster
NAME                  AGE
rediscluster-sample   48m

# mosp @ mospdeMacBook-Pro in ~/work/pingan/arch/mysrc/guestbook [21:22:49]
$ kubectl get RedisCluster -o yaml
apiVersion: v1
items:
- apiVersion: redis.xiaohongshu.org/v1
  kind: RedisCluster
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"redis.xiaohongshu.org/v1","kind":"RedisCluster","metadata":{"annotations":{},"name":"rediscluster-sample","namespace":"default"},"spec":null}
    creationTimestamp: "2022-07-29T12:34:11Z"
    generation: 1
    name: rediscluster-sample
    namespace: default
    resourceVersion: "200052"
    uid: 18eaf75f-9597-46af-bd88-abf7153c1377
  status: {}
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

# mosp @ mospdeMacBook-Pro in ~/work/pingan/arch/mysrc/guestbook [21:23:07]

开发业务逻辑

下面我们将修改 CRD 的数据结构并在 controller 中增加一些日志输出。

修改 CRD

我们将修改api/v1/rediscluster_types.go 文件的内容,在 CRD 中增加 FirstName、LastName 和 Status 字段。

// RedisClusterSpec defines the desired state of RedisCluster
type RedisClusterSpec 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 RedisCluster. Edit rediscluster_types.go to remove/update
    FirstName string `json:"firstname"`
    LastName  string `json:"lastname"`
}

// RedisClusterStatus defines the observed state of RedisCluster
type RedisClusterStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster

    // Important: Run "make" to regenerate code after modifying this file
    Status string `json:"Status"`
}

修改 Reconcile 函数

Reconcile 函数是 Operator 的核心逻辑,Operator 的业务逻辑都位于 controllers/rediscluster_controller.go 文件的 Reconcile 函数中

func (r *RedisClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    _ = log.FromContext(ctx)

    // TODO(user): your logic here

    // 获取当前的 CR,并打印
    logger := log.FromContext(ctx)
    obj := &redisv1.RedisCluster{}
    if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
        logger.Error(err, "Unable to fetch object")
        return ctrl.Result{}, nil
    } else {
        logger.Info("Greeting from Kubebuilder to", obj.Spec.FirstName, obj.Spec.LastName)
    }

    // 初始化 CR 的 Status 为 Running
    obj.Status.Status = "Running"
    if err := r.Status().Update(ctx, obj); err != nil {
        logger.Error(err, "unable to update status")
    }

    return ctrl.Result{}, nil
}

运行测试

  • 安装CRD(同上)

  • 部署controller(同上)

  • 创建CR

修改 config/samples/redis_v1_rediscluster.yaml 文件中的配置

apiVersion: redis.xiaohongshu.org/v1
kind: RedisCluster
metadata:
  name: rediscluster-sample
spec:
  # TODO(user): Add fields here
  firstname: Jimmy
  lastname: Song

执行下面命令,创建CR:

$ k8sdev apply -f  config/samples/redis_v1_rediscluster.yaml

查看controller里的运行日志:

img

参考资料

【01】使用shell命令行登陆Docker Hub出现的404Not found的问题
【02】Docker镜像推送Dockerhub
【03】使用 kubebuilder 创建并部署 k8s-operator
【04】Kustomize的基本使用
【05】【kubebuilder2.0】安装、源码分析
【06】深入解析 Kubebuilder:让编写 CRD 变得更简单
【07】一篇带给你KubeBuilder 简明教程
【08】深入解析Kubebuilder
【09】什么是RBAC
【10】Kubernetes Controller Manager 工作原理
【11】controller-runtime 之 manager 实现

微信扫一扫

作者:mospan
微信关注:墨斯潘園
本文出处:http://mospany.github.io/2022/12/14/operator-on-kubebuilder/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。