6.3 K8s Operator/CRD 入门——扩展 Kubernetes API

预计阅读时间:13 分钟

📖 目录

Kubernetes 内置的资源(Deployment、Service、ConfigMap 等)覆盖了通用场景,但当业务需要管理有状态应用(如 Redis 集群、数据库主从、自定义中间件)时,原生资源的语义不够表达运维知识。CRD + Operator 模式让你把运维经验编码成代码,让 K8s "理解" 你的应用。

学习目标

学完本章后,你将能够:

  • 编写 CRD 定义自定义资源(group/version/scope/openAPIV3Schema)并创建实例
  • 理解 Operator 模式与 Reconciliation Loop 的声明式调谐原理
  • 使用 Kubebuilder 脚手架初始化项目,创建 CRD + Controller
  • 实现 Redis Operator 的 Reconcile 逻辑、Status 汇报与子资源编排
  • 通过 Finalizer、RBAC 最小权限与 CRD 版本演进策略保障 Operator 生产可用
  • 从 OperatorHub 评估 Operator 成熟度(Level 3+)并选用合适的现成 Operator

前置知识

一、CRD——自定义资源定义

Custom Resource Definition(CRD)是 K8s API 的扩展机制。定义 CRD 后,你可以像使用内置资源一样 kubectl get redis,kubectl 和 API Server 会自动识别这个新类型。

1.1 CRD YAML 示例

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: redisclusters.cache.example.com
spec:
  group: cache.example.com
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                replicas:
                  type: integer
                  minimum: 1
                version:
                  type: string
                  enum: ["6.2", "7.0", "7.2"]
              required: ["replicas"]
            status:
              type: object
              properties:
                readyReplicas:
                  type: integer
                phase:
                  type: string
  scope: Namespaced
  names:
    plural: redisclusters
    singular: rediscluster
    kind: RedisCluster
    shortNames: [rc]

1.2 创建自定义资源实例

apiVersion: cache.example.com/v1alpha1
kind: RedisCluster
metadata:
  name: my-redis
  namespace: default
spec:
  replicas: 3
  version: "7.2"
kubectl apply -f rediscluster.yaml
kubectl get rc          # 使用 shortName
kubectl describe rc my-redis

1.3 openAPIV3Schema 字段类型速查

CRD 的校验规则全部由 openAPIV3Schema 表达,无需写代码。下表是考试与日常开发最常用的字段约束:

Schema 类型YAML 写法常用约束典型用途
stringtype: stringminLength: 3enum: [a, b]pattern: "^v\\d+"版本号、名称
integertype: integerminimum: 1maximum: 100副本数、端口
booleantype: booleandefault: false开关型参数
arraytype: array + items:maxItems: 10uniqueItems: true节点列表、标签集合
objecttype: object + properties:required: [...]additionalProperties: false嵌套配置

1.4 多版本管理与演进

生产 CRD 常需要同时服务多个 API 版本。served 控制版本是否可读写,storage 控制哪个版本写入 etcd(全集群只能有一个 storage: true)。多版本字段不一致时还需要 Conversion Webhook 做数据转换:

spec:
  group: cache.example.com
  versions:
    - name: v1alpha1
      served: false        # 旧版本下线,仅保留读取
      storage: false
    - name: v1
      served: true
      storage: true        # 当前持久化版本
      schema: { ... }
    - name: v2
      served: true         # 新版本开始灰度接入
      storage: false
      schema: { ... }
  conversion:
    strategy: Webhook      # v1 与 v2 字段不同时需转换 Webhook
    webhook:
      conversionReviewVersions: ["v1", "v2"]
      clientConfig:
        service:
          name: redis-crd-conversion
          namespace: cache-system

演进原则:新版本先 served: true 灰度,确认无误后再提升为 storage 版本并停用旧版本;字段改名时保留旧字段为 deprecated,并在转换 Webhook 中做映射,避免已有实例数据损坏。

CRD 的核心价值:让 Kubernetes API 服务器知道你的自定义资源类型,支持版本管理、Schema 校验、kubectl 自动补全。CRD 本身不包含任何运维逻辑——它只是"数据结构定义"。

二、Operator 模式——Reconciliation Loop

CRD 只定义了"长什么样",Operator 负责"怎么管"。Operator 的核心是一个持续运行的 Reconciliation Loop(调谐循环)

  1. Watch(监听)自定义资源的变化事件
  2. 读取当前状态(Actual State)
  3. 对比期望状态(Desired State)
  4. 执行调谐动作,使当前状态趋近期望状态
  5. 更新 Status 子资源,记录调谐结果
// 伪代码:Reconciliation Loop
func Reconcile(req ctrl.Request) (ctrl.Result, error) {
    // 1. 获取自定义资源
    redis := &RedisCluster{}
    if err := r.Get(ctx, req.NamespacedName, redis); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 2. 获取关联的子资源(StatefulSet / Service)
    sts := &appsv1.StatefulSet{}
    if err := r.Get(ctx, types.NamespacedName{...}, sts); err != nil {
        // 子资源不存在 → 创建
        return ctrl.Result{}, r.createRedisCluster(ctx, redis)
    }

    // 3. 对比期望 vs 实际
    if *sts.Spec.Replicas != redis.Spec.Replicas {
        *sts.Spec.Replicas = redis.Spec.Replicas
        return ctrl.Result{}, r.Update(ctx, sts)
    }

    // 4. 更新 Status
    redis.Status.ReadyReplicas = sts.Status.ReadyReplicas
    return ctrl.Result{RequeueAfter: 30 * time.Second}, r.Status().Update(ctx, redis)
}
幂等性原则:Reconcile 函数必须幂等——同一事件多次触发时,结果应相同。不要在 Reconcile 中维护全局状态或假设只执行一次。

2.1 Reconcile 的触发源

调谐循环不只有"资源变化"一个入口。controller-runtime 会把以下四类事件统一投递到同一个 Reconcile 函数:

触发源示例关联方式
主资源事件RedisCluster 创建 / 更新 / 删除默认监听(生成器自动建立)
子资源事件StatefulSet Pod 崩溃、Ready 数变化Reconciler 中 Owns(&appsv1.StatefulSet{}) 自动关联
定时重入RequeueAfter: 30 * time.Second弥补事件丢失 / 外部状态漂移
外部依赖变化Ingress、Secret 等非子资源变更Builder 的 Watches() 手动建立监听

2.2 Finalizer:删除流程的守护

直接删除 CR 会让其子资源成为孤儿。Finalizer 让"删除"变成一个可编程的清理流程:API Server 先给对象打上 deletionTimestamp 并冻结,只有 Operator 移除 Finalizer 后对象才真正消失。

// 注册 Finalizer(在 Reconcile 开头判断)
const redisFinalizer = "cache.example.com/finalizer"

func (r *RedisClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    redis := &cachev1alpha1.RedisCluster{}
    if err := r.Get(ctx, req.NamespacedName, redis); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 正在删除中:先清理子资源,再移除 Finalizer
    if !redis.DeletionTimestamp.IsZero() {
        if controllerutil.ContainsFinalizer(redis, redisFinalizer) {
            r.cleanupPVCs(ctx, redis)          // 删除 PVC 与外部资源
            controllerutil.RemoveFinalizer(redis, redisFinalizer)
            return ctrl.Result{}, r.Update(ctx, redis)
        }
        return ctrl.Result{}, nil
    }

    // 正常调谐前确保 Finalizer 已注册
    if !controllerutil.ContainsFinalizer(redis, redisFinalizer) {
        controllerutil.AddFinalizer(redis, redisFinalizer)
        return ctrl.Result{}, r.Update(ctx, redis)
    }
    // ... 后续调谐逻辑
}

2.3 Status 更新时序

  1. Reconcile 开始 → 读取对象 → 先写入 status.phase = "Pending",让用户立即看到进展
  2. 创建子资源 → 等待其 Ready(此阶段返回 RequeueAfter: 10s 而不是直接报错)
  3. 子资源就绪 → phase = "Running",同步 readyReplicasconditions
  4. 调谐失败 → 写入 condition: Failed 并携带错误消息,配合 kubectl describe 排障

注意 Status().Update()Update() 是两条独立的 API 路径,互不覆盖;kubectl ≥ 1.25 时自定义资源的 status 同样具备子资源语义,避免与 spec 写入产生竞争。

三、Kubebuilder 脚手架

