Skip to content

Operator入门

Operator 开发把 Kubernetes 控制器模式用到自定义资源上。CRD 定义新的资源类型,用户写 spec 描述期望状态,Controller 监听资源变化并执行 reconcile,把实际资源调整到接近期望状态,再把结果写回 status

Kubernetes 章节里的 CRD 与控制器Operator 运维 更偏集群运维视角;Go 语言里的 Operator 入门重点放在程序结构、controller-runtime、Reconcile、权限和本地运行。

普通 Kubernetes 资源已经有内置控制器维护,比如 Deployment Controller 会根据 Deployment 创建 ReplicaSet 和 Pod。Operator 做的是同一类事,只是资源类型换成了业务或平台自定义对象,比如 BackupJobPrometheusCertificate。自定义资源保存期望状态,自定义控制器负责把这些期望状态翻译成真实资源和外部动作。

一、从脚本到控制器

普通脚本通常是一次性执行:

text
读取参数 -> 调用 API -> 输出结果 -> 进程退出

控制器是持续运行的循环:

text
监听资源变化 -> 读取期望状态 -> 查询实际状态 -> 修正差异 -> 更新状态 -> 等待下一次变化

手工维护一个备份任务时,操作可能是创建 CronJob、挂载 Secret、设置保留时间、检查最近一次执行结果。Operator 会把这些动作收进控制器里,让集群里出现一个更贴近业务语义的资源,例如 BackupJob

这里的 BackupJob 不是 Kubernetes 内置资源。它只是一个示例自定义资源,表示“某个目标需要按计划备份”。CRD 安装后,API Server 才认识这个 kind;Controller 运行后,这个 kind 才会产生实际动作。

CRD 只负责让 API Server 认识 BackupJob 这种对象。创建 CronJob、更新 status、处理删除清理,都由 Controller 完成。

CronJob 是 Kubernetes 内置的定时任务资源。BackupJob Controller 可以根据 BackupJob.spec.schedule 创建对应 CronJob,让 Kubernetes 自己按时间启动备份 Pod。这样自定义资源负责表达“我要备份什么、什么时候备份”,内置资源负责具体调度执行。

二、Operator 的程序组成

一个基于 controller-runtime 的 Operator 通常包含这些对象:

组成作用
API TypeGo 结构体,定义 specstatus 字段
CRD由 API Type 生成,安装到集群后提供新资源类型
Manager管理 RESTConfig、缓存、Client、Controller、Webhook、指标
Controller监听资源变化,把事件交给 Reconcile
Reconciler核心处理逻辑,对比期望状态和实际状态
Scheme注册 Go 类型和 Kubernetes GroupVersionKind 的映射
RBAC授权控制器读写相关资源

API Type 和 CRD 是同一件事的两种形态。API Type 是 Go 代码里的结构体,开发时编辑它;CRD 是安装到集群里的 YAML,API Server 用它校验和保存对象。Kubebuilder 会根据 API Type 生成 CRD。

controller-runtime 对 client-go 做了一层封装。它仍然使用 RESTConfig、Informer 和 Client,只是把缓存、事件监听、队列、重试、leader election 等常用能力整理成统一结构。

Manager 是 Operator 进程里的总入口。它启动缓存、注册 Controller、暴露指标、处理 leader election,并把同一份 RESTConfig 和 Scheme 分给各个控制器使用。单个 Operator 进程里可以同时管理多个 Controller。

三、controller-runtime 里的 Client

controller-runtime 的 client.Client 和上一节的 ClientSet 不太一样。

客户端适合场景
kubernetes.ClientSet访问 Kubernetes 内置资源,调用路径类型化
dynamic.Interface临时访问未知 GVR 的资源
controller-runtime client.ClientOperator 里同时访问内置资源和自定义资源

controller-runtime client 通过 Scheme 识别资源类型。自定义资源的 Go 类型注册到 Scheme 后,代码里可以用同一个 client 读取 BackupJob,也可以读取 Deployment、Secret、CronJob。

Scheme 解决的是“Go 结构体和 Kubernetes 资源类型怎么对应”的问题。例如 BackupJob{} 这个 Go 类型要对应到 ops.example.com/v1alpha1 里的 BackupJob kind;appsv1.Deployment{} 要对应到 apps/v1 里的 Deployment。没有注册到 Scheme 的类型,client 不知道它应该请求哪个 API 路径。

默认情况下,读操作常从本地缓存读取,写操作直接发给 API Server。刚写完对象立刻从缓存读,可能短暂读到旧值。对强一致要求高的地方,需要理解 cache client 和 direct API reader 的差异。

四、Reconcile 模式

