Kubernetes Operator与CRD生产开发实战
Operator 的价值不是“把脚本放进容器”,而是把领域知识实现为持续工作的控制循环:观察期望状态和实际状态,执行幂等动作,记录状态,并在失败后安全重试。设计良好的 Operator 能让安装、扩容、备份、升级和故障恢复具有一致入口;设计不当则会制造一个拥有高权限、不断重试且难以停下的自动化风险源。
本文面向准备使用 Kubebuilder、controller-runtime 或 Operator SDK 开发生产控制器的团队。示例聚焦设计方法和运行保障,具体依赖版本与生成命令应以当前官方文档为准。
一、先判断是否真的需要Operator
适合 Operator 的场景:
- 对象有持续生命周期,而不是一次性执行;
- 需要根据外部系统或多个Kubernetes资源持续收敛;
- 扩缩容、升级、备份、恢复等领域动作有明确状态机;
- 多个团队反复执行同类操作,需要统一安全边界;
- 需要把状态、条件和事件暴露给平台与自动化系统。
若需求只是部署一组静态资源,Helm或Kustomize通常更简单;若只是定时运行命令,CronJob可能足够;若外部系统已有成熟API和工作流平台,也不必强行映射为CRD。
二、从用户API而不是控制器代码开始
CRD 是长期兼容的产品接口。先写使用示例和状态输出,再写Go代码:
apiVersion: database.example.com/v1alpha1
kind: DatabaseCluster
metadata:
name: orders-db
spec:
version: "16.4"
replicas: 3
storage:
size: 500Gi
className: fast-rwo
backup:
schedule: "0 2 * * *"
retention: 14
maintenance:
window: "Sun 02:00-04:00"
spec 表达用户期望,status 表达控制器观察到的现实。不要让用户填写可以推导的内部名称、Pod IP或临时步骤;不要把密码写在spec。字段一旦发布就很难删除,应为默认值、校验、升级路径和弃用策略预留空间。
三、设计可机器判断的Status和Conditions
状态至少包含:
observedGeneration:控制器已处理的spec版本;conditions:Available、Progressing、Degraded等条件;- 当前与期望版本、当前副本和就绪副本;
- 最近成功备份、失败原因和可重试时间;
- 外部资源标识,但不包含密钥。
条件应使用 True/False/Unknown,包含 reason、可读 message 和转换时间。同一类型保留最新条件,不要每次循环无限追加。调用方只有在 observedGeneration == metadata.generation 时,才能确认状态对应当前spec。
四、Reconcile必须幂等
协调循环不能假设事件只到达一次,也不能依赖固定顺序。典型结构:
func (r *DatabaseClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var db databasev1alpha1.DatabaseCluster
if err := r.Get(ctx, req.NamespacedName, &db); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if !db.DeletionTimestamp.IsZero() {
return r.reconcileDelete(ctx, &db)
}
if err := r.ensureFinalizer(ctx, &db); err != nil {
return ctrl.Result{}, err
}
if err := r.reconcileService(ctx, &db); err != nil {
return r.fail(ctx, &db, "ServiceError", err)
}
if err := r.reconcileStatefulSet(ctx, &db); err != nil {
return r.fail(ctx, &db, "WorkloadError", err)
}
return r.reconcileStatus(ctx, &db)
}
ensure 的含义是读取现状、计算差异、只修改必要字段。外部API调用应携带幂等键;创建前先查询,超时后不能直接假设失败,否则重试可能创建重复资源。
五、建立明确状态机
复杂流程不要依赖“当前有哪些资源”隐式推断全部阶段。可设计:Pending→Provisioning→Ready→Upgrading→Ready,失败进入Degraded但保留可恢复信息。每个迁移步骤应定义:
- 进入条件和前置检查;
- 可重复执行的动作;
- 成功证据;
- 超时和重试策略;
- 中断后的恢复点;
- 回滚条件和不可逆边界。
升级数据库、迁移存储等操作应由显式字段或审批触发,不要因为镜像默认值变化就自动执行破坏性升级。
六、正确使用OwnerReference与Finalizer
集群内从属对象可设置 controller owner reference,让垃圾回收器在父对象删除后清理。跨命名空间对象和外部云资源不能只依赖它,需要 finalizer 执行删除前动作。
finalizer流程:
- 对象正常且没有finalizer时先添加;
- 发现
deletionTimestamp后停止创建新资源; - 幂等清理外部资源并验证结果;
- 清理成功后移除finalizer;
- 永久失败时暴露条件、事件和人工处置方法。
外部服务长时间不可用会导致对象卡在Terminating。必须提供受审计的强制接管流程,并明确强制移除finalizer可能遗留的资源。
七、避免更新冲突与字段争夺
多个控制器、GitOps和用户可能同时修改对象。建议:
- 使用Patch或Server-Side Apply,只拥有必要字段;
- 更新status时使用status子资源;
- 冲突后重新读取对象并重新计算,而非覆盖最新数据;
- 不改写用户未授权给Operator管理的字段;
- 子资源名称和标签稳定,便于恢复关联;
- 使用哈希注解触发配置变化,而不是无条件更新。
若每次reconcile都修改时间戳或无意义字段,会形成自触发热循环,拖垮API Server。
八、Watch、索引和队列设计
主资源变化会触发协调,从属资源和外部事件也可能触发。只监听真正影响期望状态的事件,并使用predicate过滤status或无关字段变化。
需要反向查找时建立字段索引,例如从Secret找到引用它的所有自定义资源,避免每次全量List。工作队列应保留指数退避;已知等待条件可用 RequeueAfter,但不要把控制器变成秒级轮询器。大规模集群需测量单次协调耗时、队列深度和API调用量。
九、校验、默认值与转换Webhook
简单字段约束优先使用OpenAPI schema:枚举、范围、长度和必填关系。跨字段复杂校验再考虑CEL或验证webhook。默认值应稳定、可解释,避免升级控制器后同一旧清单得到不同结果。
当API从v1alpha1演进到v1beta1或v1时:
- 选择一个storage version;
- 定义双向转换并覆盖所有字段;
- 验证往返转换不会丢数据;
- 先让控制器同时理解新旧版本;
- 迁移存量对象后再停止旧版本服务;
- 监控仍在调用旧版本的客户端。
转换webhook属于API读取和写入路径,应按控制面关键服务建设高可用、证书和故障降级。
十、权限必须按控制器实际动作收敛
生成的RBAC标记只是起点。检查控制器是否真的需要list全部Secret、修改所有Namespace或创建ClusterRole。按API组、资源、动词和命名空间缩小权限;外部云权限使用工作负载身份,避免静态长效密钥。
分离:
- 控制器运行权限;
- 用户创建和修改CR的权限;
- 高风险动作如恢复、主从切换、删除备份的审批权限;
- 只读观察和审计权限。
Controller Pod本身启用非root、只读根文件系统、seccomp、网络策略和镜像签名验证。
十一、Leader Election与高可用
生产通常运行多个副本并启用leader election,避免单实例故障。leader切换后,新的实例会重新处理队列,因此所有动作必须幂等。租约参数不能只追求快速切换:API延迟或短暂网络抖动可能造成频繁换主。
多个副本不等于容量自动提升;同一控制器通常只有leader执行协调。需要提高吞吐时,调整最大并发协调数、索引和外部API限流,并验证目标系统能够承受并发。
十二、外部系统调用的可靠性
调用云API、DNS、数据库或工单系统时:
- 设置连接、请求和总流程超时;
- 仅对可恢复错误重试,并使用退避与抖动;
- 使用幂等键和操作ID;
- 限制每租户并发,防止一个故障对象占满队列;
- 对429和配额错误单独观测;
- 将外部操作状态写入status,而非只写日志;
- 不在内存中保存唯一进度,Pod重启后必须可恢复。
对不可逆操作采用两阶段确认或显式维护窗口。
十三、可观测性设计
建议暴露:
- reconcile总次数、结果、耗时和错误类型;
- 工作队列深度、等待时间、重试次数和最老任务年龄;
- 各状态和条件的自定义资源数量;
- 外部API延迟、错误、限流和配额;
- leader状态、缓存同步和webhook延迟;
- 资源创建、升级、备份和删除的业务SLO。
日志使用资源命名空间、名称、UID、generation、reconcile ID和操作ID关联,但不输出Secret。Event适合用户可见的关键转折,不适合每次循环都写。
十四、测试金字塔
- 纯函数单测:默认值、差异计算、状态机和错误分类;
- envtest:真实API schema、webhook、status、finalizer和RBAC行为;
- 集成测试:真实控制器与模拟外部API;
- 临时集群端到端:安装、升级、备份、恢复和卸载;
- 故障注入:API超时、Pod重启、leader切换、冲突、限流和部分成功。
测试必须覆盖协调被重复调用、在任意步骤重启、对象在处理中被修改和删除。只验证“最终创建了Pod”远远不够。
十五、发布与升级
CRD通常先于新控制器发布,但必须保证旧控制器不会误解新字段。推荐顺序:
- 备份CR和CRD;
- 安装向后兼容的CRD;
- 部署支持新旧版本的控制器;
- 小范围创建新版本对象;
- 迁移存量并观察错误和条件;
- 完成存储版本迁移;
- 最后弃用旧API。
镜像必须可回退,CRD变更和数据迁移通常不能仅靠回滚Deployment解决。每次发布记录兼容矩阵和不可逆步骤。
十六、常见事故与处置
热循环导致API Server压力升高
检查控制器是否每次都更新对象、Watch了自身status、忽略resourceVersion冲突或使用极短RequeueAfter。先降并发或暂停单个控制器,再修复差异判断。
大量对象卡在删除
检查finalizer、外部API和控制器权限。不要批量强删;先恢复控制器或外部依赖,按对象确认遗留资源,再审计移除。
升级后状态不再推进
核对CRD storedVersions、转换webhook、缓存反序列化和新控制器日志。必要时恢复兼容控制器,不要在不清楚转换结果时重写全部对象。
控制器误删资源
立即停止协调或收紧权限,保留审计日志和对象快照,确认OwnerReference、标签选择器和删除状态机。恢复后先以只读或单对象模式验证。
十七、生产验收清单
- [ ] 使用场景确实需要持续控制循环;
- [ ] spec/status边界清楚,API有版本与弃用计划;
- [ ] reconcile、外部调用和删除流程均幂等;
- [ ] status包含observedGeneration和标准conditions;
- [ ] finalizer故障有受审计的处置流程;
- [ ] RBAC和云权限已按最小权限收敛;
- [ ] webhook、leader election和证书有高可用监控;
- [ ] 队列、协调耗时、错误和外部依赖可观测;
- [ ] 任意步骤重启、冲突和重复事件通过测试;
- [ ] CRD升级、存储迁移和回滚经过演练。
十八、官方参考
- Kubernetes Operator Pattern
- Kubernetes Custom Resources
- Kubebuilder Book
- Kubebuilder Using Finalizers
- Operator SDK Documentation
一个可信的 Operator 应像优秀值班工程师:反复执行也安全,遇到未知情况会停下并说明原因,操作过程可追溯,恢复路径明确,而不是拥有权限后不停尝试。