一、引言

在 Kubernetes 生态中,Operator 是一种扩展机制,用于将运维知识编码为软件,实现复杂应用的生命周期自动化管理。无论是数据库、监控系统,还是自定义的业务应用,都可以通过 Operator 来简化部署和运维。本文将带你从零开始,使用 Kubebuilder 框架开发一个简单的 Guestbook Operator,它会根据用户创建的 Guestbook 资源,自动创建对应的 Deployment 和 Service。通过本文,你将理解 CRD 和 Controller 的核心原理,并掌握 Operator 开发的基本流程。

读者收益

  • 理解 CRD 与 Controller 的概念

  • 掌握 Kubebuilder 项目的初始化和结构

  • 学会定义自定义资源(CRD)和实现调谐逻辑

  • 能够将 Operator 部署到集群并进行测试


二、核心概念回顾

在开始编码之前,先明确三个关键概念:

  • CRD(CustomResourceDefinition):扩展 Kubernetes API 的新资源类型,例如我们可以定义一个 Guestbook 资源,它就像 PodDeployment 一样成为 Kubernetes 的原生对象。

  • CR(Custom Resource):CRD 的具体实例,例如一个名为 my-guestbook 的 Guestbook 对象,其中包含用户期望的配置(如副本数、镜像等)。

  • Controller(控制器):一段持续运行的代码,监听 CR 的变化(创建、更新、删除),并执行调谐逻辑,使集群状态向用户期望的状态靠拢。例如,当用户创建 Guestbook 时,Controller 会自动创建 Deployment 和 Service。

这种模式将“做什么”(声明式配置)与“怎么做”(自动化逻辑)分离,是云原生应用管理的核心思想。


三、环境准备

在开始之前,请确保以下工具已安装:

  • Go:1.23 或更高版本

  • Kubebuilder:3.x 版本(推荐)

  • Docker:用于构建 Operator 镜像

  • kubectl:能够访问 Kubernetes 集群(可以使用 minikube、kind 或已有集群)

  • make:用于执行 Makefile 中的任务

验证安装:

bash

go version
kubebuilder version
docker --version
kubectl version --client

四、项目初始化

创建一个目录并初始化 Kubebuilder 项目:

bash

mkdir -p ~/projects/guestbook-operator
cd ~/projects/guestbook-operator
kubebuilder init --domain my.domain --repo my.domain/guestbook-operator
  • --domain my.domain:指定 API 组的域名,例如后续的 API 组将是 guestbook.my.domain

  • --repo my.domain/guestbook-operator:Go 模块的导入路径,可以替换为真实的 GitHub 地址(如 github.com/yourname/guestbook-operator)。

初始化后,项目结构如下:

text

.
├── Dockerfile
├── Makefile
├── PROJECT
├── README.md
├── config/          # kustomize 配置(CRD、RBAC、部署清单)
├── hack/            # 辅助脚本
├── go.mod
├── go.sum
└── main.go          # 程序入口

Makefile 包含了常用的构建任务,后续我们会频繁使用它。


五、创建 API(定义 CRD)

使用 kubebuilder create api 命令创建我们的自定义资源 Guestbook

bash

kubebuilder create api --group guestbook --version v1 --kind Guestbook --resource --controller
  • --group guestbook:API 组,结合域名形成 guestbook.my.domain

  • --version v1:API 版本。

  • --kind Guestbook:资源类型名称。

  • --resource:生成 CRD 相关代码。

  • --controller:生成控制器骨架。

当提示 Create Resource [y/n] 和 Create Controller [y/n] 时,均输入 y

此时会生成以下文件:

  • api/v1/guestbook_types.go:定义 Guestbook 结构体(Spec 和 Status)。

  • api/v1/zz_generated.deepcopy.go:自动生成的 DeepCopy 方法。

  • controllers/guestbook_controller.go:控制器逻辑骨架。

  • config/crd/bases/guestbook.my.domain_guestbooks.yaml:CRD 的 YAML 定义。

  • config/samples/guestbook_v1_guestbook.yaml:示例 CR 文件。

5.1 定义 Spec 和 Status

打开 api/v1/guestbook_types.go,修改 GuestbookSpec 和 GuestbookStatus 结构体,添加我们需要的字段:

go

