Appearance
Operator入门
Operator 开发把 Kubernetes 控制器模式用到自定义资源上。CRD 定义新的资源类型,用户写 spec 描述期望状态,Controller 监听资源变化并执行 reconcile,把实际资源调整到接近期望状态,再把结果写回 status。
Kubernetes 章节里的 CRD 与控制器 和 Operator 运维 更偏集群运维视角;Go 语言里的 Operator 入门重点放在程序结构、controller-runtime、Reconcile、权限和本地运行。
普通 Kubernetes 资源已经有内置控制器维护,比如 Deployment Controller 会根据 Deployment 创建 ReplicaSet 和 Pod。Operator 做的是同一类事,只是资源类型换成了业务或平台自定义对象,比如 BackupJob、Prometheus、Certificate。自定义资源保存期望状态,自定义控制器负责把这些期望状态翻译成真实资源和外部动作。
一、从脚本到控制器
普通脚本通常是一次性执行:
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 Type | Go 结构体,定义 spec 和 status 字段 |
| 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.Client | Operator 里同时访问内置资源和自定义资源 |
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
自定义资源一般分成 spec 和 status:
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.conditions、message、observedGeneration 这些字段能说明控制器处理到了哪个版本、失败原因是什么。
conditions 通常是一组状态项,比如 Ready=True、BackupSucceeded=False。每个 condition 里会带 type、status、reason、message、lastTransitionTime。用户看到自定义资源没有生效时,先看 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/finalizerfinalizer 处理要保持幂等。清理动作执行一半失败后,下一次 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
└── Makefileapi/ 里写资源字段,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 BackupJobmake 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.0make 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 \
-AOperator 在集群里运行时不再读取本机 kubeconfig,而是使用 Pod 的 ServiceAccount。权限问题通常表现为 controller 日志里出现 forbidden,用户侧现象是 CR 创建成功,但子资源没有创建或 status 不更新。
十、常见错误
| 现象 | 用户侧表现 | 常见原因 | 排查入口 |
|---|---|---|---|
no matches for kind | apply 示例 CR 时直接报错,API Server 不认识这个 kind | CRD 没安装或 apiVersion 写错 | kubectl get crd、kubectl api-resources |
| CR 创建成功但没有子资源 | kubectl get backupjob 能看到对象,但没有 CronJob/Secret | Controller 没运行、RBAC 不足、Reconcile 没匹配到对象 | controller 日志、事件、kubectl auth can-i |
| status 不更新 | kubectl get -o yaml 里 status 为空或 observedGeneration 落后 | status 子资源未启用、权限缺少 /status、更新冲突 | CRD、RBAC、controller 日志 |
| 删除卡住 Terminating | kubectl delete 后对象一直存在,metadata 有 deletionTimestamp | finalizer 清理失败 | 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,会少很多凭空出现的概念。