Kubernetes Operator 开发入门:kubebuilder 实战从 CRD 到控制器
从零开始学习 Kubernetes Operator 开发:用 kubebuilder 脚手架创建项目、定义 CRD(自定义资源)、实现 Reconcile 控制循环、部署 Operator 到集群,通过一个完整的 AppDeployment Operator 示例,掌握 K8s 扩展开发的核心模式。
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 描述详细状态(Ready、Progressing、Degraded),比单个字段更具表达力,也更易集成告警。
设置 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 开发的精髓。
