controller-runtime 是开发 Kubernetes Controller 的主流轻量 Go SDK,封装 client-go 复杂性但保留事件与业务控制权;需注册 client、manager、reconcile 三组件,CRD 类型须提前注册 Scheme,监听资源需 For() 后调 Complete(),Reconcile 应幂等快速,避免阻塞操作与孤儿资源,本地调试需等待 cache 同步。

controller-runtime 是当前开发 Kubernetes Controller 最主流、最轻量的 Go SDK,它封装了 client-go 的底层复杂性(比如 Informer 启动、Workqueue 管理、Reconcile 重试逻辑),但保留了对资源事件和业务逻辑的完全控制权。用它写 Controller 不需要手写 Reflector、Indexer 或手动维护缓存,重点只在「怎么响应变化」。
如何初始化一个最小可用的 controller-runtime 项目
别从零建模块,直接用官方脚手架 operator-sdk init 或更轻量的 crd-gen + 手动 go mod init 即可。关键不是工具链,而是三个必须注册的组件:
-
sigs.k8s.io/controller-runtime/pkg/client:用于读写集群资源(Get/List/Create等) -
sigs.k8s.io/controller-runtime/pkg/manager:整个 Controller 生命周期的容器,含 Scheme、Cache、Client、Scheme 注册入口 -
sigs.k8s.io/controller-runtime/pkg/reconcile和Reconciler接口:你真正要写的业务逻辑入口,Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)
初始化时最容易漏的是 mgr.GetScheme().AddKnownTypes(...) —— 如果你操作的是自定义 CRD,必须提前把它的 Go 类型注册进 Scheme,否则 mgr.GetClient().Get() 会 panic 报 no kind "MyResource" is registered for version "mygroup/v1"。
如何监听 Pod 变化并触发 Reconcile
Controller 默认不监听任何资源,必须显式调用 ctrl.NewControllerManagedBy(mgr).For(&corev1.Pod{}).Complete(r)。这里有两个常见误区:
立即学习“go语言免费学习笔记(深入)”;
- 只调用
For()不够,必须接Complete()才真正启动 Informer;否则日志里看不到Starting workers,也收不到事件 -
For(&corev1.Pod{})监听的是所有命名空间的 Pod,如果只想看特定 namespace,得用Owns()+ OwnerReference,或在Reconcile中手动过滤req.Namespace - 若需关联多个资源(比如 Pod 变了要查对应 Deployment),用
Watches()注册额外 Watch,而不是在 Reconcile 里反复client.Get()—— 否则 cache 未热加载时可能查不到
示例片段:
评估 Kubernetes 集群安全态势,覆盖 RBAC、工作负载安全、网络策略、基础设施即代码(IaC)、运行时监控和密钥管理等 30 项控制项……
err := ctrl.NewControllerManagedBy(mgr).
For(&corev1.Pod{}).
Watches(
&corev1.Deployment{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
// 当 Deployment 更新时,找出所有引用它的 Pod 并入队
return getPodsForDeployment(ctx, mgr.GetClient(), o)
}),
).Complete(&PodReconciler{Client: mgr.GetClient()})
Reconcile 函数里该做什么、不该做什么
Reconcile 是核心,但它不是“每秒执行一次”的轮询函数,而是对每个事件(Add/Update/Delete)触发一次。它的设计原则是幂等 + 快速返回。以下行为会直接导致 Controller 卡死或无限重试:
- 在
Reconcile里调用阻塞式 HTTP 请求(如调外部 API)—— 应改用带超时的http.Client,或移到 Goroutine 中异步处理并用条件更新状态 - 不做对象存在性检查就直接
client.Update()—— 若对象已被删除,会返回404 NotFound,触发默认重试(指数退避),可能堆积大量失败请求 - 修改对象后没更新
resourceVersion就再次Update()—— 会报409 Conflict,同样进重试队列 - 在 Reconcile 中创建新资源却不设 OwnerReference —— 这些资源将变成孤儿,无法被 GC 清理
推荐模式:先 Get 当前对象 → 检查是否需变更 → 计算期望状态 → 调用 Patch 或 Update(用 client.Patch() + client.MergeFrom() 更安全)→ 返回 ctrl.Result{RequeueAfter: time.Minute} 控制节奏。
本地调试时为什么 List/Get 总是空或 timeout
最常见原因是 Cache 未同步完成就执行查询。controller-runtime 的 mgr.GetClient() 默认走 cache,而 cache 启动是异步的。直接在 main() 启动后立刻 client.List(),大概率返回空或 panic。
- 正确做法:在
Reconcile中访问 client,此时 cache 已 ready;或在mgr.Start()前加等待逻辑:if err := mgr.GetCache().WaitForCacheSync(ctx); err != nil { ... } - 另一个坑是 RBAC 权限没配全:比如只给了
get和list,但代码里用了watch,就会卡在 Informer 启动阶段,日志显示failed to list *v1.Pod: pods is forbidden - Minikube 或 Kind 集群中,metrics-server 未装会导致
client.MetricsClient初始化失败,但这个不影响主流程 —— 只有你显式用到 metrics 才需关心
调试时建议加一行日志:log.Info("cache synced", "ready", mgr.GetCache().CacheHasSynced()),确认时机。
真正难的从来不是写完第一个 Reconcile,而是让每一次 Update 都能准确识别「当前状态 vs 期望状态」的差异,并且不因并发、竞态或缓存延迟引入不一致。这需要你在类型定义里预留足够状态字段,在 Reconcile 开头做防御性检查,在 Patch 前比对 resourceVersion,以及——永远假设网络和 etcd 都可能临时不可用。

