Kubebuilder 是构建 K8s Operator 的官方 Go 脚手架,封装了 controller-runtime、client-go 等库,提供代码生成和项目脚手架。

3.1 安装

# 安装 kubebuilder(Linux amd64)
curl -L -o kubebuilder.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v4.3.0/kubebuilder_linux_amd64.tar.gz
tar xzf kubebuilder.tar.gz && mv kubebuilder_*/bin/kubebuilder /usr/local/bin/ && rm -rf kubebuilder*

# 验证
kubebuilder version

3.2 初始化项目

# 创建项目目录
mkdir redis-operator && cd redis-operator

# 初始化 module
go mod init github.com/yourname/redis-operator

# 初始化 kubebuilder 项目骨架
kubebuilder init --domain example.com --repo github.com/yourname/redis-operator

3.3 创建 API(CRD + Controller)

# 创建 RedisCluster CRD 和对应的 Controller
kubebuilder create api --group cache --version v1alpha1 --kind RedisCluster --resource --controller

# 项目结构生成:
# .
# ├── cmd/
# │   └── main.go                # 入口,注册 Controller
# ├── internal/
# │   └── controller/
# │       └── rediscluster_controller.go  # Reconcile 逻辑
# ├── api/
# │   └── v1alpha1/
# │       ├── rediscluster_types.go        # Spec/Status 结构体
# │       └── groupversion_info.go         # GroupVersion 注册
# ├── config/
# │   ├── crd/bases/                       # 自动生成的 CRD YAML
# │   └── rbac/                            # RBAC 权限
# └── Dockerfile                           # 容器镜像构建

3.4 本地运行与调试

开发迭代不要反复构建镜像:直接在宿主机跑 controller 进程连到开发集群,日志实时可见,改代码即重启。

# 1. 启动一个本地开发集群(Kind)
kind create cluster --name dev

# 2. 仅安装 CRD 到集群(不部署 Operator)
make install

# 3. 本地运行 controller(前台运行,实时观察日志)
make run
# 输出示例:
# 2026-07-31T10:00:00Z INFO setup starting manager
# 2026-07-31T10:00:01Z INFO starting server {"path": "/metrics"}
# 2026-07-31T10:00:02Z INFO Reconciling RedisCluster {"name": "my-redis"}

# 4. 另开终端创建 CR 实例,观察自动调谐
kubectl apply -f config/samples/cache_v1alpha1_rediscluster.yaml
kubectl get rc,sts,pod

# 5. 单元测试:envtest 拉起真实 API Server 跑 Reconcile
make test
# ok   github.com/yourname/redis-operator/internal/controller 0.8s

四、实战:Redis Operator

以下展示核心文件的关键代码片段,完整项目请参考 Kubebuilder Book

4.1 定义 Spec/Status

// api/v1alpha1/rediscluster_types.go
package v1alpha1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type RedisClusterSpec struct {
    Replicas int32  `json:"replicas"`
    Version  string `json:"version,omitempty"`
}

type RedisClusterStatus struct {
    ReadyReplicas int32  `json:"readyReplicas"`
    Phase         string `json:"phase"`  // Pending / Running / Failed
}

