•7 min read

Kubernetes Operators: Operator SDKによるカスタムコントローラーの構築

Kubernetes Operators: Operator SDKによるカスタムコントローラーの構築

生のclient-goを使ってKubernetes Operatorをゼロから書こうとすると、インフォーマー、リスナー、ワークキュー、イベントレコーダー、リフレクションタイプなど、何千行ものボイラープレートが必要になります。

Operator SDK(Kubebuilderとcontroller-runtimeの上に構築されています)は、この足場(スキャフォールディング)を自動で処理してくれます。これにより、カスタムリソース定義(CRD)スキーマの定義や、調停(reconciliation)ループの実装といったドメインロジックに集中できます。

このガイドでは、安全なクリーンアップのためのファイナライザー、冪等なサブリソース管理、envtestによる自動統合テストを完備した、本番環境に対応したGo言語によるRedisCluster Operatorの構築について説明します。


Audio Briefing
0:00 / 0:00

1. プロジェクトの初期化とスキャフォールディング

Operator SDKをインストールし、新しいGoプロジェクトをスキャフォールドします。

# 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

これにより、以下のプロジェクト構造が作成されます。

  • api/v1alpha1/rediscluster_types.go: カスタムリソースのGo構造体定義。
  • internal/controller/rediscluster_controller.go: Reconcileループ。
  • config/: CRD、RBACロール、マネージャーデプロイメント用のKustomizeマニフェスト。

Advertisement

2. CRDスキーマの定義

api/v1alpha1/rediscluster_types.goを編集して、望ましい状態(Spec)と観測された状態(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"`
}

Go構造体からKubernetes CRD YAMLファイルを生成します。

make manifests

3. 冪等な調停ループの実装

Kubernetesコントローラーの核となる契約は、**レベルトリガー型調停(level-triggered reconciliation)**です。つまり、Reconcile関数は、意図しない副作用を引き起こすことなく繰り返し実行できる必要があります。

以下は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. envtestを使ったコントローラーの高速テスト

基本的な単体/統合テストのために、リモートのKubernetesクラスターにデプロイしないでください。envtestは、実際のetcdとkube-apiserverをバイナリ形式でローカルに2秒未満で起動します。

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)))
		})
	})
})

テストスイートを実行します。

make test

Advertisement

5. Kubernetes Operatorのベストプラクティスのまとめ

  1. controllerutil.CreateOrUpdateを使用する: 競合状態や外部アノテーションの上書きを防ぎます。
  2. 常にOwns()をSetupWithManagerに登録する: 子リソースの変更が親の調停を即座にトリガーするようにします。
  3. 外部状態にはファイナライザーを使用する: 接続されたクラウドロードバランサーやAWS EBSストレージボリュームを解放する前に、カスタムリソースを削除しないでください。
  4. 高速なCIフィードバックのためにenvtestを使用する: DockerやMinikubeのオーバーヘッドなしでローカルでテストを実行します。

こちらもおすすめ

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とCustom Resources: あらゆるものを自動化する
kubernetes

Kubernetes OperatorsとCustom Resources: あらゆるものを自動化する

KubernetesのコントロールプレーンをOperatorとCustom Resource Definition (CRD) で拡張し、複雑なステートフルアプリケーションのライフサイクル管理を自動化する方法を学び、reconciliationループ、RBAC、テスト、本番環境パターンについて解説します。

Read more