Operator 是把运维知识写成代码的方式。本文用一个最小例子讲清楚怎么开发。

一个例子:自动备份 PVC

需求:定义一个 PVCBackup 资源,自动给 PVC 每小时打快照存到 S3。

1. 定义 CRD

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: pvcbackups.storage.example.com
spec:
  group: storage.example.com
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              pvcName:
                type: string
              schedule:
                type: string
              s3Bucket:
                type: string
            required: [pvcName, schedule, s3Bucket]
          status:
            type: object
            properties:
              lastBackup:
                type: string
              nextBackup:
                type: string
  scope: Namespaced
  names:
    plural: pvcbackups
    singular: pvcbackup
    kind: PVCBackup

2. 用 kubebuilder 生成脚手架

kubebuilder init --domain example.com --repo pvcbackup-operator
kubebuilder create api --group storage --version v1 --kind PVCBackup

生成 controllers/pvcbackup_controller.go

3. 实现 Reconcile

func (r *PVCBackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)

    // 1. 拿到对象
    var backup storagev1.PVCBackup
    if err := r.Get(ctx, req.NamespacedName, &backup); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 2. 检查 PVC 是否存在
    var pvc corev1.PersistentVolumeClaim
    if err := r.Get(ctx, types.NamespacedName{Name: backup.Spec.PVCName, Namespace: req.Namespace}, &pvc); err != nil {
        log.Error(err, "PVC not found")
        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
    }

    // 3. 判断是否到了下次备份时间
    if backup.Status.NextBackup != "" {
        nextTime, _ := time.Parse(time.RFC3339, backup.Status.NextBackup)
        if time.Now().Before(nextTime) {
            // 等到下次时间再 reconcile
            return ctrl.Result{RequeueAfter: time.Until(nextTime)}, nil
        }
    }

    // 4. 执行备份(创建 VolumeSnapshot + 上传 S3)
    if err := r.doBackup(ctx, &backup, &pvc); err != nil {
        return ctrl.Result{RequeueAfter: 5 * time.Minute}, err
    }

    // 5. 更新 status
    backup.Status.LastBackup = time.Now().Format(time.RFC3339)
    next, _ := cron.ParseStandard(backup.Spec.Schedule)
    backup.Status.NextBackup = next.Next(time.Now()).Format(time.RFC3339)
    return ctrl.Result{RequeueAfter: time.Until(next.Next(time.Now()))}, r.Status().Update(ctx, &backup)
}

4. Reconcile 的几条铁律

  1. 幂等:同一个对象 Reconcile 10 次结果应一样
  2. 不要在 Reconcile 里 sleep:用 RequeueAfter 让 controller-runtime 自己调度
  3. status 单独 update:spec 和 status 是不同的 subresource
  4. 错误处理分类:临时错误 RequeueAfter,永久错误记到 status 别 retry
  5. finalizer 处理删除:要清理外部资源(如 S3 上的备份)就加 finalizer

5. 监控 Operator

Operator 也是普通 Pod,加 Prometheus metrics:

import "sigs.k8s.io/controller-runtime/pkg/metrics"

var (
    reconcileCounter = prometheus.NewCounter(...)
)
func init() {
    metrics.Registry.MustRegister(reconcileCounter)
}

关键指标:
- controller_runtime_reconcile_total:reconcile 次数
- controller_runtime_reconcile_errors_total:错误次数
- workqueue_depth:队列深度(大了说明 reconcile 跟不上)

6. 部署

make docker-build docker-push IMG=registry/pvcbackup:v1
make deploy IMG=registry/pvcbackup:v1

一些反模式

  • ❌ 在 Reconcile 里写状态机:用 phase 字段记当前状态
  • ❌ 跨对象事务:K8s 没事务,要设计成幂等的
  • ❌ 调用外部 API 不超时:必加 context timeout

教训:Operator 难的不是写代码,是把"运维知识"翻译成"幂等的 Reconcile 逻辑"。

标签: none

添加新评论