本片博客将介绍kubebuilder的使用以及相关原理 ,原始教程kubebuilder项目地址。但由于原教程kubebuilder版本为v4而教程部分为v3,故而本篇会做出修正并进行相关解释。相关组件版本如下

  • k8s版本:v1.33.1
  • kubebuilder版本:v4
  • go版本:v1.22.0
  • controller-runtime:v0.18.2

kubebuilder的简要说明:

  • 是什么:是针对controller-runtime的封装脚手架工具
  • 为什么:开发者使用controller-runtime开发operator过程繁琐
  • 怎么样:使用marker机制自动化生成相关资源代码,用户只需要再顶层填充代码逻辑即可

K8s Operator工作原理

CRD,CR 和 controller

  • CRD(Custom Resource Definition)为K8s 自定义资源定义,比起K8s内置的资源(如deployment,configmap),这种资源由开发者自己定义
  • CR(Custom Resource)为K8s 自定义资源,由CRD实例化而来,CR和CRD的关系类似于 opp中的类和实例的关系
  • controller:对CRD/CR 进行控制的程序/组件

informer,cache 和reconcile

controller对于CR的控制很大一部分是reconcile(调和),其本质为做出一系列动作是的CRD的Staus(状态)趋近于Spec(期待)

informer的本质是在 client-go 的基础上提供缓存(cache)+ 事件分发(event handler)
其主要由三部分组成

  • ListWatcher:当informer被创建的时候,伴随着fieldSelector过滤器访问apiserver将对应资源全部拉下来存到本地cache中,这部分为List。然后再和apiserver建立https长连接,当资源发生变化时apiserver将event推送给informer让其维护cache,这部分为Watch。而一个event大概是这样的
1
2
3
4
type Event struct {
Type EventType // ADDED, MODIFIED, DELETED, BOOKMARK, ERROR
Object runtime.Object // 对象的完整内容,例如 Pod, CronJob 等
}
  • ResourceEventHandler:当informer察觉到cache中资源发生变化的时候触发回调,然后创建一个key出来塞到workqueue中,有三种触发回调函数,对应三种触发时机
    • addFunc():当资源被创建的时候
    • updateFunc():当资源被更新的时候
    • deleteFunc():当资源被删除的时候
      当回调函数被触发时候,回调函数会根据event中对应的Object的ns/name 生成一个key塞到workqueue中
  • ResyncPeriod:当长时间没有资源变动时候,自动触发updateFunc(),类似于一个保活机制

informer和controller的交互

webhook 和ca-manager

kubebuilder使用教程

初始化kubebuilder

初始化kubebuilder的project就一行

1
kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/project

其中

  • domain:为你生成CRD,controller,webhook的域,当生成相关api时,其指定的group和这里的domain拼接就生成该资源的Group
  • repo:该go项目的repo,写在go.mod中表明这个项目的总package

这个项目初始会有如下的目录结构

1
2
3
4
5
6
7
8
9
my-operator/
├── PROJECT
├── Makefile
├── go.mod
├── go.sum
├── config/
├── controllers/
├── api/
└── bin/
  • PROJECT:项目的元信息文件,描述 Kubebuilder 插件版本、项目类型、domain 等。例如会包含 domain: my.domain 这样的配置。

  • Makefile:自动化命令集合。常用目标包括 make run(运行控制器)、make install(安装 CRD)、make docker-build(构建镜像)。

  • go.mod / go.sum:Go modules 的依赖管理文件。记录了项目依赖的库(比如 controller-runtime)。

  • config/:存放 Kubernetes 部署配置(YAML 文件)。包括 CRD 定义、RBAC 权限、Manager 部署清单等。一般在开发过程中会用 kustomize 组合这些文件。

  • controllers/:存放控制器(Controller)的实现代码。当用 kubebuilder create api 创建新的 API 时,会在这里生成对应的控制器逻辑文件。

  • api/:存放 API 类型定义(CRD 对应的 Go struct)。用于定义 Kubernetes 自定义资源(CustomResourceDefinition)。

  • bin/:存放项目需要用到的二进制文件,例如 controller-gen、kustomize。通常在 make 时自动下载。

然后需要开始创建API,如同教程中所说

1
kubebuilder create api --group batch --version v1 --kind CronJob

我们将会创建api,并指定其group,version,kind,也就是GVK。

