Kubernetes Operator 开发实战:从 CRD 到生产级 Controller 的完整路径

系统讲解 Kubernetes Operator 开发全流程,从 CRD 设计、controller-runtime 框架、Reconcile 循环到生产级测试与发布,帮助你掌握云原生时代最重要的扩展机制

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 的核心,每次被触发时执行:

  1. 读取期望状态(CR Spec)
  2. 读取实际状态(依赖 K8s 资源状态)
  3. 计算差异
  4. 调和差异

幂等性是关键:无论执行多少次,最终结果一致。

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 高可用?

答:

  1. 多副本部署:Deployment replicas=3
  2. Leader Election:通过 lease 保证只有一个 active,其他备份
  3. 优雅停机:处理 SIGTERM 信号
  4. Webhooks 多副本:admission webhook 也需要多副本 + 选举

Q7:controller-runtime 比 client-go 优势在哪?

答:

  • Informer 缓存:自动维护本地缓存,避免每次访问 API Server
  • Manager 模式:统一管理 Controller、Caches、Webhooks 生命周期
  • Predicates:灵活的事件过滤
  • 优雅的 Builder API:链式配置 reduce 样板代码

第十章:最佳实践

10.1 CRD 设计原则

  1. 保持稳定:发布后尽量不破坏 API 兼容性
  2. 字段语义化:避免模糊命名
  3. 合理状态机:Phase 字段定义清晰状态流转
  4. Spec vs Status 分离:Spec 是期望,Status 是实际

10.2 Controller 开发原则

  1. 幂等性:Reconcile 必须幂等
  2. 单一职责:一个 Controller 只管理一类 CR
  3. 资源控制:避免内存泄漏,及时关闭连接
  4. 优雅降级:外部依赖不可用时不要 crash
  5. 充分测试:单元测试 + 集成测试 + 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 内置资源的限制

学习建议:

  1. 先动手:按本文 Demo 实操一遍
  2. 读源码:cert-manager、ArgoCD 的 Controller
  3. 做项目:为自己的业务场景写一个 Operator

下一篇,我们将进入云原生存储选型:PV/PVC、CSI 与存储最佳实践。


配套阅读:

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
使用 Hugo 构建
主题 Stack 由 Jimmy 设计