Kubernetes Operator开发实战:用Operator-sdk构建自定义控制器

2026-05-07 21:14 Kubernetes Operator开发实战:用Operator-sdk构建自定义控制器已关闭评论

Kubernetes Operator开发实战:用Operator-sdk构建自定义控制器

结论先行: 用Operator-sdk开发自定义控制器,关键在于理解“期望状态”和“实际状态”的调和循环(Reconcile Loop)。本文会从一个真实例子出发:构建一个能自动扩缩容的Web应用Operator,记录从初始化到部署的所有踩坑点,最后你会得到一个能生产使用的控制器骨架。


1. 为什么不用Helm而写Operator?

如果你只想把一组YAML包起来,用Helm就够了。但当你的应用需要根据集群状态自动做出反应(比如:数据库主从切换、自动恢复、无状态服务灰度扩缩),就需要Operator。Operator本质是一个自定义控制器,它Watch自定义资源(CRD)的变化,然后调用K8s API去调整状态。

我踩的第一个坑:不要把Operator写成“定时脚本”。Operator的Reconcile函数会被CRD创建/更新/删除事件触发,也会周期性地被队列重试触发。不要在Reconcile里写for { select {} },那样会阻塞控制器。


2. 环境准备

你需要:
- Go 1.20+
- Docker
- Kind 或 Minikube(本地集群)
- Operator-sdk v1.30+

# 安装operator-sdk
export ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)
export OS=$(uname | awk '{print tolower($0)}')
export OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.32.0
curl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}
chmod +x operator-sdk_${OS}_${ARCH} && sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdk

注意: 不要用go install operator-sdk,否则版本可能不对,后续脚手架会报错。


3. 初始化项目

我打算创建一个叫webapp-operator的项目,管理的自定义资源叫WebApp,它有一个规格Replicas(期望副本数)和一个状态字段AvailableReplicas(可用副本数)。

mkdir -p webapp-operator && cd webapp-operator
operator-sdk init --domain mycompany.io --repo github.com/mycompany/webapp-operator

这时会生成一堆文件,核心是main.gocontrollers/。接着创建API和控制器:

operator-sdk create api --group app --version v1 --kind WebApp --resource --controller

这会创建:
- api/v1/webapp_types.go:定义CRD的Spec和Status
- controllers/webapp_controller.go:Reconcile逻辑


4. 定义自定义资源(CRD)

编辑api/v1/webapp_types.go,定义期望状态和状态字段:

package v1

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

// WebAppSpec 定义期望状态
type WebAppSpec struct {
    // +kubebuilder:validation:Minimum=1
    // +kubebuilder:validation:Maximum=100
    Replicas int32 `json:"replicas"`
    Image    string `json:"image,omitempty"`
}

// WebAppStatus 定义实际状态
type WebAppStatus struct {
    AvailableReplicas int32 `json:"availableReplicas"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas"
// +kubebuilder:printcolumn:name="Available",type="integer",JSONPath=".status.availableReplicas"

type WebApp struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`
    Spec   WebAppSpec   `json:"spec,omitempty"`
    Status WebAppStatus `json:"status,omitempty"`
}

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

func init() {
    SchemeBuilder.Register(&WebApp{}, &WebAppList{})
}

关键点: +kubebuilder:subresource:status 必须加,否则无法更新Status字段。

然后执行make generatemake manifests来生成CRD的YAML。

踩坑:忘记加+kubebuilder:subresource:status,导致在Reconcile里调用Status().Update()时报错“the server could not find the requested resource”。


5. 实现Reconcile逻辑

现在打开controllers/webapp_controller.go,核心逻辑如下:

func (r *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)

    // 1. 获取WebApp实例
    var webapp appv1.WebApp
    if err := r.Get(ctx, req.NamespacedName, &webapp); err != nil {
        if k8serrors.IsNotFound(err) {
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }

    // 2. 定义期望的Deployment
    deploy := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      webapp.Name,
            Namespace: webapp.Namespace,
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: ptr.To(webapp.Spec.Replicas),
            Selector: &metav1.LabelSelector{
                MatchLabels: map[string]string{"app": webapp.Name},
            },
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: map[string]string{"app": webapp.Name},
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Name:  "web",
                            Image: webapp.Spec.Image,
                        },
                    },
                },
            },
        },
    }

    // Set OwnerReference
    if err := ctrl.SetControllerReference(&webapp, deploy, r.Scheme); err != nil {
        return ctrl.Result{}, err
    }

    // 3. 创建或更新Deployment
    found := &appsv1.Deployment{}
    err := r.Get(ctx, types.NamespacedName{Name: deploy.Name, Namespace: deploy.Namespace}, found)
    if err != nil && k8serrors.IsNotFound(err) {
        log.Info("Creating Deployment", "name", deploy.Name)
        if err := r.Create(ctx, deploy); err != nil {
            return ctrl.Result{}, err
        }
    } else if err != nil {
        return ctrl.Result{}, err
    } else {
        // 更新副本数
        if found.Spec.Replicas != deploy.Spec.Replicas {
            found.Spec.Replicas = deploy.Spec.Replicas
            if err := r.Update(ctx, found); err != nil {
                return ctrl.Result{}, err
            }
        }
    }

    // 4. 获取实际可用副本数
    var podList corev1.PodList
    if err := r.List(ctx, &podList, client.MatchingLabels{"app": webapp.Name}); err != nil {
        return ctrl.Result{}, err
    }
    var available int32
    for _, pod := range podList.Items {
        for _, cond := range pod.Status.Conditions {
            if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
                available++
            }
        }
    }

    // 5. 更新Status
    if webapp.Status.AvailableReplicas != available {
        webapp.Status.AvailableReplicas = available
        if err := r.Status().Update(ctx, &webapp); err != nil {
            return ctrl.Result{}, err
        }
    }

    return ctrl.Result{RequeueAfter: 30 * time.Second}, nil // 每30秒重新调和,保证状态一致
}

踩坑记录:
- OwnerReference一定要设置,否则删除WebApp时Deployment不会被级联删除。
- ptr.To() 而不是直接取地址,因为Replicas*int32类型。
- Status更新必须用r.Status().Update(),不能用r.Update(),后者会更新整个对象导致冲突。


6. 本地运行与测试

先创建CRD:

make install

在另一个终端运行控制器(不部署到集群,便于调试):

make run

然后创建示例WebApp资源(保存为config/samples/app_v1_webapp.yaml):

apiVersion: app.mycompany.io/v1
kind: WebApp
metadata:
  name: webapp-sample
spec:
  replicas: 3
  image: nginx:alpine

应用:

kubectl apply -f config/samples/app_v1_webapp.yaml

你会看到控制器日志输出“Creating Deployment”,然后可用副本数逐渐变为3。

测试更新replicas:

kubectl patch webapp webapp-sample --type='merge' -p '{"spec":{"replicas":5}}'

控制器会检测到变化并更新Deployment。然后kubectl get webapp会看到AvailableReplicas变为5。


7. 容器化与部署到集群

make docker-build IMG=myregistry/webapp-operator:latest构建镜像,然后推送到仓库。接着用make deploy IMG=myregistry/webapp-operator:latest部署到集群。

注意:如果你本地用Kind,需要先把镜像load到集群:

kind load docker-image myregistry/webapp-operator:latest

我在这里卡了半小时:直接make deploy后Pod一直CrashLoopBackOff,原因是Role权限不足。默认生成的config/rbac/role.yaml只给了基本的get/list/watch权限,但我的Reconcile里需要create deploymentupdate status,需要手动在controllers/webapp_controller.go文件头增加注解:

//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
//+kubebuilder:rbac:groups=app.mycompany.io,resources=webapps/status,verbs=update

然后重新make manifests生成RBAC YAML,再make deploy


8. 踩坑总结与改进方向

原因 解决方案
Status更新报错 缺少subresource注解 +kubebuilder:subresource:status
删除WebApp后Deployment残留 未设置OwnerReference 使用ctrl.SetControllerReference
权限不足 RBAC注解遗漏 检查所有涉及资源的verbs
控制器无限重启 Reconcile里发生错误且没有重试逻辑 返回ctrl.Result{}加error,或设置RequeueAfter
镜像拉取失败 未load到Kind集群 kind load docker-image

扩展思考:
- 如果WebApp的Spec里增加了“环境变量”或“ingress”,如何优雅扩展?
- 如何为Operator编写单元测试?operator-sdk提供了envtest可以启动本地apiserver。
- 生产环境建议用make release打包为OLM格式,方便在OperatorHub上分发。

最后,Operator不是万能的——如果你的应用不需要自主决策,一个简单的Helm Chart加ArgoCD足矣。但当自动修复、自动扩缩、复杂状态机出现时,Operator就是最锋利的刀。现在你可以把这篇手记当作脚手架,去构建你的第一个生产级Operator了。

你可能感兴趣的文章

来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
转载请注明出处: https://teachcourse.cn/4109.html ,谢谢支持!

资源分享

013-tail命令过滤2024-08-01 零点 ~2024-08-01 05点半时间段内的nginx日志 013-tail命令过滤2024-08-01 零点
纠结怎么开启Windows图片阅览功能呢? 纠结怎么开启Windows图片阅览功
Python常用100个关键字详细示例(5) Python常用100个关键字详细示例
005-Microsoft SQL Server Management Studio 18如何调试存储过程中的sql代码 005-Microsoft SQL Server

评论已关闭!