Contents

K8s Operator 开发 Part1:快速上手 Kubebuilder,构建你的第一个 K8s Operator

https://img.lixueduan.com/kubernetes/cover/k8s-operator-1-quick-start.png

本文主要分享 K8s Operator 相关的基本概念,以及使用 Kubebuilder 开发 K8s Operator 的大致流程。

Kubebuilder 系列内容比较多,拆分为三篇文章:

  • Kubebuilder 开发 Operator 的大致流程
  • Operator 开发过程中如何本地调试
  • Operator 开发最佳实践

1.基本概念

在开发之前,简单介绍一下 K8s Operator 相关的一些概念。

Operator Pattern

Kubernetes Operator 是一种软件扩展模式,用于基于自定义资源(Custom Resources)来管理应用程序及其组件。它遵循 Kubernetes 的核心原则,特别是控制循环(Control Loop)机制。

在 Kubernetes 中,用户通常希望通过自动化来处理重复性任务,而 Operator 正是为了在 Kubernetes 提供的基础能力之上,进一步扩展自动化管理的范围。

一句话描述:Operator 的工作核心就是通过 自定义资源(CRD)自定义控制器(Controller) 实现对应用的自动化管理。

CRD

CRD(Custom Resource Definition):是 Kubernetes 中扩展 API 的方式,它允许你定义一种新的资源类型,使其看起来就像是 Kubernetes 本身的原生资源(例如 Pods、Services 等)。每个 CRD 都是一个资源类型,它定义了你要管理的应用程序或服务的状态、字段以及控制器的行为。

CRD 对象本身是 Kubernetes 中的一种 API 对象,用于扩展 Kubernetes 的 API,使得你可以创建自定义的资源类型。

Controller

Controller: 控制器是 Kubernetes 中的核心概念之一,它负责不断地检查集群中的资源对象(如 Pod、Service、Deployment 等)是否符合预期状态。Kubernetes 内置的控制器包括 Deployment 控制器、ReplicaSet 控制器等。

k8s 的是一个高度自动化的系统,其中涵盖了常见应用程序所需的大部分功能,比如我们通过 kubectl 命令创建一个 deployment 对象之后,为什么就会有 Pod 创建出来了?

这其实是后台有多个 Controller 在后台工作实现的。

比如

  • Deployment Controller 根据 Deployment 创建 ReplicasSet 对象

  • ReplicasSet Controller 则根据 ReplicasSet 创建 Pod

K8s 资源标识 GVK&GVR

在 Kubernetes 中,GVK(Group, Version, Kind)和 GVR(Group, Version, Resource)是用于标识和访问 Kubernetes 资源的两个重要概念。

  • Group:API 组的名称。例如,apps 组包含 Deployment、StatefulSet 等资源,batch 组包含 Job、CronJob 等资源。

    • 注意;早期的 core 组的资源(如 Pod、Service 等)是没有组名的,默认使用空字符串""
  • Version:API 版本。每个资源在 Kubernetes 中可能会有多个版本(如 v1v1beta1v1alpha1 等),每个版本可能会有不同的功能和行为。

  • Kind:资源的类型。通常是资源的单数形式,如 PodDeploymentService

  • Resource:资源的名称。通常是复数形式,如 podsdeploymentsservices

GVK 和 GVR 区别在于:

  • GVR 更侧重于资源的实际操作,特别是动态客户端和工具中使用,用于指定和操作特定类型的资源实例。

  • GVK 更侧重于资源的定义和描述,特别是在控制器、操作器以及元数据操作中使用,用于唯一标识和处理特定类型的资源。

以下面这个 Deployment 为例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: caddy
  namespace: default
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  ...
  • Group 为 apps

  • Version 为 v1

  • Kind 则是 Deployment

  • Resource 则是 deployments

声明式 API

所谓声明式就是“告诉 K8s 你要什么,而不是告诉它怎么做的命令”。

举个🌰,我们希望调整到 26 度,声明式和命令式的区别:

  • 声明式:小爱同学,空调调到 26 度

  • 命令式:(看一眼,确认当前空调是 22 度)小爱同学,空调上调 4 度

在 K8s 里面,声明式的体现就是 kubectl apply 命令,在对象创建和后续更新中一直使用相同的 apply 命令,告诉 K8s 对象的终态即可。 声明式 API 让 K8s 的“容器编排”世界看起来温柔美好,而控制器(以及容器运行时,存储,网络模型等)才是这太平盛世的幕后英雄。