执行 这条命令之后 之后,Kubebuilder 自动为我们完成了大量重复性工作:

  • PROJECT 文件:

    在 PROJECT 文件中新增资源与控制器的描述:

    1
    2
    3
    4
    5
    6
    resources:
    - group: batch
    version: v1
    kind: CronJob
    path: tutorial.kubebuilder.io/project/api/v1
    controller: true

    这表示我们要管理一个新的 GVK(Group-Version-Kind = batch/v1/CronJob),对应的 Go 类型定义在 api/v1,并且需要生成 controller。

  • API 类型代码:
    新建 api/v1 文件夹,其中包括:

    • groupversion_info.go:定义 API 的 Group/Version:
    1
    2
    3
    4
    var (
    GroupVersion = schema.GroupVersion{Group: "batch.tutorial.kubebuilder.io", Version: "v1"}
    SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
    )
    • cronjob_types.go:定义 CronJob 和 CronJobList 两个 struct,并带有 kubebuilder 标记(markers):
    1
    2
    3
    4
    5
    6
    7
    8
    // +kubebuilder:object:root=true
    // +kubebuilder:subresource:status
    type CronJob struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`
    Spec CronJobSpec `json:"spec,omitempty"`
    Status CronJobStatus `json:"status,omitempty"`
    }

    这些 struct 会被 controller-gen 转换成 CRD YAML。

  • Controller 模板:生成 controllers/cronjob_controller.go,内容包括:

    • CronJobReconciler 结构体(内嵌 client、scheme)

    • 空的 Reconcile 方法,留待我们实现逻辑

    • SetupWithManager 方法,将 controller 注册到 manager

  • 配置清单:生成 CRD YAML、Sample CR、RBAC 权限文件

    • config/crd/bases/batch.tutorial.kubebuilder.io_cronjobs.yaml:CRD 定义文件(由 controller-gen 自动生成)

    • config/samples/batch_v1_cronjob.yaml:示例 CronJob 资源,方便测试

    • config/rbac/role.yaml:新增了对 cronjobs 的 RBAC 权限,例如:

    1
    2
    3
    apiGroups: ["batch.tutorial.kubebuilder.io"]
    resources: ["cronjobs", "cronjobs/status", "cronjobs/finalizers"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  • main.go 修改:注册新的 Controller 到 Manager
    在 main.go 的 main() 函数中,注册了新的控制器:

    1
    2
    3
    4
    5
    6
    7
    if err = (&controllers.CronJobReconciler{
    Client: mgr.GetClient(),
    Scheme: mgr.GetScheme(),
    }).SetupWithManager(mgr); err != nil {
    setupLog.Error(err, "unable to create controller", "controller", "CronJob")
    os.Exit(1)
    }

    这样 manager 启动时就会自动加载并运行 CronJob 的控制循环。

填充CRD

再完成初始化的相关信息之后,下面要开始填充设计我们的CRD了,这部分再官方教程中已经有很详细的解释了,但想补充一些我认为的关键信息

  1. subresource的含义:

    在CronJob的结构体定义中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // +kubebuilder:object:root=true
    // +kubebuilder:subresource:status

    // CronJob is the Schema for the cronjobs API
    type CronJob struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec CronJobSpec `json:"spec,omitempty"`
    Status CronJobStatus `json:"status,omitempty"`
    }

    第二条maker表明status是一个subresource,其含义是Status是一个独立resource,subresource 会让 API Server 暴露独立 endpoint,并区分权限,用户不可修改,只能由controller自身修改。从设计角度来看,Status应该是集群资源的实时状态,是观测而来而不应该是用户定义,用户应该对Spec进行操作再通过reconcile来达到自己想要的状态

  2. GVK信息注册到scheme

    cronjob_types.go中的init()函数中SchemeBuilder.Register(&CronJob{}, &CronJobList{})将Cronjob和CronjobList的kind信息注册到这个SchemeBuilder中,这个SchemeBuilder在该api/v1下的groupversion_info.go中维护了其GV信息,这样GVK信息就齐了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var (
    // GroupVersion is group version used to register these objects
    GroupVersion = schema.GroupVersion{Group: "batch.tutorial.kubebuilder.io", Version: "v1"}

    // SchemeBuilder is used to add go types to the GroupVersionKind scheme
    SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

    // AddToScheme adds the types in this group-version to the given scheme.
    AddToScheme = SchemeBuilder.AddToScheme
    )

    而在cmd/main.go中的init()函数调用这个gv的AddToScheme将GVK的信息注册到Scheme中

    1
    2
    3
    4
    5
    6
    func init() {
    utilruntime.Must(clientgoscheme.AddToScheme(scheme))

    utilruntime.Must(batchv1.AddToScheme(scheme))
    // +kubebuilder:scaffold:scheme
    }

    当然,如果要说,scheme是什么,scheme就是manager所维护的一份crd yaml到crd go struct的一份dict(字典)

设计controller

同上,controller设计在官方教程中已经比较全面了,照抄就行

设计webhook

由于官方最近v4教程更新不全面,所以关于webhook可能还是v3版本
再创建webhook后

1
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation

其相关webhook代码并非在internal/webhook下,而是合并到了api/v1文件夹下。

同时,webhook的启动也并非通过手动实例化CronJobCustomValidatorCronJobCustomDefaulter来进行,因为在v4版本中,通过kubebuilder创建的webhook关联的crd都模式实现了Default(),ValidateCreate(),ValidateUpdate()和ValidateDelete()接口,我们不需要再额外创建,只需要维护其对应函数即可。在当前时间点[2025/9/26],其与官方教程对应的cronjob_webhook.go的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
var cronjoblog = logf.Log.WithName("cronjob-resource")

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob.kb.io,admissionReviewVersions=v1

var _ webhook.Defaulter = &CronJob{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *CronJob) Default() {
cronjoblog.Info("default", "name", r.Name)

// TODO(user): fill in your defaulting logic.
if r.Spec.ConcurrencyPolicy == "" {
r.Spec.ConcurrencyPolicy = ConcurrencyPolicy(batchv1.ForbidConcurrent)
}
if r.Spec.Suspend == nil {
r.Spec.Suspend = new(bool)
*r.Spec.Suspend = false
}
if r.Spec.SuccessfulJobsHistoryLimit == nil {
r.Spec.SuccessfulJobsHistoryLimit = new(int32)
*r.Spec.SuccessfulJobsHistoryLimit = 3
}
if r.Spec.FailedJobsHistoryLimit == nil {
r.Spec.FailedJobsHistoryLimit = new(int32)
*r.Spec.FailedJobsHistoryLimit = 1
}
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.
// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
// +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &CronJob{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *CronJob) ValidateCreate() (admission.Warnings, error) {
cronjoblog.Info("validate create", "name", r.Name)

// TODO(user): fill in your validation logic upon object creation.
return nil, validateCronJob(r)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *CronJob) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
cronjoblog.Info("validate update", "name", r.Name)

// TODO(user): fill in your validation logic upon object update.
return nil, validateCronJob(r)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *CronJob) ValidateDelete() (admission.Warnings, error) {
cronjoblog.Info("validate delete", "name", r.Name)

// TODO(user): fill in your validation logic upon object deletion.
return nil, nil
}

func validateCronJob(cronjob *CronJob) error {
var allErrs field.ErrorList

if err := validateCronJobName(cronjob); err != nil {
allErrs = append(allErrs, err)
}

if err := validateCronJobSpec(cronjob); err != nil {
allErrs = append(allErrs, err)
}

if len(allErrs) == 0 {
return nil
}

return apierrors.NewInvalid(
schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"},
cronjob.Name,
allErrs,
)
}

func validateCronJobSpec(cronjob *CronJob) *field.Error {
return validateScheduleFormat(
cronjob.Spec.Schedule,
field.NewPath("spec").Child("schedule"))
}

func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
if _, err := cron.ParseStandard(schedule); err != nil {
return field.Invalid(fldPath, schedule, err.Error())
}
return nil
}

func validateCronJobName(cronjob *CronJob) *field.Error {
if len(cronjob.Name) > validation.DNS1035LabelMaxLength-11 {
return field.Invalid(field.NewPath("metadata").Child("name"),
cronjob.Name, "must be no more than 52 characters")
}
return nil
}

部署operator

部署CRD & controller

在完成相关CRD,controller,webhook的搭建之后,需要对这些资源进行部署。查阅MakeFile文件我们可以看到会有以下的目标

  • make manifest:将我们写的CRD,controller,webhook都通过controller-runtime生成对应的yaml文件
  • make install:将CRD apply到k8s集群
  • make deploy:将CRD,controller,webhook都apply到k8s集群
  • make docker-build IMG=: 将controller打包成镜像

但这里会有一个问题,问题在于,kubebuilder v4 的controller-gen 在生成yaml时候会自动生成属性的ip和name两个字段,但k8s在新版本会对这个两个字段进行校验检查是否为required或者有default值,但自动生成的yaml没有,所以会导致apply出错。解决方法是在Makefile中添加一个make目标

1
2
3
4
5
6
7
8
9
.PHONY: patch-crd
patch-crd: ## Patch all CRD yaml to fix required defaults
@echo "Patching CRDs..."
@for crd in config/crd/bases/*.yaml; do \
echo " -> Patching $$crd"; \
yq eval '.spec.versions[].schema.openAPIV3Schema.properties.spec.properties.jobTemplate.properties.spec.properties.template.properties.spec.properties.hostAliases.items.properties.ip.default = ""' -i $$crd; \
yq eval '.spec.versions[].schema.openAPIV3Schema.properties.spec.properties.jobTemplate.properties.spec.properties.template.properties.spec.properties.imagePullSecrets.items.properties.name.default = ""' -i $$crd; \
done
@echo "Patch applied successfully."

在make install和make deploy中所依赖的manifest后添加此目标即可

1
2
3
4
5
6
7
8
.PHONY: install
install: manifests patch-crd kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
$(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f -

.PHONY: deploy
deploy: manifests patch-crd 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) apply -f -

部署cert-manager & webhook

关于cert-manger和webhook的原理请看这里:

挖坑:cert-manger原理以及使用
挖坑:webhook原理以及使用

我们首先需要安装cert-manager使其进行webhook 私钥和证书secert的制作

1
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.2/cert-manager.yaml

查看以下secret是否存在以及对应controller-pod是否运行起来

1
2
kubectl get secret -n xxx-system
kubect get pod -n xxx-system

然后按照教程中将config/default/kustomization.yamlconfig/crd/kustomization.yaml中prefix为[CERTMANAGER]和[WEBHOOK]的注释代码给解除注释即可

具体来说

  • 针对config/crd/kustomization.yaml中的注释解除是为了启用 config/patch中的yaml,包括启动webhook和ca 注入功能
  • 而针对config/default/kustmozation中的注释主要是替换cert-manager中的的相关属性字段