从零开发一个 Kubernetes Operator:Guestbook 实践
一、引言
在 Kubernetes 生态中,Operator 是一种扩展机制,用于将运维知识编码为软件,实现复杂应用的生命周期自动化管理。无论是数据库、监控系统,还是自定义的业务应用,都可以通过 Operator 来简化部署和运维。本文将带你从零开始,使用 Kubebuilder 框架开发一个简单的 Guestbook Operator,它会根据用户创建的 Guestbook 资源,自动创建对应的 Deployment 和 Service。通过本文,你将理解 CRD 和 Controller 的核心原理,并掌握 Operator 开发的基本流程。
读者收益:
-
理解 CRD 与 Controller 的概念
-
掌握 Kubebuilder 项目的初始化和结构
-
学会定义自定义资源(CRD)和实现调谐逻辑
-
能够将 Operator 部署到集群并进行测试
二、核心概念回顾
在开始编码之前,先明确三个关键概念:
-
CRD(CustomResourceDefinition):扩展 Kubernetes API 的新资源类型,例如我们可以定义一个
Guestbook资源,它就像Pod、Deployment一样成为 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 的开发。回顾整个流程:
-
使用
kubebuilder init初始化项目。 -
使用
kubebuilder create api定义 CRD 和控制器骨架。 -
在
api/v1/*_types.go中定义资源的 Spec 和 Status。 -
在
controllers/*_controller.go中实现调谐逻辑。 -
使用
make install将 CRD 安装到集群。 -
使用
make run本地测试。 -
创建 CR 实例,验证自动创建 Deployment 和 Service。
这个基础架构可以进一步扩展为更复杂的 Operator,例如:
-
添加更多字段(环境变量、资源限制等)
-
实现滚动升级、备份恢复等高级运维功能
-
集成 Prometheus 指标暴露
-
通过 Webhook 进行字段校验
资源推荐:
希望本文能帮助你迈出 Operator 开发的第一步,享受云原生自动化的乐趣。如果你在实践过程中遇到任何问题,欢迎在评论区留言交流。
更多推荐

所有评论(0)