哪儿有什么岁月静好~

2.Operator 开发

社区开发了 kubebuilder 项目,让我们可以快速创建脚手架,极大简化了 Operator 开发过程。

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

  • 1)初始化项目

  • 2)创建 API,并填充字段

  • 3)实现 Controller,编写核心调谐逻辑(Reconcile)

  • 4)(可选)创建 Webhook,并实现接口

  • 5)本地调试,验证测试

  • 6) 构建镜像并生成 yaml,发布到集群

安装 Kubebuilder

Kubebuilder Realease 页 下载即可,这里是直接用得最新版本 v4.3.1。

version="v4.3.1"
curl -L -o kubebuilder "https://github.com/kubernetes-sigs/kubebuilder/releases/download/${version}/kubebuilder_$(go env GOOS)_$(go env GOARCH)"
chmod +x kubebuilder && mv kubebuilder /usr/local/bin/

安装成功之后使用 kubebuilder version 查看版本信息

❯ kubebuilder version
Version: main.version{KubeBuilderVersion:"4.3.1", KubernetesVendor:"1.31.0", GitCommit:"a9ee3909f7686902879bd666b92deec4718d92c9", BuildDate:"2024-11-09T12:30:22Z", GoOs:"darwin", GoArch:"arm64"}

项目初始化

源码见 -> lixd/i-operator

先创建一个空文件夹,然后在文件夹内执行下方命令

mkdir i-operator
cd i-operator

kubebuilder init --domain crd.lixueduan.com --repo github.com/lixd/i-operator
  • --domain crd.lixueduan.com 我们的项目的域名,就是后续 CRD Group 的 Domain

    • 比如后续创建一个名为 Application 的 CRD,加上 domain,完整名称就是 application.crd.lixueduan.com
  • --repo github.com/lixd/i-operator 是仓库地址,也就是 go module 名称

完整输出如下:

❯ sudo kubebuilder init --domain crd.lixueduan.com --repo github.com/lixd/i-operator
INFO Writing kustomize manifests for you to edit...
INFO Writing scaffold for you to edit...
INFO Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.19.1
INFO Update dependencies:
$ go mod tidy
Next: define a resource with:
$ kubebuilder create api

ps:遇到一个问题,mac 下执行命令未添加 sudo 时会初始化失败:

Error: failed to initialize project: unable to scaffold with "base.go.kubebuilder.io/v4": exit status 1

可以看到在最后打印出了一个提示信息

Next: define a resource with:
$ kubebuilder create api

告诉我们下一步就是创建 API,不过在这之前,我们先看下初始化生成的项目结构

❯ sudo tree i-operator
i-operator
├── Dockerfile
├── Makefile
├── PROJECT # Kubebuilder 元数据
├── README.md
├── cmd
│   └── main.go
├── config # 部署相关的配置文件
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_metrics_patch.yaml
│   │   └── metrics_service.yaml
│   ├── manager
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── network-policy
│   │   ├── allow-metrics-traffic.yaml
│   │   └── kustomization.yaml
│   ├── prometheus
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   └── rbac # rbac 授权相关配置
│       ├── kustomization.yaml
│       ├── leader_election_role.yaml
│       ├── leader_election_role_binding.yaml
│       ├── metrics_auth_role.yaml
│       ├── metrics_auth_role_binding.yaml
│       ├── metrics_reader_role.yaml
│       ├── role.yaml
│       ├── role_binding.yaml
│       └── service_account.yaml
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── test
    ├── e2e
    │   ├── e2e_suite_test.go
    │   └── e2e_test.go
    └── utils
        └── utils.go

11 directories, 29 files

创建 API 对象

(可选)开启 multigroup

默认为关闭,关闭状态下只能创建一个 group,创建第二个 group 时会报错:

unable to inject the resource to "base.go.kubebuilder.io/v3": multiple groups are not allowed by default

如果需要创建多个 Group 时使用以下命令开启 multigroup

kubebuilder edit --multigroup=true

Create API

创建 API 对象时就会用到前面提到的 GVK 了,这里我们创建一个名为 Application 的 CRD 对象,具体如下:

kubebuilder create api --group core --version v1 --kind Application --namespaced=true
  • --namespaced=true 指定 CRD 的 scope,默认是 namespaced,如果是集群范围的 CRD 在创建时这里需要设置为 false。