// GuestbookSpec defines the desired state of Guestbook
type GuestbookSpec struct {
	// Number of replicas for the guestbook deployment
	// +kubebuilder:validation:Minimum=1
	// +kubebuilder:validation:Maximum=10
	Replicas int32 `json:"replicas"`

	// Container image for the guestbook
	Image string `json:"image"`

	// Service port to expose
	// +kubebuilder:default=80
	Port int32 `json:"port,omitempty"`
}

// GuestbookStatus defines the observed state of Guestbook
type GuestbookStatus struct {
	// Ready indicates if the guestbook is ready to serve traffic
	Ready bool `json:"ready,omitempty"`
	// DeploymentName is the name of the created deployment
	DeploymentName string `json:"deploymentName,omitempty"`
}

注意 // +kubebuilder:... 注释,它们会在生成 CRD 时产生校验规则和默认值。

5.2 生成代码和 CRD

运行以下命令,根据代码中的注释生成 deepcopy 函数和 CRD 清单:

bash

make generate
make manifests

生成的 CRD 文件位于 config/crd/bases/guestbook.my.domain_guestbooks.yaml,你可以打开查看其中的 OpenAPI 校验规则。


六、实现控制器逻辑

现在编辑 controllers/guestbook_controller.go,实现 Reconcile 函数。该函数是控制器的核心,当 Guestbook 资源发生变化时会被调用。

6.1 导入必要的包

确保文件顶部导入以下包(部分可能已自动添加):

go

import (
	"context"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"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"

	guestbookv1 "my.domain/guestbook-operator/api/v1"
)

6.2 实现 Reconcile 函数

将 Reconcile 函数替换为以下内容:

go

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

	// 1. 获取 Guestbook 实例
	var gb guestbookv1.Guestbook
	if err := r.Get(ctx, req.NamespacedName, &gb); err != nil {
		if errors.IsNotFound(err) {
			// 资源已被删除,无需操作
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, err
	}

	// 2. 定义 Deployment
	deploy := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      gb.Name,
			Namespace: gb.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &gb.Spec.Replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{"app": gb.Name},
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{"app": gb.Name},
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "guestbook",
							Image: gb.Spec.Image,
							Ports: []corev1.ContainerPort{
								{ContainerPort: gb.Spec.Port},
							},
						},
					},
				},
			},
		},
	}

	// 设置 OwnerReference,以便当 Guestbook 被删除时,Deployment 也会被自动清理
	if err := ctrl.SetControllerReference(&gb, deploy, r.Scheme); err != nil {
		return ctrl.Result{}, err
	}

	// 3. 创建或更新 Deployment
	if err := r.upsertDeployment(ctx, deploy); err != nil {
		return ctrl.Result{}, err
	}

	// 4. 定义 Service
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      gb.Name,
			Namespace: gb.Namespace,
		},
		Spec: corev1.ServiceSpec{
			Selector: map[string]string{"app": gb.Name},
			Ports: []corev1.ServicePort{
				{
					Port:       gb.Spec.Port,
					TargetPort: intstr.FromInt(int(gb.Spec.Port)),
				},
			},
			Type: corev1.ServiceTypeClusterIP,
		},
	}
	if err := ctrl.SetControllerReference(&gb, svc, r.Scheme); err != nil {
		return ctrl.Result{}, err
	}

	if err := r.upsertService(ctx, svc); err != nil {
		return ctrl.Result{}, err
	}

	// 5. 更新 Status
	gb.Status.Ready = true
	gb.Status.DeploymentName = deploy.Name
	if err := r.Status().Update(ctx, &gb); err != nil {
		return ctrl.Result{}, err
	}

	log.Info("Guestbook reconciled successfully", "name", gb.Name, "namespace", gb.Namespace)
	return ctrl.Result{}, nil
}

6.3 添加辅助函数

在 Reconcile 函数下方,添加 upsertDeployment 和 upsertService 函数,用于创建或更新资源:

go

// upsertDeployment 创建或更新 Deployment
func (r *GuestbookReconciler) upsertDeployment(ctx context.Context, desired *appsv1.Deployment) error {
	var existing appsv1.Deployment
	err := r.Get(ctx, client.ObjectKeyFromObject(desired), &existing)
	if err != nil {
		if errors.IsNotFound(err) {
			return r.Create(ctx, desired)
		}
		return err
	}
	// 如果存在,则更新(保留 ResourceVersion 避免冲突)
	desired.ResourceVersion = existing.ResourceVersion
	return r.Update(ctx, desired)
}

