K8s Operator 开发入门:从 CRD 到 Reconcile
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 的几条铁律
- 幂等:同一个对象 Reconcile 10 次结果应一样
- 不要在 Reconcile 里 sleep:用
RequeueAfter让 controller-runtime 自己调度 - status 单独 update:spec 和 status 是不同的 subresource
- 错误处理分类:临时错误 RequeueAfter,永久错误记到 status 别 retry
- 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 逻辑"。