技术博客

Kubernetes Operator 开发入门:kubebuilder 实战从 CRD 到控制器

从零开始学习 Kubernetes Operator 开发:用 kubebuilder 脚手架创建项目、定义 CRD(自定义资源)、实现 Reconcile 控制循环、部署 Operator 到集群,通过一个完整的 AppDeployment Operator 示例,掌握 K8s 扩展开发的核心模式。

KubernetesOperatorkubebuilderCRDGo云原生开发

Kubernetes Operator 是“把运维知识编写成代码”的模式:你把管理某类应用的操作步骤(部署、扩缩容、备份、故障恢复)写成控制器,K8s 就能自动执行这些操作。本文用 kubebuilder 实现一个简单的 Operator,讲清楚 CRD、控制器、Reconcile 循环这三个核心概念。

为什么需要 Operator?

传统方式:运维手动操作
kubectl scale deployment myapp --replicas=5
kubectl apply -f backup-job.yaml
kubectl rollout restart deployment myapp

Operator 方式:声明期望状态,控制器自动执行
kubectl apply -f myapp-instance.yaml
# 控制器自动:创建 Deployment、配置 HPA、设置定时备份、监控并自动重启

Operator = CRD(自定义资源类型)+ Controller(控制循环)


一、环境准备

# 安装 Go(1.21+)
wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
go version

# 安装 kubebuilder
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 version

# 安装 controller-gen(生成 RBAC、CRD 等)
go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest

# 准备本地开发集群(用 kind 或 minikube)
kind create cluster --name operator-dev
kubectl cluster-info

二、创建 Operator 项目

我们将创建一个 AppDeployment Operator:接受简化的应用描述,自动管理 Deployment + Service + HPA。

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

# 初始化项目(Go module 名 + 域名)
kubebuilder init --domain zdyedu.cn --repo github.com/zdyedu/appdeployment-operator

# 查看生成的项目结构
tree -L 2
# .
# ├── cmd/
# │   └── main.go          ← 程序入口
# ├── config/              ← Kustomize 配置(RBAC、CRD、Webhook 等)
# ├── internal/
# │   └── controller/      ← 控制器逻辑放这里
# ├── api/                 ← CRD 类型定义放这里(create api 后生成)
# ├── go.mod
# └── Makefile

# 创建 API(即 CRD)
kubebuilder create api \
  --group apps \
  --version v1alpha1 \
  --kind AppDeployment
# Create Resource? [y/n]: y   → 创建 CR 类型
# Create Controller? [y/n]: y → 创建控制器

三、定义 CRD(自定义资源类型)

// api/v1alpha1/appdeployment_types.go

package v1alpha1

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

// AppDeploymentSpec 是用户描述期望状态的字段
type AppDeploymentSpec struct {
    // 应用镜像
    // +kubebuilder:validation:Required
    Image string `json:"image"`

    // 副本数,默认 1
    // +kubebuilder:validation:Minimum=0
    // +kubebuilder:validation:Maximum=100
    // +kubebuilder:default=1
    Replicas *int32 `json:"replicas,omitempty"`

    // 服务端口
    // +kubebuilder:validation:Required
    Port int32 `json:"port"`

    // 是否启用 HPA 自动扩缩容
    // +kubebuilder:default=false
    EnableHPA bool `json:"enableHPA,omitempty"`

    // HPA 配置(当 EnableHPA=true 时有效)
    HPAConfig *HPAConfig `json:"hpaConfig,omitempty"`

    // 环境变量
    Env []EnvVar `json:"env,omitempty"`
}

type HPAConfig struct {
    MinReplicas *int32 `json:"minReplicas,omitempty"`
    MaxReplicas int32  `json:"maxReplicas"`
    // CPU 利用率目标(百分比)
    // +kubebuilder:default=70
    CPUUtilization *int32 `json:"cpuUtilization,omitempty"`
}

type EnvVar struct {
    Name  string `json:"name"`
    Value string `json:"value"`
}