Reconcile 的输入通常只有一个 namespace/name,也就是发生变化的对象键。控制器拿到 key 后重新读取当前对象,再根据最新状态处理。

典型流程:

步骤内容
读取自定义资源根据 namespace/name 获取 BackupJob
处理已删除对象对象不存在时结束;带 finalizer 时执行外部清理
读取关联资源查看 CronJob、Secret、ConfigMap 是否存在
创建或更新资源实际状态不符合 spec 时进行调整
更新 status写入 Ready、原因、最后执行时间等状态
返回结果成功结束、延迟重试或按错误重试

Reconcile 不是“事件来了就按事件内容改一下”。更稳的写法是每次都读取当前完整状态,然后计算实际状态和期望状态的差异。新增、更新、重试、进程重启后重新同步,都走同一套逻辑。

这种写法要求 Reconcile 幂等。幂等的意思是同一个对象被处理多次,结果仍然稳定。例如 CronJob 已经存在且字段正确时,Reconcile 再跑一次应该什么也不改;CronJob 不存在时才创建;字段不一致时才更新。控制器重启、watch 重连、事件重复投递都会让同一个对象被多次处理。

Reconcile 返回错误时,controller-runtime 会把这个 key 重新放回队列,后续再试。返回 RequeueAfter 时,即使没有新事件,也会在指定时间后再处理一次,适合定期检查外部状态。

五、spec 和 status

自定义资源一般分成 specstatus

yaml
apiVersion: ops.example.com/v1alpha1
kind: BackupJob
metadata:
  name: mysql-daily
  namespace: ops
spec:
  target: mysql
  schedule: "10 2 * * *"
  retentionDays: 7
status:
  ready: true
  lastBackupTime: "2026-05-29T02:10:00Z"
  message: "last backup succeeded"

spec 是期望状态,通常由用户、GitOps 或平台写入。status 是实际状态,通常由 Controller 写入。排查 Operator 时,status.conditionsmessageobservedGeneration 这些字段能说明控制器处理到了哪个版本、失败原因是什么。

conditions 通常是一组状态项,比如 Ready=TrueBackupSucceeded=False。每个 condition 里会带 typestatusreasonmessagelastTransitionTime。用户看到自定义资源没有生效时,先看 status 比直接翻 controller 日志更快。

observedGeneration 常用于判断 status 是否对应最新 spec。用户改了 spec 后,metadata.generation 会增加;控制器处理完成后,把 status.observedGeneration 更新成同一个值。两者不一致时,说明 status 可能还是旧状态。

六、OwnerReference 和 finalizer

Operator 创建子资源时,通常给子资源加 OwnerReference。这样可以表达“这个 CronJob 属于这个 BackupJob”。

OwnerReference 的作用:

作用说明
资源关系kubectl describe 时能看出父子关系
事件回溯子资源变化可以触发父资源 Reconcile
垃圾回收父资源删除后,Kubernetes 可以清理子资源

finalizer 用于删除前清理外部资源。比如 Operator 在云厂商创建了一个对象存储 bucket,删除 CR 时需要先删除或解绑外部 bucket,再移除 finalizer。控制器异常时,带 finalizer 的资源可能卡在 Terminating

yaml
metadata:
  finalizers:
    - backup.ops.example.com/finalizer

finalizer 处理要保持幂等。清理动作执行一半失败后,下一次 Reconcile 会再次进入删除流程;重复删除已经不存在的外部资源,应该返回成功或可识别的结果。

OwnerReference 处理的是 Kubernetes 内部资源之间的归属关系,finalizer 处理的是删除前还需要额外做的清理动作。只创建 CronJob 这类子资源时,OwnerReference 往往够用;还创建了云资源、DNS 记录、外部账号这类 Kubernetes 不知道的对象时,通常需要 finalizer。

七、用 Kubebuilder 生成骨架

Kubebuilder 是 controller-runtime 生态里常用的脚手架工具。它生成项目结构、API 类型、Controller、RBAC 标记、Makefile 和部署清单。

常见初始化命令:

bash
kubebuilder init \
  --domain example.com \
  --repo example.com/backup-operator

kubebuilder create api \
  --group ops \
  --version v1alpha1 \
  --kind BackupJob

生成后的目录大致是:

text
backup-operator/
├── api/
│   └── v1alpha1/
│       ├── backupjob_types.go
│       └── groupversion_info.go
├── internal/
│   └── controller/
│       └── backupjob_controller.go
├── config/
│   ├── crd/
│   ├── rbac/
│   ├── manager/
│   └── samples/
├── cmd/
│   └── main.go
├── go.mod
└── Makefile

