Kubernetes Operators: Xây dựng Custom Controller với Operator SDK

Table of Contents
Viết một Kubernetes Operator từ đầu bằng client-go thô đòi hỏi hàng nghìn dòng mã boilerplate: informers, listers, workqueues, event recorders và reflection types.
Operator SDK (được xây dựng trên Kubebuilder và controller-runtime) xử lý phần khung này cho bạn. Nó cho phép bạn tập trung vào logic nghiệp vụ: định nghĩa schema Custom Resource Definition (CRD) của bạn và triển khai vòng lặp đối chiếu.
Hướng dẫn này sẽ hướng dẫn bạn xây dựng một RedisCluster Operator bằng Go sẵn sàng cho sản xuất, hoàn chỉnh với finalizers để dọn dẹp an toàn, quản lý tài nguyên phụ bất biến và kiểm thử tích hợp tự động với envtest.
1. Khởi tạo và tạo khung dự án
Cài đặt Operator SDK và tạo khung một dự án Go mới:
# Initialize operator repository
operator-sdk init \
--domain db.example.com \
--repo github.com/my-org/redis-operator \
--plugins=go/v4
# Generate API (CRD) and Controller
operator-sdk create api \
--group cache \
--version v1alpha1 \
--kind RedisCluster \
--resource \
--controller
Điều này tạo ra cấu trúc dự án:
api/v1alpha1/rediscluster_types.go: Định nghĩa struct Go cho Custom Resource của bạn.internal/controller/rediscluster_controller.go: Vòng lặpReconcile.config/: Kustomize manifests cho CRD, vai trò RBAC và triển khai manager.
2. Định nghĩa Schema CRD
Chỉnh sửa api/v1alpha1/rediscluster_types.go để định nghĩa trạng thái mong muốn (Spec) và trạng thái quan sát được (Status):
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// RedisClusterSpec defines the desired state of RedisCluster
type RedisClusterSpec struct {
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=9
// +kubebuilder:default=3
Replicas int32 `json:"replicas"`
// +kubebuilder:validation:Required
Version string `json:"version"`
// +kubebuilder:default="256Mi"
MemoryLimit string `json:"memoryLimit,omitempty"`
}
// RedisClusterStatus defines the observed state of RedisCluster
type RedisClusterStatus struct {
Nodes []string `json:"nodes,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// RedisCluster is the Schema for the redisclusters API
type RedisCluster struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec RedisClusterSpec `json:"spec,omitempty"`
Status RedisClusterStatus `json:"status,omitempty"`
}
Tạo các tệp YAML CRD của Kubernetes từ các struct Go:
make manifests
3. Triển khai vòng lặp đối chiếu bất biến
Hợp đồng cốt lõi của một bộ điều khiển Kubernetes là đối chiếu kích hoạt theo cấp độ: hàm Reconcile của bạn phải có khả năng chạy lặp lại mà không gây ra tác dụng phụ không mong muốn.
Đây là internal/controller/rediscluster_controller.go:
package controller
import (
"context"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
cachev1alpha1 "github.com/my-org/redis-operator/api/v1alpha1"
)
const redisFinalizer = "cache.db.example.com/finalizer"
type RedisClusterReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=cache.db.example.com,resources=redisclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cache.db.example.com,resources=redisclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
func (r *RedisClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
// 1. Fetch the RedisCluster instance
var cluster cachev1alpha1.RedisCluster
if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil // Object deleted, ignore
}
return ctrl.Result{}, err
}
// 2. Handle Finalizers (Graceful Deletion)
if cluster.ObjectMeta.DeletionTimestamp.IsZero() {
// Register finalizer if not present
if !controllerutil.ContainsFinalizer(&cluster, redisFinalizer) {
controllerutil.AddFinalizer(&cluster, redisFinalizer)
if err := r.Update(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
}
} else {
// Object is being deleted
if controllerutil.ContainsFinalizer(&cluster, redisFinalizer) {
logger.Info("Cleaning up external Redis state...")
// (Perform external cleanup like draining nodes or releasing cloud disks here)
controllerutil.RemoveFinalizer(&cluster, redisFinalizer)
if err := r.Update(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// 3. Reconcile Underlying StatefulSet using CreateOrUpdate
sts := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: cluster.Name + "-nodes",
Namespace: cluster.Namespace,
},
}
opResult, err := controllerutil.CreateOrUpdate(ctx, r.Client, sts, func() error {
// Set owner reference for automatic garbage collection
if err := ctrl.SetControllerReference(&cluster, sts, r.Scheme); err != nil {
return err
}
labels := map[string]string{"app": "redis", "cluster": cluster.Name}
sts.Spec.Replicas = &cluster.Spec.Replicas
sts.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels}
sts.Spec.Template = corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: labels},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "redis",
Image: "redis:" + cluster.Spec.Version,
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse(cluster.Spec.MemoryLimit),
},
},
},
},
},
}
return nil
})
if err != nil {
logger.Error(err, "Failed to reconcile StatefulSet")
return ctrl.Result{}, err
}
if opResult != controllerutil.OperationResultNone {
logger.Info("StatefulSet reconciled", "operation", opResult)
}
// 4. Update Status
cluster.Status.Nodes = []string{cluster.Name + "-0", cluster.Name + "-1"}
if err := r.Status().Update(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
}
func (r *RedisClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&cachev1alpha1.RedisCluster{}).
Owns(&appsv1.StatefulSet{}). // Re-trigger reconcile when child StatefulSet changes
Complete(r)
}
4. Kiểm thử bộ điều khiển nhanh với envtest
Đừng triển khai lên một cụm Kubernetes từ xa cho các bài kiểm thử đơn vị/tích hợp cơ bản. envtest khởi động một etcd và kube-apiserver thực sự cục bộ dưới dạng nhị phân trong vòng chưa đầy 2 giây:
var _ = Describe("RedisCluster Controller", func() {
Context("When creating a RedisCluster", func() {
It("Should create a child StatefulSet successfully", func() {
ctx := context.Background()
cluster := &cachev1alpha1.RedisCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-redis",
Namespace: "default",
},
Spec: cachev1alpha1.RedisClusterSpec{
Replicas: 3,
Version: "7.2-alpine",
},
}
Expect(k8sClient.Create(ctx, cluster)).Should(Succeed())
// Verify StatefulSet is created by the controller
stsLookupKey := types.NamespacedName{Name: "test-redis-nodes", Namespace: "default"}
createdSts := &appsv1.StatefulSet{}
Eventually(func() bool {
err := k8sClient.Get(ctx, stsLookupKey, createdSts)
return err == nil
}, time.Second*10, time.Millisecond*250).Should(BeTrue())
Expect(*createdSts.Spec.Replicas).To(Equal(int32(3)))
})
})
})
Chạy bộ kiểm thử:
make test
5. Tóm tắt các phương pháp hay nhất cho Kubernetes Operators
- Sử dụng
controllerutil.CreateOrUpdate: Ngăn chặn các điều kiện tranh chấp và ghi đè các chú thích bên ngoài. - Luôn đăng ký
Owns()trongSetupWithManager: Đảm bảo các thay đổi tài nguyên con kích hoạt đối chiếu tài nguyên cha ngay lập tức. - Sử dụng Finalizers cho trạng thái bên ngoài: Không bao giờ xóa một Custom Resource trước khi giải phóng các bộ cân bằng tải đám mây hoặc ổ đĩa lưu trữ AWS EBS được đính kèm.
- Sử dụng
envtestđể phản hồi CI nhanh: Các bài kiểm thử chạy cục bộ mà không có Docker hoặc Minikube overhead.
Bạn cũng có thể thích
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Kubernetes Operators và Custom Resources: Tự động hóa mọi thứ
Mở rộng mặt phẳng điều khiển Kubernetes với Operators và Custom Resource Definitions (CRD) để tự động hóa quản lý vòng đời cho các ứng dụng có trạng thái phức tạp, tìm hiểu về vòng lặp đối chiếu, RBAC, kiểm thử và các mẫu sản xuất.
Read more
GitOps với Flux: Hạ tầng khai báo và CD cho Kubernetes
GitOps sản xuất với Flux v2: Nguồn GitRepository, vòng lặp đối chiếu Kustomize/Helm, giải mã bí mật SOPS và cập nhật hình ảnh container tự động.
Read more
Triển khai bảo mật Zero-Trust trong Kubernetes: Hướng dẫn sản xuất hoàn chỉnh
Hướng dẫn thực tế để loại bỏ bảo mật chu vi mạng phẳng trong Kubernetes: NetworkPolicies mặc định từ chối, định danh workload SPIFFE/SPIRE và mTLS nghiêm ngặt.
Read more