// AppDeploymentStatus 是控制器写入的实际状态
type AppDeploymentStatus struct {
    // 当前实际副本数
    ReadyReplicas int32 `json:"readyReplicas,omitempty"`

    // 条件(用于 kubectl get 时显示状态)
    // +listType=map
    // +listMapKey=type
    Conditions []metav1.Condition `json:"conditions,omitempty"`

    // 应用访问地址
    ServiceEndpoint string `json:"serviceEndpoint,omitempty"`

    // 最后更新时间
    LastUpdated *metav1.Time `json:"lastUpdated,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Image",type=string,JSONPath=`.spec.image`
// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.spec.replicas`
// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// AppDeployment 是 CRD 的主体类型
type AppDeployment struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   AppDeploymentSpec   `json:"spec,omitempty"`
    Status AppDeploymentStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true
type AppDeploymentList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`
    Items           []AppDeployment `json:"items"`
}

func init() {
    SchemeBuilder.Register(&AppDeployment{}, &AppDeploymentList{})
}
# 生成 CRD YAML 和 DeepCopy 方法
make generate
make manifests

# 查看生成的 CRD
cat config/crd/bases/apps.zdyedu.cn_appdeployments.yaml | head -50

四、实现 Reconcile 控制器

控制器的核心是 Reconcile 函数:每次资源状态变化时被调用,比较“期望状态”和“实际状态”,执行操作使其一致。

// internal/controller/appdeployment_controller.go

package controller

import (
    "context"
    "fmt"
    appsv1alpha1 "github.com/zdyedu/appdeployment-operator/api/v1alpha1"
    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    autoscalingv2 "k8s.io/api/autoscaling/v2"
    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/util/intstr"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/log"
)

// AppDeploymentReconciler 是控制器主结构
type AppDeploymentReconciler struct {
    client.Client
    Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=apps.zdyedu.cn,resources=appdeployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps.zdyedu.cn,resources=appdeployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete

// Reconcile 是控制器的核心逻辑
func (r *AppDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := log.FromContext(ctx)

    // 1. 获取 CR(用户定义的 AppDeployment 对象)
    app := &appsv1alpha1.AppDeployment{}
    if err := r.Get(ctx, req.NamespacedName, app); err != nil {
        if errors.IsNotFound(err) {
            // CR 已被删除,不需要处理(K8s GC 会清理 OwnerReference 关联的资源)
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }

    logger.Info("Reconciling AppDeployment", "name", app.Name)

    // 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 err := r.reconcileHPA(ctx, app); err != nil {
        return ctrl.Result{}, err
    }

    // 5. 更新 CR 的 Status
    if err := r.updateStatus(ctx, app); err != nil {
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

// reconcileDeployment 确保 Deployment 与期望状态一致
func (r *AppDeploymentReconciler) reconcileDeployment(ctx context.Context, app *appsv1alpha1.AppDeployment) error {
    deployment := &appsv1.Deployment{}
    err := r.Get(ctx, client.ObjectKey{Name: app.Name, Namespace: app.Namespace}, deployment)

    // 构建期望的 Deployment
    desired := r.buildDeployment(app)

    if errors.IsNotFound(err) {
        // 不存在则创建
        return r.Create(ctx, desired)
    } else if err != nil {
        return err
    }

    // 存在则更新(只更新需要改变的字段)
    deployment.Spec.Replicas = desired.Spec.Replicas
    deployment.Spec.Template.Spec.Containers[0].Image = app.Spec.Image
    return r.Update(ctx, deployment)
}

func (r *AppDeploymentReconciler) buildDeployment(app *appsv1alpha1.AppDeployment) *appsv1.Deployment {
    replicas := app.Spec.Replicas
    if replicas == nil {
        one := int32(1)
        replicas = &one
    }

    labels := map[string]string{
        "app.kubernetes.io/name":       app.Name,
        "app.kubernetes.io/managed-by": "appdeployment-operator",
    }

    // 构建容器环境变量
    var envVars []corev1.EnvVar
    for _, e := range app.Spec.Env {
        envVars = append(envVars, corev1.EnvVar{
            Name:  e.Name,
            Value: e.Value,
        })
    }

    deployment := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      app.Name,
            Namespace: app.Namespace,
            Labels:    labels,
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: replicas,
            Selector: &metav1.LabelSelector{MatchLabels: labels},
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{Labels: labels},
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{{
                        Name:  app.Name,
                        Image: app.Spec.Image,
                        Ports: []corev1.ContainerPort{{
                            ContainerPort: app.Spec.Port,
                        }},
                        Env: envVars,
                    }},
                },
            },
        },
    }

    // 设置 OwnerReference(CR 删除时自动级联删除 Deployment)
    ctrl.SetControllerReference(app, deployment, r.Scheme)
    return deployment
}

func (r *AppDeploymentReconciler) reconcileService(ctx context.Context, app *appsv1alpha1.AppDeployment) error {
    svc := &corev1.Service{}
    err := r.Get(ctx, client.ObjectKey{Name: app.Name, Namespace: app.Namespace}, svc)

    labels := map[string]string{"app.kubernetes.io/name": app.Name}
    desired := &corev1.Service{
        ObjectMeta: metav1.ObjectMeta{
            Name:      app.Name,
            Namespace: app.Namespace,
        },
        Spec: corev1.ServiceSpec{
            Selector: labels,
            Ports: []corev1.ServicePort{{
                Port:       app.Spec.Port,
                TargetPort: intstr.FromInt32(app.Spec.Port),
            }},
            Type: corev1.ServiceTypeClusterIP,
        },
    }
    ctrl.SetControllerReference(app, desired, r.Scheme)

    if errors.IsNotFound(err) {
        return r.Create(ctx, desired)
    }
    return err
}

func (r *AppDeploymentReconciler) reconcileHPA(ctx context.Context, app *appsv1alpha1.AppDeployment) error {
    hpa := &autoscalingv2.HorizontalPodAutoscaler{}
    err := r.Get(ctx, client.ObjectKey{Name: app.Name, Namespace: app.Namespace}, hpa)

    if !app.Spec.EnableHPA {
        // 不需要 HPA,如果存在则删除
        if !errors.IsNotFound(err) {
            return r.Delete(ctx, hpa)
        }
        return nil
    }

    // 需要 HPA,创建或更新
    cfg := app.Spec.HPAConfig
    if cfg == nil {
        cfg = &appsv1alpha1.HPAConfig{MaxReplicas: 10}
    }
    cpuUtil := int32(70)
    if cfg.CPUUtilization != nil {
        cpuUtil = *cfg.CPUUtilization
    }

    desired := &autoscalingv2.HorizontalPodAutoscaler{
        ObjectMeta: metav1.ObjectMeta{Name: app.Name, Namespace: app.Namespace},
        Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
            ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
                APIVersion: "apps/v1",
                Kind:       "Deployment",
                Name:       app.Name,
            },
            MinReplicas: cfg.MinReplicas,
            MaxReplicas: cfg.MaxReplicas,
            Metrics: []autoscalingv2.MetricSpec{{
                Type: autoscalingv2.ResourceMetricSourceType,
                Resource: &autoscalingv2.ResourceMetricSource{
                    Name: corev1.ResourceCPU,
                    Target: autoscalingv2.MetricTarget{
                        Type:               autoscalingv2.UtilizationMetricType,
                        AverageUtilization: &cpuUtil,
                    },
                },
            }},
        },
    }
    ctrl.SetControllerReference(app, desired, r.Scheme)

    if errors.IsNotFound(err) {
        return r.Create(ctx, desired)
    }
    return nil
}

func (r *AppDeploymentReconciler) updateStatus(ctx context.Context, app *appsv1alpha1.AppDeployment) error {
    deployment := &appsv1.Deployment{}
    if err := r.Get(ctx, client.ObjectKey{Name: app.Name, Namespace: app.Namespace}, deployment); err != nil {
        return err
    }

    app.Status.ReadyReplicas = deployment.Status.ReadyReplicas
    app.Status.ServiceEndpoint = fmt.Sprintf("%s.%s.svc.cluster.local:%d",
        app.Name, app.Namespace, app.Spec.Port)
    now := metav1.Now()
    app.Status.LastUpdated = &now

    return r.Status().Update(ctx, app)
}

// SetupWithManager 注册控制器监听的资源
func (r *AppDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&appsv1alpha1.AppDeployment{}).   // 主要监听 AppDeployment 变化
        Owns(&appsv1.Deployment{}).           // 也监听控制器拥有的 Deployment 变化
        Owns(&corev1.Service{}).              // 监听 Service 变化
        Owns(&autoscalingv2.HorizontalPodAutoscaler{}).
        Complete(r)
}

五、部署和测试

# 安装 CRD 到集群
make install

# 本地运行控制器(开发调试用)
make run

# 创建一个 CR 实例测试
cat << 'EOF' | kubectl apply -f -
apiVersion: apps.zdyedu.cn/v1alpha1
kind: AppDeployment
metadata:
  name: web-app
  namespace: default
spec:
  image: nginx:1.27
  replicas: 2
  port: 80
  enableHPA: true
  hpaConfig:
    minReplicas: 2
    maxReplicas: 10
    cpuUtilization: 70
  env:
  - name: ENV
    value: production
EOF

# 观察控制器工作
kubectl get appdeployment web-app
# NAME      IMAGE        REPLICAS   READY   AGE
# web-app   nginx:1.27   2          2       30s

kubectl get deployment,service,hpa
# 控制器自动创建了 Deployment、Service、HPA

# 修改副本数,观察 Deployment 自动更新
kubectl patch appdeployment web-app \
  --type=merge -p '{"spec":{"replicas":5}}'
# 构建并推送镜像(生产部署)
make docker-build docker-push IMG=registry.cn-shanghai.aliyuncs.com/zdyedu/appdeployment-operator:v0.1.0

# 部署到集群
make deploy IMG=registry.cn-shanghai.aliyuncs.com/zdyedu/appdeployment-operator:v0.1.0

# 查看 Operator Pod
kubectl get pods -n appdeployment-operator-system
kubectl logs -n appdeployment-operator-system -l control-plane=controller-manager

Operator 开发最佳实践

幂等性(Idempotency): Reconcile 可能被多次调用(网络抖动、Watch 事件),每次都应该产生相同结果。检查资源是否存在再决定 Create 或 Update,不要假设只会调用一次。

错误处理与重试: 返回 ctrl.Result{RequeueAfter: time.Second*30} 可以在 30 秒后重新触发 Reconcile,适合等待外部资源就绪的场景。

状态条件(Conditions):metav1.Condition 描述详细状态(ReadyProgressingDegraded),比单个字段更具表达力,也更易集成告警。

设置 OwnerReference: 通过 ctrl.SetControllerReference 建立父子关系,CR 删除时 K8s GC 自动清理所有子资源,不需要在代码里手动删除。

小结

kubebuilder 让 Operator 开发从零到可用只需几百行 Go 代码。核心流程:kubebuilder create api 生成脚手架 → 编辑 _types.go 定义 CRD 字段 → 实现 Reconcile 控制循环(Get 期望状态 → 对比实际状态 → Create/Update 使其一致)→ make install && make run 本地测试。理解了 Reconcile 循环的幂等性设计,就掌握了 Operator 开发的精髓。