


















前面我们花了大量篇幅讲 client-go:Informer、WorkQueue、Leader 选举、Indexer……它们是 k8s 客户端编程的"原子"。而当我们真正写一个生产级 Operator 时,会发现直接用 client-go 拼装这些"原子"非常繁琐——要自己写 NewController、NewSharedInformerFactory、NewQueue、串联事件回调……代码量动辄几百行,且每一处实现都暗藏"忘了做缓存同步"、"忘了优雅退出"之类的坑。
controller-runtime 就是为了解决这个痛点而诞生的。它是 k8s 官方 SIG API Machinery 维护的 Operator 开发框架,把 client-go 的"原子"封装成 4 个高级抽象:Manager、Controller、Client、Reconcile。我们只要写一个 Reconcile 函数(20-50 行),就能跑起一个生产级 Operator。这篇文章就是要把这个"魔法"拆开看,让你不仅会用、还能在出问题时有底气去 debug 框架本身。
读完后你将能够:① 画出 controller-runtime 的内部组件图;② 理解 Manager 启动时都做了哪些事;③ 看懂 Controller.Watch 的源码实现;④ 解释 client-go 的"ListWatcher"在 controller-runtime 中如何被转成事件源;⑤ 在框架不满足需求时,知道去哪里扩展。
Kubernetes 1.36.1 controller-runtime Kubebuilder Operator 框架
🔓 学习重点提示 — 建议先通读全文,再重点回顾标注内容
★ 重点掌握(必须)
• Manager 内部结构:cluster、runnables、caches、client
• Controller.Watch 链路:source → eventhandler → queue → reconcile
• client.Client vs RESTMapper:为什么 Get 会自动选 GV
• predicate 过滤机制:Generic、LabelChangedPredicate 的内部实现
☆ 次重点(了解即可)
• Reconciler 注入:Manager.GetClient/SetFields 怎么用
• Admission Webhook 框架:manager.Webhook.Server 怎么启
• 测试用 envtest:envtest.Environment 与 manager 的差异
k8s Operator 开发有几个层次。最底层是 client-go:直接调 Informer、WorkQueue、Lister。中层是 controller-runtime:把这些"原子"封装成 Reconcile 模式。最上层是 Operator SDK(旧称 ansible-operator、helm-operator、go-operator 三个分支):基于 controller-runtime 加上 Ansible/Helm 写业务逻辑的脚手架。
┌────────────────────────────────────────────────────────┐
│ Operator SDK (Ansible Operator / Helm Operator) │
│ ▲ │
│ │ 基于 │
│ controller-runtime ← 我们要拆的就是这一层 │
│ ▲ │
│ │ 封装 │
│ client-go (Informer, WorkQueue, Lister, Clientset) │
│ ▲ │
│ │ HTTP / TLS │
│ kube-apiserver │
└────────────────────────────────────────────────────────┘
controller-runtime 本身是一个独立的 Go module(sigs.k8s.io/controller-runtime),由 k8s SIG API Machinery 团队维护。它对外暴露 4 个核心包:manager、controller、client、reconcile,加上一系列辅助包(builder、predicate、handler、source、webhook 等)。Kubebuilder 是它的"代码生成器搭档"——你写 CRD 的 Go struct,它帮你生成 controller-runtime 项目骨架、Makefile、Dockerfile、部署 YAML。
一个常见误解
"controller-runtime = client-go 的新版本"——错。它依赖 client-go,是 client-go 之上的封装。k8s 1.36 同时维护着两条线:client-go(核心包,直接和 apiserver 打交道)和 controller-runtime(Operator 框架)。它们的版本号独立。
Manager 是 controller-runtime 的"中央协调器"。一个 Operator 进程只创建一个 Manager,所有 Controller、Client、Cache、Webhook Server 都从 Manager 拿。Manager 内部有 5 个关键组件:
┌────────────────────────────────────────────┐
│ manager (Manager) │
│ ┌──────────────────────────────────────┐ │
│ │ cluster ──> 持有 kubeconfig + rest.Config │
│ │ ├── client.Client (读写 apiserver) │ │
│ │ ├── cache.Cache (本地缓存) │ │
│ │ ├── RESTMapper (GVK ↔ GVR 映射) │ │
│ │ └── recorder.EventRecorder │ │
│ ├──────────────────────────────────────┤ │
│ │ runnables ──> 一组需要"起停"的组件 │ │
│ │ ├── Controller (若干个) │ │
│ │ ├── Webhook.Server │ │
│ │ ├── MetricsServer │ │
│ │ ├── HealthProbe │ │
│ │ └── leaderelection (可选) │ │
│ ├──────────────────────────────────────┤ │
│ │ startCache ──> 启动所有 cache.Cache │ │
│ │ startHealthz ──> 启动健康检查端点 │ │
│ │ metricsServer ──> 启动 Prometheus 端点 │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘
cluster 是 Manager 的"对外接口",它把 4 个对象(Client、Cache、RESTMapper、EventRecorder)打包暴露。Controller 通过 manager.GetClient() 拿 Client,manager.GetCache() 拿 Cache。这种"聚合根"模式让 Controller 无需关心 Client/Cache 是怎么初始化的。
// sigs.k8s.io/controller-runtime/pkg/manager/manager.go(节选)
type cluster struct {
config *rest.Config
httpClient *http.Client
cache cache.Cache
client client.Client
mapper meta.RESTMapper
recorder record.EventRecorder
fieldIndexer client.FieldIndexer
}
func (c *cluster) GetClient() client.Client { return c.client }
func (c *cluster) GetCache() cache.Cache { return c.cache }
func (c *cluster) GetRESTMapper() meta.RESTMapper { return c.mapper }
func (c *cluster) GetEventRecorderFor(name string) record.EventRecorder {
return c.recorder.EventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: name})
}
注意 GetEventRecorderFor(name):每个 Controller 拿到的 EventRecorder 会带自己的 Component 名(通常是 CR 的 GVK),这样 Kubernetes Event 不会混乱。生产中"为什么我的事件没有发出来" 80% 原因是没有调用 Recorder.Eventf(obj, corev1.EventTypeWarning, "ReconcileFailed", "%v", err),剩下 20% 是没启 EventBroadcaster。
Manager 不会让 Controller 直接 goroutine.Run,而是让每个 Controller 把自己注册成 runnable。Manager 启动时统一并发拉起所有 runnable。优雅退出时,按注册顺序的逆序关闭。
// sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go(节选)
type Runnable interface {
Start(ctx context.Context) error
// NeedLeaderElection 返回 true 表示该 runnable 只在 Leader 上运行
NeedLeaderElection() bool
}
// Manager 内部用一个 sync.Map 存所有 runnable
type runnables struct {
mu sync.Mutex
startedMu sync.Mutex
started bool
ctx context.Context
fns []Runnable
}
一个 NeedLeaderElection() 返回值是框架里非常关键的设计:Metrics Server、Health Probe 这类"辅助服务"通常返回 false(每个副本都该跑),而业务 Controller 通常返回 true(只在 Leader 上跑)。这避免了每次升级都要为 Metrics 多写一个 RBAC 角色。
cache.Cache 是 controller-runtime 封装 client-go cache 的接口。它对外暴露 Get、List、Watch 三个方法,但默认从本地缓存读——不会真发 HTTP 请求到 apiserver。这是 controller-runtime 比裸 client-go 慢热、但稳的关键:启动时 cache.WaitForCacheSync 等所有缓存同步完成,Reconcile 拿到的对象一定是最新的本地状态。
💡 注意
Controller 里的 client.Client 默认走 cache,不是 直连 apiserver。如果 Reconcile 里要拿"绝对最新"的对象(比如判断 ResourceVersion),要用 client.Reader 接口或显式 cli.Get(ctx, key, obj, &client.GetOptions{Raw: &rawOpt}) 强制读 apiserver。生产中常见的"Status 没更新"问题 90% 来自这个误解。
client-go 原生调用需要先 cli.AppsV1().Deployments(ns).Get(name)——这种"链式"调用是类型安全的,但写起来冗长。controller-runtime 的 cli.Get(ctx, key, &deploy) 一行就搞定,背后靠 RESTMapper 在运行时查"Deployment 这个 GVK 对应哪个 REST 路径"。
// sigs.k8s.io/controller-runtime/pkg/client/typed_client.go(节选)
func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object) error {
gvk, err := apiutil.GVKForObject(obj, c.scheme)
if err != nil { return err }
// 把 GVK 转成 REST 路径
mapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil { return err }
// 发 HTTP 请求
return c.restClient.Get().AbsPath(mapping.Resource.Resource).
NamespaceIfScoped(key.Namespace, mapping.Scope.Name() == "namespace").
Name(key.Name).Do(ctx).Into(obj)
}
这段代码浓缩了 controller-runtime 的核心魔法:① apiutil.GVKForObject 从 obj 的类型反射出 GVK;② mapper.RESTMapping 查 Discovery API 拿到的全局映射表;③ 用 restClient 发 HTTP 请求。三个步骤让用户写 cli.Get(...) 即可,不用关心 Group、Version、Resource 三个维度的差异。
Watch 是 controller-runtime 最核心的 API。它的语义是:"我关心资源 X 的变化,当 X 变化时,把 X 关联的 key 入队,让 Reconcile 跑"。在底层,它把 client-go 的 Informer 回调和 WorkQueue 串联了起来。
// sigs.k8s.io/controller-runtime/pkg/builder/controller.go
func (blder *ControllerBuilder) Watch(src source.Source, eventhandler handler.EventHandler, predicates ...predicate.Predicate) *Builder {
blder.watchRequest = append(blder.watchRequest, watchRequest{
src: src,
eventhandler: eventhandler,
predicates: predicates,
})
return blder
}
// 1) src source.Source ← 从哪里来(Kind/Channel/Provider/Inform...)
// 2) eventhandler ← 入队什么(EnqueueRequestForObject / MapFunc)
// 3) predicates []predicate.P ← 哪些事件被丢弃
// 4) For(&customResource{}) ← 快捷注册"我关心这个 GVK" + default handler
Watch 的 4 个钩子形成完整的事件管道:Source 产生原始事件 → Predicates 过滤 → EventHandler 转成 reconcile.Request → 推入 WorkQueue。其中每一个钩子都可替换,框架默认的 For(&CR{}) 帮你把最常见的组合(Kind source + EnqueueRequestForObject handler)一步到位。
// sigs.k8s.io/controller-runtime/pkg/source/source.go
// Source 4 种实现
type Source interface {
Start(ctx context.Context, handler EventHandler, queue workqueue.RateLimitingInterface, predicates ...predicate.Predicate) error
}
// Kind ← 监听某个 GVK(最常用)
// Channel ← 监听 channel(自产事件)
// Informer ← 监听外部 Informer(对接老代码)
// Func ← 函数式 source(高级用法)
Kind source 内部会调用 cache.GetInformerForKind(ctx, gvk),在 cache 上注册一个 Informer。这就是为什么 Watch 自带缓存——它复用了 Manager 启动时建好的 cache.Cache。不要在 Reconcile 里直接 new 一个 Informer,会和 Manager 的缓存脱钩。
EnqueueRequestForObject 是最常见的 handler:直接把事件对象的 namespace/name 入队。进阶版有:
// pkg/controller/application_controller.go
func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
// 1) 关注 Application 自身的变化
For(&appv1.Application{}).
// 2) 关注子 Deployment 的变化(通过 OwnerReferences 自动 Enqueue Owner)
Owns(&appsv1.Deployment{}).
// 3) 关注子 Service 的变化
Owns(&corev1.Service{}).
// 4) 加上 GenericPredicate:忽略 DeletionTimestamp 已经设置的事件
WithEventFilter(predicate.GenerationChangedPredicate{}).
// 5) 只关心 dev 标签的对象
WithEventFilter(predicate.NewPredicateFuncs(func(obj client.Object) bool {
return obj.GetLabels()["env"] == "dev"
})).
Complete(r)
}
上面这段代码是 controller-runtime "全功能 Watch"的典型写法。链式 API 把"关注什么资源 + 关注哪些事件 + 如何入队"讲得明明白白。生成 Operator 时,Kubebuilder 也会生成类似骨架。
client.Client 是 controller-runtime 给业务代码"打交道"最多的接口。它有 6 个方法:Get、List、Create、Update、Patch、Delete,加 1 个 Status().Update()。看起来简单,背后却藏着 4 层实现。
// sigs.k8s.io/controller-runtime/pkg/client/client.go
// controller-runtime 默认构造的 client.Client 实际上是 4 层包装: // // 1) 业务接口 client.Client(6 方法) // 2) 默认实现 *typedClient(GVK → GVR → HTTP) // 3) Decorator 链:client.NewClient(...) 会按顺序装饰 // a) 序列化层:把 Go struct → JSON / Protobuf // b) Patch 转换层:client.MergeFrom / StrategicMerge / Apply // c) Status 子资源层:Status() 返回的 wrapper 走 /status 路径 // 4) 最底层:rest.Interface(client-go 的 restClient)
这 4 层装饰器让 controller-runtime 支持了非常多的"魔法":例如 Status().Update() 是怎么自动走 PUT /status 路径的?是因为 Status 装饰器在 Update 时会把 path 改成 /status。Patch 的差异计算、FieldOwner、Force 等都是装饰器层加的。
| client 角色 | 用途 | 来源 |
|---|---|---|
| client.Client | Reconcile 主用:Get/List/Create/Update/Patch/Delete | mgr.GetClient() |
| client.Reader | 只读子集:Get/List | cli (隐式实现) |
| client.Writer | 只写子集:Create/Update/Patch/Delete | cli (隐式实现) |
| client.StatusClient | Update/Patch Status | cli.Status() |
把 client 按"读 / 写 / 状态"切成多个子接口,是 Go 语言的接口隔离原则在 controller-runtime 中的应用。当一个函数只读不写时,参数类型用 client.Reader 比 client.Client 更准确,函数实现也更轻。
Reconciler 是 controller-runtime 给业务代码的核心接口——整个 Operator 的业务逻辑都在这里:
// sigs.k8s.io/controller-runtime/pkg/reconcile/reconcile.go
type Request struct {
NamespacedName types.NamespacedName
}
type Result struct {
Requeue bool
RequeueAfter time.Duration
}
type Reconciler interface {
Reconcile(ctx context.Context, req Request) (Result, error)
}
Request 只含 namespace + name——这就是为什么 Reconcile 必须自己 Get 对象(lister 的延迟决定了不能用)。Result 决定是否重新入队:Requeue: true 立即入队,RequeueAfter: 30*time.Second 30 秒后入队。返回 error 时由 workqueue 限速器决定退避重试。
// pkg/controller/application_controller.go
type ApplicationReconciler struct {
client.Client
Scheme *runtime.Scheme
Log logr.Logger
Recorder record.EventRecorder
}
func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("application", req.NamespacedName)
log.Info("开始 Reconcile")
app := &appv1.Application{}
if err := r.Get(ctx, req.NamespacedName, app); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// ... 业务逻辑
return ctrl.Result{}, nil
}
// SetupWithManager 由 controller-runtime 在启动时调用
func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appv1.Application{}).
Complete(r)
}
注意 client.Client 是内嵌字段——这样 Reconciler 直接拥有 Get/List/Create/Update/Patch 方法,不用写 r.Client.Get(...)。这是 Go 的"组合优于继承"在 controller-runtime 中的体现。
ManagerOptions 注入 vs 显式 SetFields
controller-runtime 提供两种 Reconciler 注入方式:① 把字段在 main.go 中显式赋值(r := &Reconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()});② 在 main.go 调用 setupLog, _ := mgr.SetFields(r) 让 Manager 自动注入。后者更优雅但需要 Reconciler 的字段有正确的 tag(// +kubebuilder:rbac)。
Predicate 是 Watch 链路的"过滤器"。controller-runtime 内置 6 种:
| Predicate | 行为 | 典型场景 |
|---|---|---|
| GenerationChangedPredicate | 只在 .metadata.generation 变化时触发 | 忽略 Status 更新 |
| LabelChangedPredicate | 只在 labels 变化时触发 | 按标签路由 |
| ResourceVersionChangedPredicate | 只在 ResourceVersion 变化时触发 | 全事件监听 |
| NewPredicateFuncs | 自定义 Create/Update/Delete/Generic 4 钩子 | 业务自定义 |
| And / Or / Not | 逻辑组合 | 复杂条件 |
// 只关心 Spec.Image 变化,忽略其他字段更新
type ImageChangedPredicate struct {
predicate.Funcs // 嵌入空实现(默认所有事件都放行)
}
func (p ImageChangedPredicate) Update(e event.UpdateEvent) bool {
oldApp, ok1 := e.ObjectOld.(*appv1.Application)
newApp, ok2 := e.ObjectNew.(*appv1.Application)
if !ok1 || !ok2 {
return true // 类型不匹配,放行
}
return oldApp.Spec.Image != newApp.Spec.Image // 只在 Image 变化时触发
}
这是 production-grade Operator 常用的优化:避免 Spec 中无关字段(replicas、env、resource)的更新触发 Reconcile。在 1000+ CR 规模下,正确的 Predicate 配置可以把 Reconcile 频率降低 10-50 倍。
// 让 Application 关心其下所有子 Deployment 的状态变化
return ctrl.NewControllerManagedBy(mgr).
For(&appv1.Application{}).
// 不用 Owns(它假设 OwnerReferences 已经设置好),改用 Watches 自定义
Watches(
&source.Kind{Cache: mgr.GetCache(), Type: &appsv1.Deployment{}},
handler.EnqueueRequestsFromMapFunc(func(a client.Object) []reconcile.Request {
// 遍历 OwnerReferences,找到 Application 类型的 Owner 入队
for _, ref := range a.GetOwnerReferences() {
if ref.Kind == "Application" && ref.APIVersion == appv1.GroupVersion.String() {
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Namespace: a.GetNamespace(),
Name: ref.Name,
},
}}
}
}
return nil
}),
).
Complete(r)
Watches 是 Owns 的超集。Owns 内部其实就是 Watches + 内置的 OwnerReference 映射函数。当内置逻辑不满足时(比如你想根据 Pod 的 status.phase 决定是否触发 Application Reconcile),就用 Watches 自定义。
理解了 4 个组件后,我们把它们"串"起来看 Manager 启动时做了什么。这是一段精简的伪代码(基于 controller-runtime v0.19+):
// sigs.k8s.io/controller-runtime/pkg/manager/manager.go(Start 入口)
func (cm *controllerManager) Start(ctx context.Context) error {
// 1) 启动 Cache(启动所有 Informer)
cm.cache.Start(ctx)
// 2) 等缓存同步完成(最长默认 60s)
if err := cm.cache.WaitForCacheSync(ctx); err != nil {
return fmt.Errorf("cache sync failed: %w", err)
}
// 3) 启动所有非 Leader-only 的 runnable(metrics、healthz)
go cm.startNonLeaderElectionRunnables(ctx)
// 4) 启动 Leader 选举
if cm.leaderElection {
// 4.1 周期性 renew lease
// 4.2 拿到 lease 后调 startLeaderElectionRunnables
// 4.3 失去 lease 时调 stopRunnables
} else {
// 单副本模式:直接起所有 runnable
cm.startLeaderElectionRunnables(ctx)
}
// 5) 阻塞到 ctx 取消
<-ctx.Done()
return nil
}
启动流程的 5 个步骤:
🌟 实用技巧
启动慢多半卡在 WaitForCacheSync。调优方法:① 缩小 Watch 范围——只 Watch 自己关心的 namespace;② 开启 --feature-gates=APIPriorityAndFairness=true;③ 用 MetricsBindAddress 暴露 controller_runtime_cache_sync_total 看每个 GV 的同步耗时。
▼ Q1: controller-runtime 和 Kubebuilder 是什么关系?
A: Kubebuilder 是脚手架工具(代码生成器 + 项目模板),controller-runtime 是运行时框架。你用 kubebuilder init 创建项目,kubebuilder create api 生成 CRD 和 Controller 模板,本质上都基于 controller-runtime 库。Operator SDK 是更高一层的封装。
▼ Q2: Reconciler 函数里的 ctx 是从哪里来的?
A: 来自 Manager.Start(ctx) 的 ctx 链。当 SIGTERM 触发时,Manager 取消 ctx,Controller 的 worker 协程中所有 <-ctx.Done() 收到信号,正在跑的 Reconcile 也会收到。可以放心地把 ctx 透传给 cli.Get(ctx, ...)。
▼ Q3: 怎么让 Reconcile 的并发度提高?
A: 在 builder 里加 WithOptions(controller.Options{MaxConcurrentReconciles: 4})。默认是 1。生产经验:4 核 8G Pod 设 2-4,太高会触发 APIServer 429。
▼ Q4: Reconcile 中 Get 一个 List,ListWatcher 是从哪里来的?
A: controller-runtime 内部根据 GVK 动态构造 ListWatcher。它先调 client.Get() 拿单个对象,然后从对象的 GVK 推导出 ResourceList 类型,再构造 ListWatch 闭包。这个机制让用户写 cli.List(ctx, &podList) 时不用关心 Pod 是 core/v1 还是其他版本。
▼ Q5: 为什么我的 Owns 没生效?
A: Owns 要求子资源的 OwnerReferences 字段正确设置了 Application 引用。常见错误:① 创建子资源时没设 OwnerReferences;② 用了 controllerutil.SetOwnerReference(app, child, r.Scheme) 但没传 Scheme;③ OwnerReference 写错了 APIVersion 字符串(要 appv1.GroupVersion.String(),不能写 "v1")。
▼ Q6: Manager 启动时报 "no GVK found for type",怎么处理?
A: Scheme 没注册。检查 scheme.AddToScheme(s) 是否调用,或者你的 CRD 是否用了 appsv1.AddToScheme 之类的方式注册。对 Kubebuilder 生成项目,要确认 init 阶段在 zz_generated.deepcopy.go 和 groupversion_info.go 中的 SchemeBuilder 都已注册。
▼ Q7: Reconcile 拿不到 EventRecorder 怎么注入?
A: 方式一:r.Recorder = mgr.GetEventRecorderFor("application-controller"),方式二:在 main.go 用 mgr.SetFields(r) 自动注入(前提是 Reconciler 有 Recorder record.EventRecorder 字段)。
▼ Q8: 怎么在 unit test 里 mock client.Client?
A: 用 sigs.k8s.io/controller-runtime/pkg/client/fake:fake.NewClientBuilder().WithObjects(...).Build() 返回一个 mock client.Client,对 Get/List/Create 都有 stub 实现。生产代码测试时通常还会加 .WithStatusSubresource(...) 模拟 status 子资源行为。
▼ Q9: controller-runtime 自身有版本兼容性问题吗?
A: 有,且比较多。controller-runtime 大版本(v0.7 → v0.19)API 有不兼容改动。生产经验:锁版本,不要跨大版本升级。k8s 1.36 推荐 controller-runtime v0.19+。升级前一定先在 staging 环境跑完整集成测试。
▼ Q10: cache.Cache 的 watch 是怎么知道哪个 GVK 启 Informer 的?
A: 是"按需启动"模式。Controller 调 source.Kind 时会去 cache 里查这个 GVK 的 Informer,cache 发现还没有就建一个并启动。所以 先调 SetupWithManager,再 Start Manager 的顺序很关键。
▼ Q11: 如何调试 controller-runtime 自身的 bug?
A: 三个层次:① 看 controller-runtime 源码(pkg/manager、pkg/controller、pkg/source);② 加 klog --v=10 看框架的内部日志;③ 用 delve 调试 Go 进程,break 在 manager.Start 入口跟 5 个步骤走一遍。
▼ Q12: 多个 Controller 能不能共享同一个 Manager?
A: 能。Kubebuilder 默认一个项目一个二进制,但里面可以建多个 Reconciler,ctrl.NewControllerManagedBy(mgr).For(...).Complete(reconciler1) 和 reconciler2 都用同一个 mgr。共享 cache、共享 client、共享 metrics——这是 controller-runtime 的最大价值之一。
▼ Q13: kubebuilder 中 RBAC 注释怎么自动生成?
A: 在 main.go 加 // +kubebuilder:rbac 注释(Kubebuilder v2 用 +kubebuilder:rbac,v3 用 +kubebuilder),运行 make manifests 即可。生成的 RBAC 写到 config/rbac/role.yaml。
▼ Q14: 怎么让 Operator 优雅处理 Webhook 启动失败?
A: Webhook Server 默认启用了:mgr.GetWebhookServer().Start(ctx) 在 Manager.Start 中。证书是 Webhook Server 启动的前置条件,生产环境通常用 cert-manager 注入证书:cert-manager.io/inject-ca-from: app-operator-system/app-operator-serving-cert。
▼ Q15: controller-runtime 的 metrics 怎么暴露给 Prometheus?
A: 启动时 metricsserver.NewServer(metricsserver.Options{BindAddress: ":8080"}) 加到 ManagerOptions。Operator 默认会注册 controller_runtime_reconcile_total、controller_runtime_reconcile_errors_total、controller_runtime_workqueue_depth、controller_runtime_terminal_reconcile_errors_total 四个核心指标。
▼ Q16: 为什么我用 cli.List 拿不到东西?
A: 99% 是 ListOptions 没设 Namespace。controller-runtime 的 cli.List(ctx, &list) 默认只 list当前 Reconcile 请求对应的 namespace。要 list 全部 namespace 显式设 cli.List(ctx, &list, client.InNamespace("")) 或者 cli.ListAllNamespace(ctx, &list)。
▼ Q17: Scheme 是必须的吗?能不能不要?
A: 必须。Scheme 是 GVK ↔ Go type 的映射表,没有它 cli.Get 就不知道 &appv1.Application{} 对应哪个 GVK。Kubebuilder 生成的 groupversion_info.go 中的 SchemeBuilder 就是用来注册 Scheme 的。
▼ Q18: controller-runtime v0.19 vs v0.10 主要差异?
A: 关键差异:① 引入 client.MergeFrom / client.Apply 的统一 Patch 接口;② reconcile.Request 改名为 reconcile.Request(无变化);③ 增加 FieldOwner;④ reconcile.Result.Requeue 默认 false;⑤ ControllerOptions 改名为 Options 内部结构。
▼ Q19: 怎么在 main.go 验证 Manager 启动成功?
A: 启动后立刻 curl localhost:8081/healthz 200 表示 health probe OK;curl localhost:8081/readyz 200 表示 ready(所有 cache 已同步)。这两个端点是 Manager 自带 healthz server 提供的,生产 k8s 会用它们判断 Pod 是否 ready。
▼ Q20: 我可以把同一个 CRD 注册到两个 Controller 吗?
A: 可以,但需要不同的 Reconciler 实现,且都要通过 For(&MyCR{}) 声明。底层 cache 会为这个 GVK 启动一个 Informer,两个 Controller 共享同一个 Informer,事件会通过 eventhandler 推到各自的 WorkQueue。生产中不太常见(通常一个 CR 一个 Controller),但理论上支持。
▼ Q21: controller-runtime 在 k8s 1.36 上有兼容性 issue 吗?
A: 1.36 推荐 controller-runtime v0.19+。注意 1.36 引入了 StorageVersionMigrator 等新机制,旧版本 controller-runtime 的某些 GVK 处理可能需要手动 workaround。生产经验:go mod tidy 后跑 go test ./... + make test-integration 验证全链路。
Kubernetes 编程 / Operator 专题【左扬精讲】—— controller-runtime 框架内幕 · 基于 k8s 1.36.1 + controller-runtime v0.19
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。