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
前置知识
- 6.2:Kubernetes 入门 Kubernetes 入门——Deployment、Service、ConfigMap 等核心对象
- 6.1:容器底层原理 容器底层原理——镜像分层与容器运行机制
- 3.1:Docker 容器入门 Docker 容器入门——镜像构建与容器生命周期管理
- k8s05 K8s:CNI网络插件——Pod 网络模型与 Service 通信原理
一、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 写法 | 常用约束 | 典型用途 |
|---|---|---|---|
| string | type: string | minLength: 3、enum: [a, b]、pattern: "^v\\d+" | 版本号、名称 |
| integer | type: integer | minimum: 1、maximum: 100 | 副本数、端口 |
| boolean | type: boolean | default: false | 开关型参数 |
| array | type: array + items: | maxItems: 10、uniqueItems: true | 节点列表、标签集合 |
| object | type: 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 中做映射,避免已有实例数据损坏。
二、Operator 模式——Reconciliation Loop
CRD 只定义了"长什么样",Operator 负责"怎么管"。Operator 的核心是一个持续运行的 Reconciliation Loop(调谐循环):
- Watch(监听)自定义资源的变化事件
- 读取当前状态(Actual State)
- 对比期望状态(Desired State)
- 执行调谐动作,使当前状态趋近期望状态
- 更新 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)
}
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 更新时序
- Reconcile 开始 → 读取对象 → 先写入
status.phase = "Pending",让用户立即看到进展 - 创建子资源 → 等待其 Ready(此阶段返回
RequeueAfter: 10s而不是直接报错) - 子资源就绪 →
phase = "Running",同步readyReplicas与conditions - 调谐失败 → 写入
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 Enterprise | Redis Inc | 集群创建、备份、故障转移 | 生产级 Redis |
| PostgreSQL (CloudNativePG) | Community | 主从、备份、PVC 管理 | 云原生 PG |
| Kafka (Strimzi) | Red Hat | Broker、Topic、User 管理 | 消息队列 |
| Prometheus | CoreOS | 集群部署、Thanos 集成 | 监控告警 |
| cert-manager | Jetstack | 证书签发、自动轮换 | 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
六、状态机设计与 Conditions 约定
成熟的 Operator 不会只用单个 phase 字段表达状态,而是采用"phase 概览 + conditions 明细"的两层模型。conditions 遵循社区约定:字段名大写驼峰,由 type + status + reason + message 四要素组成,status 只能是 True / False / Unknown。
状态机设计
| phase | 触发条件 | 可迁移到的状态 |
|---|---|---|
| Pending | CR 创建、子资源未就绪 | Running / Failed |
| Running | 所有子资源 Ready | Degraded / 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
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 流水线。
练习题
- 使用 Kubebuilder 脚手架从零创建一个简单的 ConfigMap Operator:定义 CRD(spec 中包含 key/value),Reconcile 逻辑负责创建/更新对应的 ConfigMap,并用
kubectl get cm验证。 - 为上述 Operator 添加 Finalizer,实现删除自定义资源时自动清理关联的 ConfigMap;用
kubectl delete测试清理逻辑是否生效。 - 在 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。
延伸阅读
- Kubebuilder 官方文档——从零构建 Operator 的最佳实践
- Operator SDK——Red Hat 主导的 Operator 开发框架
- OperatorHub.io——查找社区维护的成熟 Operator
- K8s 官方:扩展 API——CRD 与聚合 API 的区别
- controller-runtime——Operator 底层依赖的运行时库
- Operator Pattern 详解——深入理解 Reconciliation Loop 设计