type RedisCluster struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`
    Spec              RedisClusterSpec   `json:"spec"`
    Status            RedisClusterStatus `json:"status,omitempty"`
}

4.2 Reconcile 核心逻辑

// internal/controller/rediscluster_controller.go
func (r *RedisClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)

    // 获取 RedisCluster 资源
    redis := &cachev1alpha1.RedisCluster{}
    if err := r.Get(ctx, req.NamespacedName, redis); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 创建或更新 StatefulSet
    sts := r.desiredStatefulSet(redis)
    if err := ctrl.SetControllerReference(redis, sts, r.Scheme); err != nil {
        return ctrl.Result{}, err
    }

    found := &appsv1.StatefulSet{}
    err := r.Get(ctx, types.NamespacedName{Name: sts.Name, Namespace: sts.Namespace}, found)
    if err != nil {
        if errors.IsNotFound(err) {
            log.Info("Creating StatefulSet", "name", sts.Name)
            return ctrl.Result{}, r.Create(ctx, sts)
        }
        return ctrl.Result{}, err
    }

    // 检查是否需要更新
    if *found.Spec.Replicas != redis.Spec.Replicas {
        *found.Spec.Replicas = redis.Spec.Replicas
        log.Info("Updating StatefulSet replicas", "from", *found.Spec.Replicas, "to", redis.Spec.Replicas)
        return ctrl.Result{}, r.Update(ctx, found)
    }

    // 更新 Status
    redis.Status.ReadyReplicas = found.Status.ReadyReplicas
    if redis.Status.ReadyReplicas == redis.Spec.Replicas {
        redis.Status.Phase = "Running"
    } else {
        redis.Status.Phase = "Pending"
    }
    return ctrl.Result{RequeueAfter: 30 * time.Second}, r.Status().Update(ctx, redis)
}

4.3 构建与部署

# 本地构建镜像
make docker-build IMG=yourrepo/redis-operator:v0.1

# 推送到集群可访问的仓库
make docker-push IMG=yourrepo/redis-operator:v0.1

# 部署到集群(包含 CRD + RBAC + Deployment)
make deploy IMG=yourrepo/redis-operator:v0.1

# 验证
kubectl get crd redisclusters.cache.example.com
kubectl get rc -n redis-operator-system

4.4 从定义到部署:完整闭环验证

# 1. 创建 RedisCluster 实例
kubectl apply -f config/samples/cache_v1alpha1_rediscluster.yaml
# rediscluster.cache.example.com/my-redis created

# 2. 观察 Operator 自动创建的子资源
kubectl get statefulset,svc
# NAME                READY   AGE
# statefulset.apps/my-redis 0/3  5s
# NAME                TYPE        CLUSTER-IP
# service/my-redis    ClusterIP   10.96.0.10

# 3. 查看调谐结果(Status 已汇报)
kubectl get rediscluster my-redis -o yaml | grep -A 6 status
# status:
#   conditions:
#   - type: Ready
#     status: "True"
#   phase: Running
#   readyReplicas: 3

# 4. 修改 replicas 触发新一轮调谐
kubectl patch rediscluster my-redis --type=merge -p '{"spec":{"replicas":5}}'
kubectl get rediscluster my-redis -o jsonpath='{.status.readyReplicas}'
# 5

五、Operator Hub 与生态

OperatorHub.io 是 Operator 的官方市场,聚合了社区和厂商维护的成熟 Operator。安装前务必审查其 RBAC 权限和安全评估。

Operator来源管理能力适用场景
Redis EnterpriseRedis Inc集群创建、备份、故障转移生产级 Redis
PostgreSQL (CloudNativePG)Community主从、备份、PVC 管理云原生 PG
Kafka (Strimzi)Red HatBroker、Topic、User 管理消息队列
PrometheusCoreOS集群部署、Thanos 集成监控告警
cert-managerJetstack证书签发、自动轮换TLS 管理
# 使用 OLM(Operator Lifecycle Manager)安装 Operator
# 先安装 OLM
curl -sL https://raw.githubusercontent.com/operator-framework/operator-lifecycle-manager/master/deploy/upstream/install.sh | bash -s v0.26.0

# 搜索可用 Operator
kubectl get packagemanifests | grep redis

# 通过 Subscription 订阅(自动安装和升级)
kubectl create -f - <<EOF
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: redis-enterprise
  namespace: operators
spec:
  channel: stable
  name: redis-enterprise-operator
  source: community-operators
  sourceNamespace: olm
EOF
Operator Maturity Model:社区将 Operator 分为五级——Basic Install → Seamless Upgrades → Full Lifecycle → Deep Insights → Auto Pilot。评估 Operator 时关注其成熟度等级,生产环境建议选择 Level 3+ 的 Operator。

六、状态机设计与 Conditions 约定

成熟的 Operator 不会只用单个 phase 字段表达状态,而是采用"phase 概览 + conditions 明细"的两层模型。conditions 遵循社区约定:字段名大写驼峰,由 type + status + reason + message 四要素组成,status 只能是 True / False / Unknown。

状态机设计

phase触发条件可迁移到的状态
PendingCR 创建、子资源未就绪Running / Failed
Running所有子资源 ReadyDegraded / Failed / Pending(配置变更)
Degraded部分副本可用、依赖故障Running / Failed
Failed不可恢复错误(如 Schema 冲突)Pending(用户修正 spec 后)

Conditions 约定示例

status:
  phase: Running
  conditions:
  - type: Ready
    status: "True"
    reason: AllReplicasReady
    message: 3/3 replicas are ready
  - type: Available
    status: "True"
    reason: ServiceReady
    message: Service my-redis is serving traffic
  - type: Progressing
    status: "False"
    reason: ReconciliationComplete
    message: Last reconcile at 2026-07-31T10:00:00Z
事件 vs Conditions:Event(r.Recorder.Event())是一次性、给人看的瞬时通知;Conditions 是持久化在对象上的状态声明,供控制器与自动化消费。两者都要记录,但别用 Event 做状态判断。

常见错误

  • CRD 创建后 kubectl get 无输出——检查 apiVersion 是否包含正确的 group/version,确认 CRD 的 scope 与资源实例的 namespace 一致;用 kubectl describe crd redisclusters.cache.example.com 查看 Conditions 是否为 Established。
  • Operator 重复创建/删除子资源(Reconcile 风暴)——Reconcile 函数未正确处理 NotFound 或未返回 RequeueAfter,导致无限循环;确保 Delete 后返回空 Result 且不触发 Requeue。
  • RBAC 权限不足导致 Operator 无法 Watch 资源——Kubebuilder 默认生成的 ClusterRole 可能缺少对子资源(如 StatefulSet、Service)的权限,在 config/rbac/role.yaml 中补充 verbs: ["get","list","watch","create","update","patch","delete"]
  • CRD Schema 校验失败拒绝创建实例——openAPIV3Schema 中 required 字段未列出或类型不匹配,用 kubectl apply --dry-run=client -f 提前校验 YAML 语法。
  • Operator 部署后 CrashLoopBackOff——检查容器日志确认是否缺少 leader election 注解或 kubeconfig 配置;本地调试可用 make run 直接在主机运行,绕过容器化部署。

最佳实践

  • 幂等性设计——Reconcile 函数必须保证多次执行结果一致,避免维护全局状态;每次调谐都从 API Server 读取最新状态,不依赖本地缓存。
  • Finalizer 清理——在删除自定义资源前注册 Finalizer,确保关联的子资源(StatefulSet、PVC、Secret)被正确清理,防止资源泄漏。
  • 版本演进策略——CRD 新增版本时保留旧版本(served: true),通过 Conversion Webhook 或多版本 Schema 实现平滑迁移,避免破坏已有实例。
  • 状态汇报及时性——Reconcile 完成后务必更新 Status 子资源,设置合理的 RequeueAfter 间隔(如 30s),让 kubectl get 能反映真实运行状态。
  • 测试与 CI——使用 envtest(controller-runtime 内置)在本地启动 API Server 进行集成测试;将 CRD 校验、Reconcile 逻辑、RBAC 权限纳入 CI 流水线。

练习题

  1. 使用 Kubebuilder 脚手架从零创建一个简单的 ConfigMap Operator:定义 CRD(spec 中包含 key/value),Reconcile 逻辑负责创建/更新对应的 ConfigMap,并用 kubectl get cm 验证。
  2. 为上述 Operator 添加 Finalizer,实现删除自定义资源时自动清理关联的 ConfigMap;用 kubectl delete 测试清理逻辑是否生效。
  3. 在 OperatorHub 中搜索 PostgreSQL Operator(如 CloudNativePG),阅读其 README 了解安装方式和 RBAC 要求,评估其成熟度等级是否满足生产需求。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释 K8s Operator 的控制器模式和 CRD 作用尝试向他人讲解
命令操作能不查文档完成 kubebuilder 项目创建和 CRD 定义在终端实际执行
原理掌握能说出 Operator 的 reconciliation loop 和状态机原理画出流程图
故障排查能独立排查 Operator 控制器不收敛或 CRD 状态异常的问题模拟故障并修复
最佳实践能说明为什么需要为 Operator 编写完善的测试用例对比不同方案

本章总结

CRD 让 Kubernetes API 能表达业务特有的资源,Operator 则把"如何运维这类应用"的专家知识编码进 Reconcile 循环,实现声明式自治。从 Kubebuilder 脚手架出发,把握幂等调谐、Finalizer 清理、RBAC 最小权限与版本演进,即可构建生产级 Operator;已有成熟方案时,优先从 OperatorHub 选用高成熟度 Operator。

延伸阅读

↑ 回到顶部