Kubernetes Operator 开发实战:从 CRD 到生产级 Controller 的完整路径
引言:Operator 是什么,为什么重要
K8s 的核心理念是声明式 API:用户描述期望状态,控制器持续调和。但 K8s 内置的控制器只能处理内置资源(Pod、Deployment、Service 等),对自定义业务场景无能为力。
比如:
- 怎么让 K8s 知道如何部署一个数据库集群?
- 怎么让 K8s 自动备份 PV?
- 怎么让 K8s 自动扩缩 Redis Cluster?
Operator = CRD + Controller,它把业务运维知识编码到控制器中,让 K8s 真正成为"云时代的操作系统"。
第一章:Operator 核心概念
1.1 CRD(CustomResourceDefinition)
CRD 是 K8s 的资源扩展机制——让你定义新的 API 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: applications.example.com
spec:
group: example.com
scope: Namespaced
names:
plural: applications
singular: application
kind: Application
shortNames: [app]
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
image:
type: string
replicas:
type: integer
minimum: 1
maximum: 10
|
部署后,就可以像 Pod 一样使用 Application:
1
2
3
4
5
6
7
|
apiVersion: example.com/v1
kind: Application
metadata:
name: my-app
spec:
image: nginx:1.25
replicas: 3
|
1.2 Controller
Controller 是 CRD 的"大脑"——监听 CRD 对象变化,把期望状态转换为实际状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
用户创建 Application CR
│
▼
Controller Watch 到变化
│
▼
检查当前状态 vs 期望状态
│
▼
调用 K8s API 创建/更新底层资源
│
▼
持续 Watch,持续调和
|
1.3 Operator = CRD + Controller + 业务知识
把运维一个应用的全部知识封装到 Operator 中:
- 部署顺序、依赖关系
- 升级策略、回滚机制
- 备份恢复、扩缩容
- 监控告警、故障自愈
这正是云原生时代最重要的能力之一——让平台自我管理。
第二章:开发环境搭建
2.1 推荐技术栈
| 工具 |
用途 |
| Kubebuilder |
Operator 脚手架(官方推荐) |
| Operator SDK |
Operator 框架(基于 Kubebuilder) |
| controller-runtime |
核心运行时库 |
| envtest |
本地测试(envtest 用 etcd + kube-apiserver) |
| kind |
本地 K8s 集群测试 |
2.2 安装 Kubebuilder
1
2
3
4
5
6
|
# macOS
brew install kubebuilder
# Linux
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/amd64
chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
|
2.3 创建项目
1
2
|
mkdir myapp-operator && cd myapp-operator
kubebuilder init --domain example.com --repo github.com/myorg/myapp-operator
|
2.4 创建 API(CRD + Controller)
1
2
3
4
5
6
|
kubebuilder create api \
--group apps \
--version v1 \
--kind Application \
--resource \
--controller
|
Kubebuilder 会生成:
1
2
3
|
api/v1/application_types.go # CRD 类型定义
controllers/application_controller.go # Controller 骨架
config/ # Kustomize 部署配置
|
第三章:CRD 类型定义
3.1 完整的 Spec 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
// api/v1/application_types.go
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type ApplicationSpec struct {
// 镜像
Image string `json:"image"`
// 副本数
Replicas *int32 `json:"replicas,omitempty"`
// 端口
Port int32 `json:"port"`
// 环境变量
Env []EnvVar `json:"env,omitempty"`
// 资源限制
Resources ResourceRequirements `json:"resources,omitempty"`
// 自动扩缩
Autoscaling AutoscalingConfig `json:"autoscaling,omitempty"`
}
type EnvVar struct {
Name string `json:"name"`
Value string `json:"value,omitempty"`
ValueFrom *EnvVarSource `json:"valueFrom,omitempty"`
}
type ResourceRequirements struct {
Requests ResourceList `json:"requests,omitempty"`
Limits ResourceList `json:"limits,omitempty"`
}
type ResourceList struct {
Cpu string `json:"cpu,omitempty"`
Memory string `json:"memory,omitempty"`
}
type AutoscalingConfig struct {
Enabled bool `json:"enabled"`
MinReplicas int32 `json:"minReplicas"`
MaxReplicas int32 `json:"maxReplicas"`
TargetCPU int32 `json:"targetCPU"`
}
// 应用状态
type ApplicationStatus struct {
// 阶段:Pending, Running, Failed
Phase string `json:"phase,omitempty"`
// 可用副本数
ReadyReplicas int32 `json:"readyReplicas"`
// Service 端点
Endpoint string `json:"endpoint,omitempty"`
// 最近一次调和时间
LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"`
// 错误信息
Message string `json:"message,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type Application struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ApplicationSpec `json:"spec,omitempty"`
Status ApplicationStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type ApplicationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Application `json:"items"`
}
func init() {
SchemeBuilder.Register(&Application{}, &ApplicationList{})
}
|
3.2 生成 CRD YAML
1
2
|
make manifests # 生成 CRD YAML
make generate # 生成 DeepCopy 方法
|
第四章:Controller 实现
4.1 Reconcile 循环核心
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
// controllers/application_controller.go
func (r *ApplicationReconciler) Reconcile(
ctx context.Context,
req ctrl.Request,
) (ctrl.Result, error) {
log := log.FromContext(ctx)
// 1. 获取 Application 对象
app := &appsv1.Application{}
if err := r.Get(ctx, req.NamespacedName, app); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. 调和 Deployment
if err := r.reconcileDeployment(ctx, app); err != nil {
return ctrl.Result{}, err
}
// 3. 调和 Service
if err := r.reconcileService(ctx, app); err != nil {
return ctrl.Result{}, err
}
// 4. 调和 HPA(如启用)
if app.Spec.Autoscaling.Enabled {
if err := r.reconcileHPA(ctx, app); err != nil {
return ctrl.Result{}, err
}
}
// 5. 更新 Status
if err := r.updateStatus(ctx, app); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: time.Minute}, nil
}
|
4.2 调和 Deployment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
func (r *ApplicationReconciler) reconcileDeployment(
ctx context.Context,
app *appsv1.Application,
) error {
desired := r.buildDeployment(app)
// 获取现有的
existing := &appsv1.Deployment{}
err := r.Get(ctx, types.NamespacedName{
Name: app.Name,
Namespace: app.Namespace,
}, existing)
if errors.IsNotFound(err) {
// 创建
return r.Create(ctx, desired)
} else if err != nil {
return err
}
// 更新
existing.Spec = desired.Spec
return r.Update(ctx, existing)
}
func (r *ApplicationReconciler) buildDeployment(
app *appsv1.Application,
) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: app.Name,
Namespace: app.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: app.Spec.Replicas,
Selector: &metav1.LabelSelector{
MatchLabels: r.labels(app),
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: r.labels(app),
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app",
Image: app.Spec.Image,
Ports: []corev1.ContainerPort{
{ContainerPort: app.Spec.Port},
},
Env: r.buildEnv(app),
Resources: r.buildResources(app),
},
},
},
},
},
}
}
|
4.3 Owner Reference:级联删除
关键设计:让 Deployment 关联到 Application,让删除 Application 时自动删除 Deployment。
1
2
3
4
5
6
7
8
9
|
func (r *ApplicationReconciler) buildDeployment(
app *appsv1.Application,
) *appsv1.Deployment {
dep := &appsv1.Deployment{ /* ... */ }
// 设置 OwnerReference
ctrl.SetControllerReference(app, dep, r.Scheme)
return dep
}
|
4.4 SetupWithManager
1
2
3
4
5
6
7
|
func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1.Application{}).
Owns(&appsv1.Deployment{}). // 监听 Deployment 变化
Owns(&corev1.Service{}).
Complete(r)
}
|
第五章:高级模式
5.1 Finalizers(清理逻辑)
有些资源在删除前需要清理(如外部数据库连接、对象存储文件)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
const applicationFinalizer = "apps.example.com/finalizer"
func (r *ApplicationReconciler) Reconcile(
ctx context.Context,
req ctrl.Request,
) (ctrl.Result, error) {
app := &appsv1.Application{}
if err := r.Get(ctx, req.NamespacedName, app); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 检查是否被删除
if !app.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(app, applicationFinalizer) {
if err := r.finalize(ctx, app); err != nil {
return ctrl.Result{}, err
}
controllerutil.RemoveFinalizer(app, applicationFinalizer)
if err := r.Update(ctx, app); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// 添加 Finalizer
if !controllerutil.ContainsFinalizer(app, applicationFinalizer) {
controllerutil.AddFinalizer(app, applicationFinalizer)
if err := r.Update(ctx, app); err != nil {
return ctrl.Result{}, err
}
}
// ... 正常调和
}
func (r *ApplicationReconciler) finalize(
ctx context.Context,
app *appsv1.Application,
) error {
// 清理外部资源
log := log.FromContext(ctx)
log.Info("Cleaning up external resources",
"name", app.Name)
// 例如:删除对象存储中的备份
// if err := r.s3Client.DeleteObject(app.Name); err != nil {
// return err
// }
return nil
}
|
5.2 状态管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func (r *ApplicationReconciler) updateStatus(
ctx context.Context,
app *appsv1.Application,
) error {
// 查询 Deployment 状态
dep := &appsv1.Deployment{}
if err := r.Get(ctx, types.NamespacedName{
Name: app.Name, Namespace: app.Namespace,
}, dep); err != nil {
return err
}
app.Status.ReadyReplicas = dep.Status.ReadyReplicas
app.Status.Phase = r.computePhase(dep)
app.Status.LastSyncTime = &metav1.Time{Time: time.Now()}
if dep.Status.ReadyReplicas == *app.Spec.Replicas {
app.Status.Phase = "Running"
} else {
app.Status.Phase = "Pending"
}
return r.Status().Update(ctx, app)
}
|
5.3 Webhook 校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// api/v1/application_webhook.go
func (app *Application) ValidateCreate() (admission.Warnings, error) {
if app.Spec.Replicas != nil && *app.Spec.Replicas < 1 {
return nil, field.Invalid(
field.NewPath("spec").Child("replicas"),
*app.Spec.Replicas,
"replicas must be at least 1",
)
}
return nil, nil
}
func (app *Application) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
// 不允许修改 image 字段(只能通过升级路径)
oldApp := old.(*Application)
if app.Spec.Image != oldApp.Spec.Image {
return nil, field.Forbidden(
field.NewPath("spec").Child("image"),
"image is immutable, create new Application instead",
)
}
return nil, nil
}
|
第六章:测试
6.1 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// controllers/application_controller_test.go
var _ = Describe("Application Controller", func() {
Context("When reconciling a new Application", func() {
It("Should create a Deployment", func() {
ctx := context.Background()
app := &appsv1.Application{
Spec: appsv1.ApplicationSpec{
Image: "nginx:1.25",
Port: 80,
},
}
controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: app.Name,
Namespace: app.Namespace,
},
})
deployment := &appsv1.Deployment{}
err := k8sClient.Get(ctx, types.NamespacedName{
Name: app.Name, Namespace: app.Namespace,
}, deployment)
Expect(err).NotTo(HaveOccurred())
Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal("nginx:1.25"))
})
})
})
|
6.2 envtest 集成测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{envtest.NewlineReporter{}},
)
var (
testEnv *envtest.Environment
cfg *rest.Config
k8sClient client.Client
)
BeforeSuite(func() {
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
}
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
})
AfterSuite(func() {
Expect(testEnv.Stop()).To(Succeed())
})
}
|
6.3 E2E 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 用 kind 创建测试集群
kind create cluster --config test/kind.yaml
# 部署 CRD
make install
# 部署 Controller
make run
# 部署测试 Application
kubectl apply -f config/samples/apps_v1_application.yaml
# 验证
kubectl get application
kubectl get deployment
|
第七章:生产部署
7.1 镜像构建与推送
1
2
|
make docker-build IMG=my-registry.com/myapp-operator:v1.0.0
make docker-push IMG=my-registry.com/myapp-operator:v1.0.0
|
7.2 部署到集群
1
|
make deploy IMG=my-registry.com/myapp-operator:v1.0.0
|
7.3 高可用:多副本 + Leader Election
1
2
3
4
5
6
|
// main.go
ctrl.Options{
LeaderElection: true,
LeaderElectionID: "myapp-operator-leader",
LeaderElectionNamespace: "myapp-operator-system",
}
|
1
2
3
|
# 部署多副本
spec:
replicas: 3
|
只有 active 副本执行 Reconcile,其他待命,避免脑裂。
7.4 Prometheus 监控
1
2
3
4
5
6
7
8
9
10
11
|
# ServiceMonitor
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: myapp-operator
spec:
selector:
matchLabels:
app: myapp-operator
endpoints:
- port: metrics
|
默认暴露的指标:
1
2
3
4
|
controller_runtime_reconcile_total
controller_runtime_reconcile_errors_total
controller_runtime_reconcile_time_seconds
workqueue_adds_total
|
第八章:经典 Operator 案例
8.1 MySQL Operator
让 K8s 知道如何部署 MySQL 集群:
1
2
3
4
5
6
7
8
9
10
11
|
apiVersion: mysql.oracle.com/v2
kind: InnoDBCluster
metadata:
name: my-cluster
spec:
instances: 3
router:
instances: 2
secretName: my-cluster-secret
tls:
useSelfSigned: true
|
自动完成:主从复制、备份恢复、滚动升级。
8.2 Redis Operator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
apiVersion: redis.redis.opstreelabs.in/v1beta1
kind: Redis
metadata:
name: redis-cluster
spec:
clusterSize: 3
kubernetesConfig:
image: quay.io/opstree/redis:v7.0.12
storage:
volumeClaimTemplate:
spec:
storageClassName: gp3
resources:
requests:
storage: 10Gi
|
8.3 备份 Operator(Velero)
把"备份"这一业务知识封装到 Operator,让你简单声明:
1
2
3
4
5
6
7
8
9
|
apiVersion: velero.io/v1
kind: Backup
metadata:
name: daily-backup
spec:
schedule: "0 2 * * *"
includedNamespaces:
- production
ttl: 720h
|
第九章:面试高频问题
Q1:Operator 是什么?和 Helm Chart 有什么区别?
答:
- Helm:模板化的部署工具,部署完就退出,不持续管理
- Operator:持续运行的控制器,部署后持续监控、自动修复
Operator 适合复杂应用(数据库、消息队列),需要持续运维;
Helm 适合简单部署场景。
Q2:什么是 Reconcile 循环?
答:
Reconcile 是 Operator 的核心,每次被触发时执行:
- 读取期望状态(CR Spec)
- 读取实际状态(依赖 K8s 资源状态)
- 计算差异
- 调和差异
幂等性是关键:无论执行多少次,最终结果一致。
Q3:CRD 和 CR 是什么关系?
答:
- CRD:CustomResourceDefinition,定义新的资源类型(schema)
- CR:Custom Resource,使用 CRD 定义创建的实际对象
类比:CRD 是 Java 的 Class 定义,CR 是对象实例。
Q4:Owner Reference 有什么作用?
答:建立资源父子关系:
- 删除父资源时自动删除子资源(级联)
- 父资源的状态变更可以传递给子资源
在 Operator 中通常设置 CR 作为 Deployment/Service 的 Owner。
Q5:Finalizer 是什么?
答:Finalizer 是删除前的钩子:
- 当 CR 被删除时,K8s 不立即删除,而是先检查 Finalizer
- Controller 处理 Finalizer 中的清理逻辑(如清理外部资源)
- 处理完成后移除 Finalizer,K8s 才真正删除 CR
保证重要清理逻辑有机会执行。
Q6:如何保证 Operator 高可用?
答:
- 多副本部署:Deployment replicas=3
- Leader Election:通过 lease 保证只有一个 active,其他备份
- 优雅停机:处理 SIGTERM 信号
- Webhooks 多副本:admission webhook 也需要多副本 + 选举
Q7:controller-runtime 比 client-go 优势在哪?
答:
- Informer 缓存:自动维护本地缓存,避免每次访问 API Server
- Manager 模式:统一管理 Controller、Caches、Webhooks 生命周期
- Predicates:灵活的事件过滤
- 优雅的 Builder API:链式配置 reduce 样板代码
第十章:最佳实践
10.1 CRD 设计原则
- 保持稳定:发布后尽量不破坏 API 兼容性
- 字段语义化:避免模糊命名
- 合理状态机:Phase 字段定义清晰状态流转
- Spec vs Status 分离:Spec 是期望,Status 是实际
10.2 Controller 开发原则
- 幂等性:Reconcile 必须幂等
- 单一职责:一个 Controller 只管理一类 CR
- 资源控制:避免内存泄漏,及时关闭连接
- 优雅降级:外部依赖不可用时不要 crash
- 充分测试:单元测试 + 集成测试 + E2E 测试
10.3 发布策略
1
2
3
4
5
6
7
8
9
|
# 1. 开发环境
make install && make run
# 2. 测试环境
make docker-build IMG=registry.dev/myapp-operator:dev
make deploy IMG=registry.dev/myapp-operator:dev
# 3. 生产环境
# 使用 GitOps(ArgoCD)管理 Operator 部署
|
结语:Operator 是云原生的"杀手锏"
掌握 Operator 开发,你就具备了定制 Kubernetes 行为的能力。这意味着:
- 任何复杂系统都能"声明化"
- 业务运维知识可以沉淀到代码中
- 不再受限于 K8s 内置资源的限制
学习建议:
- 先动手:按本文 Demo 实操一遍
- 读源码:cert-manager、ArgoCD 的 Controller
- 做项目:为自己的业务场景写一个 Operator
下一篇,我们将进入云原生存储选型:PV/PVC、CSI 与存储最佳实践。
配套阅读: