•6 min read

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

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

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.


Audio Briefing
0:00 / 0:00

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ặp Reconcile.
  • config/: Kustomize manifests cho CRD, vai trò RBAC và triển khai manager.

Advertisement

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

Advertisement

5. Tóm tắt các phương pháp hay nhất cho Kubernetes Operators

  1. 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.
  2. Luôn đăng ký Owns() trong SetupWithManager: Đả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.
  3. 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.
  4. 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

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Kubernetes Operators và Custom Resources: Tự động hóa mọi thứ
kubernetes

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