Appearance
Kubernetes 基础与 CRD 概念
本篇是 Kubernetes Operator 开发系列教程的第一篇。在动手写 Operator 之前,我们需要先打牢两个基础:一是回顾 Kubernetes 的核心概念,二是理解自定义资源(CRD)和 Operator 模式的本质。如果你已经对 K8s 比较熟悉,可以快速浏览前两节,重点放在 CRD 和 Operator 模式的部分。本篇目标是为后续编写真正的 Operator 代码做好概念和环境上的准备。
一、Kubernetes 核心概念回顾
Kubernetes(简称 K8s)是一个容器编排系统,负责自动化地部署、扩缩容和管理容器化应用。它把集群抽象成一组声明式的 API 对象,开发者只需描述「我期望集群处于什么状态」,K8s 的控制平面就会持续不断地把实际状态向期望状态收敛。理解下面三个核心对象,是理解一切 Operator 工作方式的前提。
1. Pod
Pod 是 K8s 中最小的可部署单元。一个 Pod 里可以包含一个或多个紧密耦合的容器,它们共享网络命名空间和存储卷,并通过 localhost 互相通信。Pod 的生命周期是短暂的——一旦被销毁,它就彻底消失,K8s 不会尝试「修复」一个具体的 Pod,而是直接创建一个新的来替代它。
yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
limits:
cpu: "500m"
memory: "256Mi"需要特别理解的一点是:Pod 是临时的,它的 IP 会变化,名字也会变化。因此实际生产中几乎不会直接创建裸 Pod,而是通过更高层级的控制器来管理。
2. Deployment
Deployment 是一个更高层级的控制器,它管理一组相同的 Pod(通过 ReplicaSet),并保证任意时刻都有指定数量的副本在运行。当 Pod 挂掉时,Deployment 会自动创建新的 Pod 来补足数量;当需要滚动更新镜像版本时,Deployment 会逐步替换旧 Pod,从而实现零停机发布。
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80Deployment 的工作方式完美地体现了「控制器模式」:它内部有一个控制循环,不断比较 spec.replicas(期望副本数)和实际运行的 Pod 数量,发现差异就做出调整。这个模式正是 Operator 的思想源头——Operator 只是把同样的模式应用到了「自定义资源」上。
3. Service
因为 Pod 的 IP 不稳定,K8s 提供了 Service 来为一组 Pod 提供稳定的访问入口。Service 通过标签选择器(selector)找到对应的 Pod,并为其分配一个稳定的 ClusterIP 和 DNS 名字。无论背后的 Pod 如何创建销毁,Service 的地址都保持不变。
yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: ClusterIP
selector:
app: nginx
ports:
- port: 80
targetPort: 80
protocol: TCP4. 控制器模式的本质
上面三个对象其实都遵循同一个范式——声明式 + 控制循环:
- 用户提交一个声明式的资源清单(YAML),描述期望状态。
- 控制器(运行在控制平面)监视资源变化。
- 控制器对比期望状态和实际状态,执行操作消除差异。
- 控制器持续循环,保证系统始终收敛到期望状态。
这个范式是 Kubernetes 的灵魂,也是 Operator 模式的理论基石。当你理解了 Deployment 控制器是怎么工作的,你就已经理解了一半的 Operator。
二、什么是自定义资源(CRD)
1. 内置资源的局限
K8s 内置了一套丰富的资源类型:Pod、Deployment、Service、ConfigMap、Secret、Job、CronJob 等等。这些资源覆盖了「运行容器」这一核心场景。但是现实世界中有很多更复杂的应用——数据库集群、消息队列、机器学习训练任务、API 网关配置——它们的生命周期管理远比「跑几个容器副本」复杂。
如果只能用内置资源,运维人员就得手动组合 Deployment + Service + ConfigMap + PVC,再写一堆手动操作步骤(建库、初始化账号、配置主从)。这正是 CRD 要解决的问题。
2. CRD 的定义
CRD(CustomResourceDefinition,自定义资源定义) 是 K8s 提供的一种扩展机制,它允许你在集群中注册全新的资源类型。注册之后,你就可以像使用内置资源一样,用 kubectl 创建、查询、修改这种自定义资源,K8s 的 API Server 会负责存储和校验。
举个直观的例子,注册一个名为 RedisCluster 的 CRD 后,你就可以这样创建一个 Redis 集群:
yaml
apiVersion: cache.example.com/v1
kind: RedisCluster
metadata:
name: my-redis
spec:
size: 3
image: redis:7.0
port: 6379这就比手动写 Deployment + Service + ConfigMap 要直观得多。用户只需声明「我要一个 3 节点的 Redis 集群」,至于怎么把这个集群创建出来,就交给 Operator(也就是自定义控制器)来处理。
3. 为什么需要 CRD
CRD 的核心价值在于抽象:
- 领域建模:你可以用贴近业务的术语(比如
RedisCluster、KafkaTopic、PostgresBackup)来描述系统状态,而不是用底层资源堆叠。 - 声明式管理:复用 K8s 声明式 API 的所有好处——版本化、一致性、可审计、可回滚。
- 统一工具链:
kubectl、RBAC、admission webhook、finalizer 等机制对自定义资源同样适用,无需额外造轮子。 - 生态集成:CRD 一旦定义,就可以被 Helm、Kustomize、ArgoCD、GitOps 工具链直接消费。
4. CRD 的关键组成
一个完整的 CRD 定义涉及以下几个核心字段:
- API Group:资源所属的 API 分组,形如
cache.example.com,用于避免命名冲突。 - Version:API 版本,如
v1、v1beta1,支持多版本共存和转换。 - Kind:资源的类型名,驼峰命名,如
RedisCluster。 - Spec:用户填写的「期望状态」,是 CRD 的核心输入。
- Status:控制器填写的「实际状态」,用于反馈系统当前情况。
下面是一个 CRD 定义本身的 YAML 示例(注意这是「定义资源类型」的资源,不是具体实例):
yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: redisclusters.cache.example.com
spec:
group: cache.example.com
names:
kind: RedisCluster
listKind: RedisClusterList
singular: rediscluster
plural: redisclusters
shortNames:
- rc
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- size
- image
properties:
size:
type: integer
minimum: 1
maximum: 10
image:
type: string
port:
type: integer
default: 6379
password:
type: string
status:
type: object
properties:
phase:
type: string
nodes:
type: array
items:
type: string实际开发中我们很少手写这么长的 CRD YAML,而是用 Go 结构体定义类型,再用工具(controller-gen)自动生成。这会在后续章节详细讲解。
三、声明式 API vs 命令式 API
理解这两种 API 风格的差异,对于写出正确的 Operator 至关重要。
1. 命令式 API
命令式 API 的核心是「做这个动作」。比如:
kubectl run nginx --image=nginx:创建一个 Pod。kubectl scale deployment nginx --replicas=5:把副本数扩到 5。- SQL 里的
UPDATE users SET age = 20:把年龄改成 20。
每次操作都是一次「动作」,系统执行完就结束了,不记录意图。命令式 API 的问题在于:当系统状态发生漂移(比如某个 Pod 被人工删掉了),它不会自动纠正,因为「补足副本数」这个意图从未被记录下来。
2. 声明式 API
声明式 API 的核心是「描述期望状态」。比如:
yaml
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 3这表达的不是「把副本数调成 3」这个动作,而是「我期望任何时候都有 3 个副本在运行」这个意图。控制器会持续不断地工作来维护这个意图——副本少了就补,多了就删。
声明式 API 的关键优势:
- 自愈:状态漂移会被自动纠正。
- 幂等:同一个声明提交多次,效果等同于一次。
- 可审计:意图被持久化在 etcd 里,随时可查。
- 可回滚:通过修改声明即可,无需记录「逆向操作」。
3. Operator 必须是声明式的
Operator 本质上就是一个声明式控制器。用户的 CRD 实例描述的是「我期望的集群状态」,而不是「请执行这些步骤」。因此编写 Reconcile 循环时,我们要思考的永远是「期望状态 vs 实际状态」,而不是「发生了什么事件,我要做什么动作」。这一点会在第四章深入展开。
四、Operator 模式
1. 将运维知识编码为软件
Operator 这个概念由 CoreOS(后被 Red Hat 收购)在 2016 年提出。它的核心思想一句话就能说清:把人类运维专家管理某个应用的知识,编码进一个软件控制器里。
以管理一个 PostgreSQL 数据库为例,一个有经验的 DBA 知道:
- 主库挂了要怎么办——提升一个从库为主库。
- 怎么做在线备份——
pg_dump还是pg_basebackup,什么时候做。 - 怎么扩容——加副本、调整参数、迁移数据。
- 升级版本要注意什么——先升从库,验证后再切主。
这些知识传统上存在于运维文档、脚本、甚至老员工的脑子里。Operator 的目标就是把这些知识变成代码:定义一个 PostgresCluster CRD,用户只需声明「我要一个 3 节点高可用 PG 集群、每天备份」,Operator 就自动完成上述所有运维动作。
2. Operator 的核心组件
一个 Operator 由两部分组成:
┌─────────────────────────────────────────┐
│ Operator │
│ │
│ ┌──────────────┐ ┌────────────────┐ │
│ │ CRD │ │ Controller │ │
│ │ (数据模型) │ │ (控制循环) │ │
│ │ │ │ │ │
│ │ Spec ←──→ │ │ Reconcile() │ │
│ │ Status │ │ Watch & Sync │ │
│ └──────────────┘ └────────────────┘ │
└─────────────────────────────────────────┘
↑ ↓
└──── API Server ────────┘
↑
│
etcd 存储- CRD:定义「自定义资源长什么样」,也就是数据模型。它规定了
spec和status的结构。 - Controller:定义「自定义资源该怎么被实现」,也就是控制逻辑。它运行一个 Reconcile 循环,监视 CR 实例和关联资源的变化,把实际状态向期望状态收敛。
二者缺一不可。只有 CRD 没有 Controller,那这个资源就是一堆存在 etcd 里的死数据,没有任何实际效果;只有 Controller 没有 CRD,那它就没有可以监听和操作的对象。
3. Operator 与 Helm、Kustomize 的区别
很多初学者会问:Helm 和 Kustomize 也能把一套复杂的 YAML 模板化、参数化部署,为什么还需要 Operator?它们的区别在于「一次性 vs 持续性」。
| 维度 | Helm / Kustomize | Operator |
|---|---|---|
| 工作时机 | 部署时执行一次 | 持续运行,7x24 监控 |
| 自愈能力 | 无,漂移后需手动重跑 | 有,自动纠正状态漂移 |
| 生命周期 | 只管「创建」这一步 | 覆盖创建、扩缩、升级、备份、故障转移 |
| 复杂运维 | 静态模板,无运行时决策 | 可编码复杂运维逻辑(如主从切换) |
| 知识载体 | YAML 模板 | Go 代码 |
简单总结:Helm/Kustomize 解决的是「怎么把资源方便地部署上去」,Operator 解决的是「怎么让应用持续按预期运行」。它们是互补关系而非替代关系——很多 Operator 内部也会用 Helm 渲染模板,但生命周期管理交给控制循环。
4. 什么场景适合用 Operator
不是所有应用都需要 Operator。一般来说,满足以下条件之一的应用非常适合用 Operator:
- 有复杂的有状态生命周期(数据库、消息队列、分布式存储)。
- 需要自动化故障恢复(主从切换、副本补齐)。
- 有标准化的运维流程需要固化(备份、升级、扩容)。
- 是一个平台级组件,被很多团队复用(统一管理 Kafka Topic、DB 实例)。
而无状态的简单 Web 服务,用 Deployment + HPA 就够了,不必上 Operator。
五、Operator Framework 生态
围绕 Operator 已经形成了一个完整的开源生态,主要由以下工具组成。
1. Operator SDK
Operator SDK 是 Red Hat 主导的工具集,用于 scaffolding(脚手架生成)、构建、测试和打包 Operator。它支持三种实现方式:
- Go:基于 controller-runtime,最主流、性能最好,本系列教程采用这种方式。
- Ansible:用 Ansible playbook 描述运维逻辑,适合不写 Go 的运维团队。
- Helm:把现有 Helm chart 包装成 Operator,迁移成本最低但能力有限。
2. Kubebuilder
Kubebuilder 是 Kubernetes SIG(特别兴趣小组)官方维护的脚手架工具,是 Operator SDK Go 类型实现的底层基础。它负责生成项目骨架、Makefile、CRD 定义、controller 代码模板。本系列教程的代码就是用 Kubebuilder 生成的。Operator SDK 的 Go 路径实际上是在 Kubebuilder 之上做了封装。
Kubebuilder 和 Operator SDK 的关系可以理解为:Kubebuilder 是「内核」,Operator SDK 是「发行版」。对于纯 Go 开发者,二者用起来差别不大,Kubebuilder 更轻量、更贴近上游。
3. OLM(Operator Lifecycle Manager)
OLM 是用来管理 Operator 本身生命周期的组件。它解决的问题是:「Operator 自己怎么被安装、升级、卸载、依赖管理」。OLM 提供了一个 Catalog(目录)机制,让你可以把 Operator 发布到一个集中的仓库,集群管理员通过 OLM 一键安装并管理版本。Red Hat OpenShift 内置了 OLM。
对于本系列教程,我们主要使用 Kubebuilder 进行开发,OLM 在最后一章发布部分会简要介绍。
4. controller-runtime 与 controller-gen
这两个是底层库和工具:
- controller-runtime:提供 Manager、Controller、Client、Cache、Webhook 等基础抽象,是写 Operator 控制循环的核心依赖。
- controller-gen:从 Go 结构体的注释(
+kubebuilder标记)生成 CRD 的 YAML 定义和 deepcopy 代码。
第二章会深入讲解 controller-runtime 的架构。
六、环境准备
在正式开始写代码之前,需要把开发环境准备好。本节给出在 Linux / macOS / Windows(WSL2 推荐)下的安装步骤。
1. 安装 Go
Operator 开发需要 Go 1.20 及以上版本。到 Go 官网 下载对应平台的安装包。安装后验证:
bash
go version
# go version go1.22.0 linux/amd64建议配置好国内代理以加速依赖下载:
bash
go env -w GOPROXY=https://goproxy.cn,direct2. 安装 kubectl
kubectl 是与 K8s 集群交互的命令行工具。安装方法可参考官方文档。安装后验证:
bash
kubectl version --client3. 安装本地集群:kind 或 minikube
开发阶段不需要一个真正的生产集群,本地集群足够。推荐使用 kind(Kubernetes in Docker),它轻量、启动快、支持多节点。
安装 kind:
bash
# Linux/macOS
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# 或者用 Go 安装
go install sigs.k8s.io/kind@latest创建一个集群:
bash
kind create cluster --name operator-dev
kubectl cluster-info --context kind-operator-dev如果你更习惯 minikube,也可以:
bash
minikube start --cpus=4 --memory=4g4. 安装 Kubebuilder
Kubebuilder 提供了命令行工具用于生成项目骨架。安装方式:
bash
# 下载最新 release
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
chmod +x kubebuilder
sudo mv kubebuilder /usr/local/bin/
# 验证
kubebuilder version5. 安装 controller-gen 和其他工具
Kubebuilder 项目依赖几个代码生成工具,通常通过 Makefile 中的 target 自动安装,但也可以手动装:
bash
go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest
go install sigs.k8s.io/kustomize/kustomize/v5@latest6. 验证环境
执行下面命令确认所有工具就位:
bash
go version
kubectl version --client
kind version
kubebuilder version
controller-gen --version如果以上命令都能正常输出,说明环境已经准备完成,可以进入下一章开始写代码了。
七、小结
本篇回顾了 Kubernetes 的核心概念,并系统介绍了 CRD 与 Operator 模式。要点回顾:
- K8s 的本质是声明式 + 控制循环。Pod、Deployment、Service 都遵循这一范式,Operator 只是把范式延伸到了自定义资源。
- CRD 是 K8s 的扩展机制,允许你注册自定义资源类型,复用 K8s 的全部 API 机制(kubectl、RBAC、admission、存储)。
- 声明式 API 关注「期望状态」,而非「执行动作」。这是 Operator 自愈和幂等能力的来源。
- Operator = CRD + Controller,把运维专家的知识编码进软件,实现应用生命周期自动化。
- 生态工具:Kubebuilder/Operator SDK 做脚手架,controller-runtime 做运行时,OLM 管理发布。
- 环境准备:Go + kubectl + kind + kubebuilder,这是后续所有章节的基础。
下一篇我们会深入 controller-runtime 的架构,并用 Kubebuilder 初始化第一个 Operator 项目骨架。理解了这些底层组件之后,再去写 Reconcile 循环就会水到渠成。