// upsertService 创建或更新 Service
func (r *GuestbookReconciler) upsertService(ctx context.Context, desired *corev1.Service) error {
	var existing corev1.Service
	err := r.Get(ctx, client.ObjectKeyFromObject(desired), &existing)
	if err != nil {
		if errors.IsNotFound(err) {
			return r.Create(ctx, desired)
		}
		return err
	}
	desired.ResourceVersion = existing.ResourceVersion
	desired.Spec.ClusterIP = existing.Spec.ClusterIP // 保留已分配的 ClusterIP
	return r.Update(ctx, desired)
}

6.4 添加 RBAC 权限注解

在控制器文件顶部(Reconcile 函数上方)添加 RBAC 注解,确保 Operator 有权限操作 Deployment 和 Service:

go

// +kubebuilder:rbac:groups=guestbook.my.domain,resources=guestbooks,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=guestbook.my.domain,resources=guestbooks/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

这些注解会在执行 make manifests 时生成对应的 ClusterRole。

6.5 确保控制器注册

文件末尾的 SetupWithManager 方法已经由 Kubebuilder 生成,无需修改:

go

func (r *GuestbookReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&guestbookv1.Guestbook{}).
		Owns(&appsv1.Deployment{}).
		Owns(&corev1.Service{}).
		Complete(r)
}

七、生成并安装 CRD

在本地测试之前,需要将 CRD 安装到集群中。

bash

make install

该命令会使用 kustomize 构建 config/crd 目录下的资源,并通过 kubectl apply 将 CRD 应用到集群。验证:

bash

kubectl get crd | grep guestbook

输出应包含 guestbooks.guestbook.my.domain


八、本地运行 Operator

启动 Operator,让它监听集群中的 Guestbook 资源:

bash

make run

此时 Operator 会在终端输出日志,并持续运行。保持该终端窗口打开,另开一个终端进行测试。


九、创建 Guestbook 实例

编辑示例文件 config/samples/guestbook_v1_guestbook.yaml,内容如下:

yaml

apiVersion: guestbook.my.domain/v1
kind: Guestbook
metadata:
  name: guestbook-sample
  namespace: default
spec:
  replicas: 2
  image: gcr.io/google-samples/gb-frontend:v4
  port: 80

应用该资源:

bash

kubectl apply -f config/samples/guestbook_v1_guestbook.yaml

观察 Operator 终端输出,你会看到类似:

text

INFO    Guestbook reconciled successfully  {"name": "guestbook-sample", "namespace": "default"}

检查资源是否自动创建:

bash

kubectl get deploy -l app=guestbook-sample
kubectl get svc guestbook-sample

你会看到 Deployment 有 2 个副本,Service 也已创建。查看 Guestbook 的状态:

bash

kubectl get guestbook guestbook-sample -o yaml

在 status 字段中,ready: true 和 deploymentName 已被更新。


十、测试更新与清理

10.1 更新资源

修改 config/samples/guestbook_v1_guestbook.yaml,将 replicas 改为 3,再次 apply:

bash

kubectl apply -f config/samples/guestbook_v1_guestbook.yaml

观察 Deployment 的副本数是否变为 3,Operator 日志中应显示再次调谐。

10.2 删除资源

bash

kubectl delete -f config/samples/guestbook_v1_guestbook.yaml

由于我们设置了 OwnerReference,Deployment 和 Service 也会被自动删除。Operator 日志中不会有错误,因为删除事件被正确处理。

按 Ctrl+C 停止本地运行的 Operator。


十一、总结与展望

通过本文,你已经完成了一个简单的 Kubernetes Operator 的开发。回顾整个流程:

  1. 使用 kubebuilder init 初始化项目。

  2. 使用 kubebuilder create api 定义 CRD 和控制器骨架。

  3. 在 api/v1/*_types.go 中定义资源的 Spec 和 Status。

  4. 在 controllers/*_controller.go 中实现调谐逻辑。

  5. 使用 make install 将 CRD 安装到集群。

  6. 使用 make run 本地测试。

  7. 创建 CR 实例,验证自动创建 Deployment 和 Service。

这个基础架构可以进一步扩展为更复杂的 Operator,例如:

  • 添加更多字段(环境变量、资源限制等)

  • 实现滚动升级、备份恢复等高级运维功能

  • 集成 Prometheus 指标暴露

  • 通过 Webhook 进行字段校验

资源推荐

希望本文能帮助你迈出 Operator 开发的第一步,享受云原生自动化的乐趣。如果你在实践过程中遇到任何问题,欢迎在评论区留言交流。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