注意:创建过程中会询问是否创建资源(Create Resource [y/n]) 和 控制器(Create Controller [y/n]) 都输入 y 同意即可:

INFO Create Resource [y/n]
y
INFO Create Controller [y/n]
y

完整输出如下:

❯ sudo kubebuilder create api --group core --version v1 --kind Application
INFO Create Resource [y/n]
y
INFO Create Controller [y/n]
y
INFO Writing kustomize manifests for you to edit...
INFO Writing scaffold for you to edit...
INFO api/v1/application_types.go
...
INFO internal/controller/application_controller_test.go
INFO Update dependencies:
$ go mod tidy
INFO Running make:
$ make generate
mkdir -p /Users/lixueduan/17x/projects/i-operator/bin
Downloading sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4
go: downloading sigs.k8s.io/controller-tools v0.16.4
...
go: downloading golang.org/x/text v0.19.0
/Users/lixueduan/17x/projects/i-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests

同样在最后给出了提示

Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests

先实现 API,然后执行 make manifests 命令去生成相应的 manifest 文件,这样就可以 Apply 到集群里去了。

同样了,在这之前先看下项目目录结构变化

❯ tree i-operator
i-operator
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── api
│   └── v1
│       ├── application_types.go
│       ├── groupversion_info.go
│       └── zz_generated.deepcopy.go
├── bin
│   ├── controller-gen -> /Users/lixueduan/17x/projects/i-operator/bin/controller-gen-v0.16.4
│   └── controller-gen-v0.16.4
├── cmd
│   └── main.go
├── config
│   ├── crd
│   │   ├── kustomization.yaml
│   │   └── kustomizeconfig.yaml
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_metrics_patch.yaml
│   │   └── metrics_service.yaml
│   ├── manager
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── network-policy
│   │   ├── allow-metrics-traffic.yaml
│   │   └── kustomization.yaml
│   ├── prometheus
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac
│   │   ├── application_editor_role.yaml
│   │   ├── application_viewer_role.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── metrics_auth_role.yaml
│   │   ├── metrics_auth_role_binding.yaml
│   │   ├── metrics_reader_role.yaml
│   │   ├── role.yaml
│   │   ├── role_binding.yaml
│   │   └── service_account.yaml
│   └── samples
│       ├── core_v1_application.yaml
│       └── kustomization.yaml
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
├── internal
│   └── controller
│       ├── application_controller.go
│       ├── application_controller_test.go
│       └── suite_test.go
└── test
    ├── e2e
    │   ├── e2e_suite_test.go
    │   └── e2e_test.go
    └── utils
        └── utils.go

具体如下:

  • 新增了 /api/v1 目录

  • 新增 /bin 目录

  • config 目录下新增 /config/crd 和 /config/samples

  • 新增 /internal/controllers 目录

API 目录下就是我们的 CRD 对象, /internal/controllers 目录下则是 Controller 了,接下来我们要做的就是完善 CRD 并实现 Controller。

Operator 实现

本次 Demo 我们实现一个 Application 对象,创建对象后 Controller 会根据 App 中指定的 Image 启动对应的 Deployment 运行应用。

完善 CRD

api/v1/application_types.go 内容如下:

这就是我们创建的 Application 对象,默认只填充了一个 Foo 字段,现在我们需要增加其他字段。

// ApplicationSpec defines the desired state of Application.
type ApplicationSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    Image   string `json:"image,omitempty"`
    Enabled bool   `json:"enabled,omitempty"`
}

// ApplicationStatus defines the observed state of Application.
type ApplicationStatus struct {
    Ready bool `json:"ready,omitempty"`
}

Application 一共增加了 3 个字段:

  • spec.Image:应用对应的镜像,用于部署服务

  • spec.Enabled:标记当前应用是否需要部署

  • status.Ready 则是记录应用是否准备好

Controller 实现

Controller 则是 Watch Application 对象的变化,Demo 希望的逻辑是:

根据 Application 对象创建、删除、更新情况同步维护 Deployment:

  • Spec 部分:根据 Spec 中的信息做对应的操作

    • 比如 spec.Enabled 为 true 则需要部署应用,根据 spec.Image 创建 Deployment,如果 Deployment 已经存在就判断 Image 是否一致,一致则跳过,不一致则更新。

    • 如果 spec.Enabled 为 false 则不需要部署应用,如果 Deployment 存在则删除

  • Status 部分:更新 Item 的 Status 信息,便于用户知道当前的状态

    • 比如 Application Status 提供了一个 Ready 字段,当前逻辑比较简单,如果 Deployment readyReplicas >= 1,就设置 Ready 为 True,否则为 False。

Controller 逻辑在internal/controller/application_controller.go 中,我们只需要在 Reconcile 方法中实现自定义逻辑即可。

完整代码如下:

func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

	logger := log.FromContext(ctx)
	log := logger.WithValues("application", req.NamespacedName)

	log.Info("start reconcile")
	// query app
	var app v1.Application
	err := r.Get(ctx, req.NamespacedName, &app)
	if err != nil {
		log.Error(err, "unable to fetch application")
		// we'll ignore not-found errors, since they can't be fixed by an immediate
		// requeue (we'll need to wait for a new notification), and we can get them
		// on deleted requests.
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// examine DeletionTimestamp to determine if object is under deletion
	if app.ObjectMeta.DeletionTimestamp.IsZero() {
		// The object is not being deleted, so if it does not have our finalizer,
		// then lets add the finalizer and update the object. This is equivalent
		// to registering our finalizer.
		if !controllerutil.ContainsFinalizer(&app, AppFinalizer) {
			controllerutil.AddFinalizer(&app, AppFinalizer)
			if err = r.Update(ctx, &app); err != nil {
				log.Error(err, "unable to add finalizer to application")
				return ctrl.Result{}, err
			}
		}
	} else {
		// The object is being deleted
		if controllerutil.ContainsFinalizer(&app, AppFinalizer) {
			// our finalizer is present, so lets handle any external dependency
			if err = r.deleteExternalResources(&app); err != nil {
				log.Error(err, "unable to cleanup application")
				// if fail to delete the external dependency here, return with error
				// so that it can be retried.
				return ctrl.Result{}, err
			}

			// remove our finalizer from the list and update it.
			controllerutil.RemoveFinalizer(&app, AppFinalizer)
			if err = r.Update(ctx, &app); err != nil {
				return ctrl.Result{}, err
			}
		}

		// Stop reconciliation as the item is being deleted
		return ctrl.Result{}, nil
	}

	// Your reconcile logic
	log.Info("run reconcile logic")
	if err = r.syncApp(ctx, app); err != nil {
		log.Error(err, "unable to sync application")
		return ctrl.Result{}, err
	}
	// sync status
	var deploy appsv1.Deployment
	objKey := client.ObjectKey{Namespace: app.Namespace, Name: deploymentName(app.Name)}
	err = r.Get(ctx, objKey, &deploy)
	if err != nil {
		log.Error(err, "unable to fetch deployment", "deployment", objKey.String())
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}
	copyApp := app.DeepCopy()
	// now,if ready replicas is gt 1,set status to true
	copyApp.Status.Ready = deploy.Status.ReadyReplicas >= 1
	if !reflect.DeepEqual(app, copyApp) { // update when changed
		log.Info("app changed,update app status")
		if err = r.Client.Status().Update(ctx, copyApp); err != nil {
			log.Error(err, "unable to update application status")
			return ctrl.Result{}, err
		}
	}

	
	return ctrl.Result{}, nil
}

一般的 Reconcile 逻辑:

  • 1)使用 NamespacedName 查询 Item

    • 获取不到如果是 NotFound 则正常返回,说明 Item 可能被删除了

    • 如果是其他错误就返回 Error

  • 2)使用 DeletionTimestamp.IsZero 判断 Item 是否在删除中

    • 如果否则说明 Item 不在删除中,当前为正常状态,接着判断是否有添加指定 Finalizer,有则跳过,没有则加上。

    • 如果是则说明 Item 已经在删除中了,再次判断是否有指定 Finalizer,有则执行清理操作,清理完移除 Finalizer,如果没有则直接返回,说明清理工作也做完了,之后这个 Item 就会被删除

  • 3)接下来就是真正的调谐逻辑了,根据 App.Spec 信息维护 Deployment 对象,并根据 Deployment 状态更新 App.Status

Controller 部分就开发完成了,接下来可以根据需求决定是否创建 Webhook。

(可选)Webhook

K8s 中有两类 Webhook:

  • Validating Admission Webhook:在资源创建、更新或删除时进行 验证,确保资源满足一定的规则,拒绝不符合要求的请求。

  • Mutating Admission Webhook:在资源创建或更新时,可以修改资源对象的内容。

开发 Operator 时可以创建一个 ValidatingAdmissionWebhook 来对用户创建的 CRD 对象进行校验,遇到不符合要求的对象直接拒绝。

或者直接创建 MutatingAdmissionWebhook 自动调整或注入所需配置,确保资源始终符合要求,减少人工干预。

比如对于 Application 对象来说,可以使用 ValidatingAdmissionWebhook 校验用户指定的 spec.Image 格式是否正确。

Create Webhook

和创建 API 命令类似:命令如下:

# GVK 需要和创建 API 时保持一致
kubebuilder create webhook --group core --version v1 --kind Application --defaulting --programmatic-validation

输出如下:

❯ kubebuilder create webhook --group core --version v1 --kind Application --defaulting --programmatic-validation
INFO Writing kustomize manifests for you to edit... ilder create webhook --group core --version v1 --kind Application --defaulting --programmatic-validation
INFO Writing scaffold for you to edit...          
INFO internal/webhook/v1/application_webhook.go   
INFO internal/webhook/v1/application_webhook_test.go 
INFO internal/webhook/v1/webhook_suite_test.go    
INFO Update dependencies:
$ go mod tidy           
INFO Running make:
$ make generate                
/Users/lixueduan/17x/projects/i-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new Webhook and generate the manifests with:
$ make manifests

看下有什么变化:

  • Config 目录下增加了 Webhook 相关配置

  • internal/webhook 目录下增加了 Webhook 默认实现

Webhook 实现

接下来我们要做的就是实现 Webhook,internal/webhook/v1/application_webhook.go 中增加自定义逻辑,包括以下几个方法:

  • Default:用于给对象设置一些默认值,如果对象中某些字段没有被明确设置,可以通过这个 webhook 来设置它们的默认值,即对应 mutating webhook

  • ValidateCreate、ValidateUpdate、ValidateDelete:这些方法是用于验证的。它们分别在对象创建、更新和删除之前被调用,用于确保对象符合特定的验证规则,对应 validating webhook

以 ValidateCreate 为例,判断 spec.Image 格式是否正确:

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Application.
func (v *ApplicationCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    application, ok := obj.(*corev1.Application)
    if !ok {
       return nil, fmt.Errorf("expected a Application object but got %T", obj)
    }
    applicationlog.Info("Validation for Application upon creation", "name", application.GetName())

    if isValidImageName(application.Spec.Image) {
       return nil, fmt.Errorf("invalid image name: %s", application.Spec.Image)
    }

    return nil, nil
}

3.测试

连接远程集群

然后将 Kubeconfig 复制到本地写入 ~/.kube/config 文件,同时在本地安装 kubectl,验证下,本地可以正常使用 kubectl 命令,就像这样:

❯ kubectl get po -A
NAMESPACE          NAME                                       READY   STATUS    RESTARTS       AGE
calico-apiserver   calico-apiserver-6f86f48f4b-cw7nw          1/1     Running   2 (6d5h ago)   7d22h
calico-apiserver   calico-apiserver-6f86f48f4b-mww2r          1/1     Running   2 (6d5h ago)   7d22h
calico-system      calico-kube-controllers-5f8646f489-8lpms   1/1     Running   0              7d22h
calico-system      calico-node-295tr                          1/1     Running   0              7d22h
calico-system      calico-typha-759985f586-q9dwp              1/1     Running   0              7d22h
calico-system      csi-node-driver-bpmd5                      2/2     Running   0              7d22h
calico-system      tigera-operator-5f4668786-dj2th            1/1     Running   1 (6d5h ago)   7d22h
default            app-demo-86b66c84cd-4947h                  1/1     Running   0              5d23h
kube-system        coredns-5d78c9869d-krwzb                   1/1     Running   0              7d22h
kube-system        coredns-5d78c9869d-ppx2c                   1/1     Running   0              7d22h
kube-system        etcd-bench                                 1/1     Running   0              7d22h
kube-system        kc-kubectl-78c9594489-pd6gw                1/1     Running   0              7d22h
kube-system        kube-apiserver-bench                       1/1     Running   0              7d22h
kube-system        kube-controller-manager-bench              1/1     Running   2 (6d5h ago)   7d22h
kube-system        kube-proxy-4x99q                           1/1     Running   0              7d22h
kube-system        kube-scheduler-bench                       1/1     Running   2 (6d5h ago)   7d22h