api/ 里写资源字段,internal/controller/ 里写 Reconcile,config/ 里放生成出来的 CRD、RBAC 和部署清单。小型 Operator 的核心改动通常集中在 API Type 和 Reconciler 两处。

config/samples/ 里的 YAML 是一个示例自定义资源,用来验证 CRD 和 Controller 是否工作。它不是 CRD 本身,而是 CRD 安装后创建出来的一条 BackupJob 实例。

八、本地运行链路

本地开发 Operator 时,常见流程是先把 CRD 安装进集群,再在本机运行控制器进程。

当前状态:集群里还没有 BackupJob 这种资源。

安装 CRD:

bash
make install
kubectl get crd | grep backupjobs
kubectl api-resources | grep BackupJob

make install 通常只安装 CRD,不会启动控制器。执行后,集群里能创建 BackupJob 对象,但如果控制器没运行,它不会自动创建 CronJob,也不会更新 status。

启动控制器:

bash
make run

另一个终端创建示例资源:

bash
kubectl apply -f config/samples/ops_v1alpha1_backupjob.yaml
kubectl get backupjob -A
kubectl describe backupjob mysql-daily -n ops

创建示例资源后,API Server 会保存这条 BackupJob。正在运行的控制器通过缓存和队列收到变化,再进入 Reconcile。Reconcile 里如果创建了 CronJob,可以继续用 kubectl get cronjob -n ops 验证子资源是否出现。

控制器进程会使用当前 kubeconfig 连接集群。本地 kubectl 指向哪个 context,make run 通常也会指向同一个集群。多集群环境下,运行前先确认 kubectl config current-context

九、部署到集群

本地运行验证通过后,控制器通常会被打成镜像,作为 Deployment 跑在集群里。部署清单会包含 ServiceAccount、RBAC、Deployment、LeaderElection 相关权限和指标端口。

常见命令:

bash
make docker-build docker-push IMG=registry.example.com/ops/backup-operator:v0.1.0
make deploy IMG=registry.example.com/ops/backup-operator:v0.1.0

make docker-build 构建控制器镜像,make docker-push 推送到镜像仓库,make deploy 把 Deployment、ServiceAccount、RBAC 等资源安装到集群里。IMG 指定集群里最终要拉取的控制器镜像地址。

部署后检查:

bash
kubectl get deploy,pod -n backup-operator-system
kubectl logs deploy/backup-operator-controller-manager -n backup-operator-system
kubectl auth can-i list backupjobs.ops.example.com \
  --as=system:serviceaccount:backup-operator-system:backup-operator-controller-manager \
  -A

Operator 在集群里运行时不再读取本机 kubeconfig,而是使用 Pod 的 ServiceAccount。权限问题通常表现为 controller 日志里出现 forbidden,用户侧现象是 CR 创建成功,但子资源没有创建或 status 不更新。

十、常见错误

现象用户侧表现常见原因排查入口
no matches for kindapply 示例 CR 时直接报错,API Server 不认识这个 kindCRD 没安装或 apiVersion 写错kubectl get crdkubectl api-resources
CR 创建成功但没有子资源kubectl get backupjob 能看到对象,但没有 CronJob/SecretController 没运行、RBAC 不足、Reconcile 没匹配到对象controller 日志、事件、kubectl auth can-i
status 不更新kubectl get -o yaml 里 status 为空或 observedGeneration 落后status 子资源未启用、权限缺少 /status、更新冲突CRD、RBAC、controller 日志
删除卡住 Terminatingkubectl delete 后对象一直存在,metadata 有 deletionTimestampfinalizer 清理失败metadata.finalizers、controller 日志、外部资源状态
控制器反复更新同一对象Events 或日志里持续出现 update,同一资源频繁变更Reconcile 不幂等、默认值和期望值反复漂移对比对象 YAML、查看 managedFields 和日志
本地运行连错集群示例 CR 出现在另一个集群,当前集群没有变化kubeconfig context 不对kubectl config current-context

Operator 的排查入口通常不是业务 Pod 的日志,而是 Controller 日志、CR 的 status、Events、RBAC 和子资源的 OwnerReference。控制器把“为什么要创建或修改这些对象”的原因保存在这些地方。

十一、和 client-go 的关系

Operator 看起来比 client-go 多了很多目录和生成文件,但底层链路仍然相同:

client-go 提供访问 Kubernetes API 的底层能力;controller-runtime 把这些能力组织成适合写控制器的框架。理解 kubeconfig、RESTConfig、ClientSet、Informer 后,再看 Operator 的 Manager、Cache、Client、Reconcile,会少很多凭空出现的概念。