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

Table of Contents
生のclient-goを使ってKubernetes Operatorをゼロから書こうとすると、インフォーマー、リスナー、ワークキュー、イベントレコーダー、リフレクションタイプなど、何千行ものボイラープレートが必要になります。
Operator SDK(Kubebuilderとcontroller-runtimeの上に構築されています)は、この足場(スキャフォールディング)を自動で処理してくれます。これにより、カスタムリソース定義(CRD)スキーマの定義や、調停(reconciliation)ループの実装といったドメインロジックに集中できます。
このガイドでは、安全なクリーンアップのためのファイナライザー、冪等なサブリソース管理、envtestによる自動統合テストを完備した、本番環境に対応したGo言語によるRedisCluster Operatorの構築について説明します。
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マニフェスト。
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
5. Kubernetes Operatorのベストプラクティスのまとめ
controllerutil.CreateOrUpdateを使用する: 競合状態や外部アノテーションの上書きを防ぎます。- 常に
Owns()をSetupWithManagerに登録する: 子リソースの変更が親の調停を即座にトリガーするようにします。 - 外部状態にはファイナライザーを使用する: 接続されたクラウドロードバランサーやAWS EBSストレージボリュームを解放する前に、カスタムリソースを削除しないでください。
- 高速なCIフィードバックのために
envtestを使用する: DockerやMinikubeのオーバーヘッドなしでローカルでテストを実行します。
こちらもおすすめ
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Kubernetes OperatorsとCustom Resources: あらゆるものを自動化する
KubernetesのコントロールプレーンをOperatorとCustom Resource Definition (CRD) で拡張し、複雑なステートフルアプリケーションのライフサイクル管理を自動化する方法を学び、reconciliationループ、RBAC、テスト、本番環境パターンについて解説します。
Read more
FluxによるGitOps:Kubernetesのための宣言的インフラストラクチャとCD
Flux v2による本番環境のGitOps:GitRepositoryソース、Kustomize/Helmリコンシリエーションループ、SOPSによるシークレット復号、コンテナイメージの自動更新。
Read more
Kubernetesにおけるゼロトラストセキュリティの実装: 完全なプロダクションガイド
Kubernetesにおけるフラットネットワークの境界セキュリティを排除するための実用的なガイドで、default-deny NetworkPolicies、SPIFFE/SPIREワークロードアイデンティティ、厳格なmTLSについて解説します。
Read more