对于没有 Webhook 的 Operator,现在就满足本地调试的条件了,但是如果有 Webhook 则还需要额外配置。

生成 Manifests 并安装到集群

执行 make manifests 命令,会根据我们定义的 CRD 生成对应的 yaml 文件,以及其他部署相关的 yaml 文件:

❯ make manifests
/Users/lixueduan/17x/projects/i-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

生成的 crd 中我们定义的 Spec 和 Status 部分如下:

      spec:
        description: ApplicationSpec defines the desired state of Application.
        properties:
          enabled:
            type: boolean
          image:
            type: string
        type: object
      status:
        description: ApplicationStatus defines the observed state of Application.
        properties:
          ready:
            type: boolean

随后执行 make install 命令即可将 CRD 部署到集群,这也就是为什么需要在本地准备好 Kubeconfig 以及 kubectl 工具。

❯ make install
/Users/lixueduan/17x/projects/i-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases09:58:14  
Downloading sigs.k8s.io/kustomize/kustomize/v5@v5.5.0
go: downloading sigs.k8s.io/kustomize/kustomize/v5 v5.5.0
go: downloading sigs.k8s.io/kustomize/api v0.18.0
go: downloading sigs.k8s.io/kustomize/cmd/config v0.15.0
go: downloading sigs.k8s.io/kustomize/kyaml v0.18.1
go: downloading k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00
/Users/lixueduan/17x/projects/i-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/applications.core.crd.lixueduan.com created

本地启动 Controller

执行 make run 命令即可在本地运行 Controller,这也就是为什么需要在本地准备好 kubeconfig 文件。

ps:如果之前创建了 Webhook 这里会无法正常启动,具体见下一篇本地调试~

❯ make run
/Users/lixueduan/17x/projects/i-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases10:00:30  
/Users/lixueduan/17x/projects/i-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./cmd/main.go
2024-12-19T10:03:35+08:00       INFO    setup   starting manager
2024-12-19T10:03:35+08:00       INFO    starting server {"name": "health probe", "addr": "[::]:8081"}
2024-12-19T10:03:35+08:00       INFO    Starting EventSource    {"controller": "application", "controllerGroup": "core.crd.lixueduan.com", "controllerKind": "Application", "source": "kind source: *v1.Application"}

这样本地运行可以比较方便的调试 Controller,当然了也可以直接以 Debug 方式启动,打断点进行调试。

4.部署

之前是本地运行,要部署到集群,则是先将 Controller 构建成镜像。

构建镜像

也很简单,Kubebuilder 在初始化时都准备好了,直接执行 make docker-buildx 命令就好。

会使用 Docker Buildx 构建多架构镜像,因此需要准备好 Buildx 环境。

IMG=lixd96/controller:latest PLATFORMS=linux/arm64,linux/amd64 make docker-buildx

生成部署 yaml

真正将 Controller 部署到集群时,一般使用 Deployment 形式部署。

运行make build-installer 即可生成 CRD 以及 部署 Controller 的 Deployment 对应的 Yaml。

IMG=lixd96/controller:latest make build-installer
/Users/lixueduan/17x/projects/i-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases14:48:30  
/Users/lixueduan/17x/projects/i-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
mkdir -p dist
cd config/manager && /Users/lixueduan/17x/projects/i-operator/bin/kustomize edit set image controller=lixd96/controller:latest
/Users/lixueduan/17x/projects/i-operator/bin/kustomize build config/default > dist/install.yaml

最终生成的 dist/install.yaml 就包含了部署 Operator 所需要的多有资源,部署是 apply 该文件即可。

至此,Operator 开发基本完成,逻辑都比较简单,旨在分享一下具体的开发流程~

5.小结

K8s Operator 开发分为以下步骤:

  • 1)使用 Kubebuilder 初始化项目

  • 2)创建 API 对象并填充字段

  • 3)实现 Controller

  • 4)(可选)创建 Webhook

  • 5)本地开发调试

  • 6)构建镜像并生成部署 manifest

本篇主要是分享使用 Kubebuilder 开发 K8s Operator 的大致流程,下一篇单独分享如何在本地进行调试。

源码见 -> lixd/i-operator

6.参考

Kubebuilder Quick Start

K8s Operator 模式