必须用 kubebuilder 而非手写 client-go:它自动封装 workqueue 限速、深拷贝、context 超时、错误分类,并强制使用 ctrl.Request/Result 抽象;CRD 中 storage、scope、plural 字段配置错误将导致控制器启动或 reconcile 失败;Reconcile 必须检查删除时间戳、正确设置 ownerReferences、调用 SetControllerReference;本地调试常见问题包括 deepcopy 未生成、RBAC 权限缺失、controller 未注册。

用 Go 开发 Kubernetes CRD 控制器,核心路径只有一条:基于 controller-runtime 构建,用 kubebuilder 脚手架生成骨架,再填充 Reconcile 逻辑。别碰裸写 client-go Informer 循环——90% 的人卡在事件漏处理、对象缓存不一致、错误重试失控上,不是逻辑写不对,是底层控制流没被封装住。
为什么必须用 kubebuilder 而不是手写 client-go
直接调 client-go 的 SharedIndexInformer 看似灵活,但实际要自己维护:
– 工作队列(workqueue.RateLimitingInterface)的限速与重入策略
– 对象深拷贝(DeepCopyObject())缺失会导致并发修改 panic
– Get/List 请求没带 context 超时,控制器会永久 hang 住
– 错误返回没区分 NotFound 和真实 error,导致无限 reconcile
kubebuilder 自动生成的 Reconciler 结构体已预置这些防护,且强制你用 ctrl.Request 和 ctrl.Result 抽象,避免手动拼 key 或错判重试时机。
CRD 定义里最容易翻车的三个字段
CRD YAML 中看似简单的字段,直接影响控制器能否正常启动或触发 reconcile:
立即学习“go语言免费学习笔记(深入)”;
-
spec.versions[*].storage: true必须且只能设在一个 version 上,否则kubectl apply -f crd.yaml成功但 API Server 不接受 CR 实例 -
spec.scope填Namespaced却在Reconcile里用client.Get(ctx, types.NamespacedName{Namespace: "", Name: "x"}, &obj)会静默失败——namespace 为空时 client 默认查集群级资源 -
spec.names.plural必须全小写且无下划线,比如redisclusters合法,RedisClusters或redis-clusters会导致kubectl get rc找不到资源
Reconcile 函数里必须检查的三件事
一个健壮的 Reconcile 不是“创建缺失资源”就完事,它得扛住反复调用、中间状态、跨 namespace 引用:
- 先用
r.Client.Get(ctx, req.NamespacedName, instance)拿到 CR 实例,立刻检查instance.DeletionTimestamp != nil—— 若非空,说明用户正在删这个 CR,该走清理逻辑(比如删掉关联的 StatefulSet),而不是重建 - 所有下游资源(如 Pod、Service)的
metadata.ownerReferences必须显式设置,用metav1.NewControllerRef(instance, schema.GroupVersionKind{Group: "cache.example.com", Version: "v1alpha1", Kind: "RedisCluster"}),否则垃圾回收机制不会自动清理 - 调用
r.Client.Create/Update前,务必用controllerutil.SetControllerReference(instance, obj, r.Scheme),否则ownerReferences字段会因 Scheme 注册顺序问题被忽略
本地调试时 controller-manager 启动失败的常见原因
运行 make run 报错,多数和 RBAC 或 Scheme 初始化有关:
- 错误信息含
no kind "RedisCluster" is registered for version "cache.example.com/v1alpha1":说明api/v1/zz_generated.deepcopy.go没生成,执行make generate再试 - 报
failed to list *v1alpha1.RedisCluster: redisclusters.cache.example.com is forbidden:config/rbac/role.yaml里rules缺少对应apiGroups: ["cache.example.com"]条目 - 日志停在
Starting EventSource "kind source: *v1alpha1.RedisCluster"不往下走:检查main.go中mgr.Add是否漏掉你的 controller,或builder.ControllerManagedBy(mgr)后没接.For(&v1alpha1.RedisCluster{})
真正难调试的永远不是逻辑分支,而是 controller-runtime 隐式依赖的 Scheme 注册顺序、OwnerReference 的 UID 绑定时机、以及 Informer cache 初始同步完成前的 Get 请求——这些点一旦出错,现象都是“reconcile 根本不触发”,而不是 panic 或报错。


















