Kubernetes Operator - 解説

このドキュメントでは、Kubernetes Operatorの実装における設計判断、技術的詳細、パフォーマンス最適化、そしてよくある間違いとその回避策について詳しく解説します。

---

目次

---

1. 実装の設計判断とその理由

1.1 なぜcontroller-runtimeを使うのか

選択肢の比較:

アプローチ メリット デメリット 適用場面
**client-go直接使用** 完全な制御、軽量 大量のボイラープレート、エラーハンドリングが複雑 特殊なユースケース、既存システムへの組み込み
**controller-runtime** 抽象化されたAPI、ベストプラクティスが組み込み やや重い、内部動作の理解が必要 標準的なOperator開発(推奨)
**Kubebuilder** scaffoldingツール、プロジェクト構造の標準化 controller-runtime依存 新規プロジェクト立ち上げ
**Operator SDK** 追加のヘルパー、OLM統合 Red Hat色が強い、学習コスト エンタープライズ環境

推奨:controller-runtime + Kubebuilder

理由:

  • コミュニティ標準: Kubernetes SIGが公式にサポート
  • 抽象化のバランス: 低レベルすぎず、高レベルすぎない
  • 拡張性: カスタマイズが必要な場合でも柔軟に対応可能
  • 豊富なエコシステム: 多くのOperatorがこのパターンを採用
  • 1.2 Status Subresourceの設計判断

    なぜStatusを分離するのか:

    // +kubebuilder:subresource:status
    type MyApp struct {
        metav1.TypeMeta   `json:",inline"`
        metav1.ObjectMeta `json:"metadata,omitempty"`
    
        Spec   MyAppSpec   `json:"spec,omitempty"`     // ユーザーが編集
        Status MyAppStatus `json:"status,omitempty"`   // Operatorのみが更新
    }
    

    メリット:

  • Optimistic Concurrency Control: SpecとStatusの更新が独立し、競合が減る
  • RBAC: ユーザーはSpecのみ編集権限、Statusは読み取り専用に設定可能
  • 明確な責任分離: 「望ましい状態」(Spec) と「現在の状態」(Status) が明確
  • Statusの設計原則:

    type MyAppStatus struct {
        // 1. Conditions: 標準化されたステータス表現(Kubernetes標準)
        Conditions []metav1.Condition `json:"conditions,omitempty"`
    
        // 2. Observable Facts: 観測可能な事実のみ記録
        ReadyReplicas     int32 `json:"readyReplicas,omitempty"`
        AvailableReplicas int32 `json:"availableReplicas,omitempty"`
    
        // 3. Generation Tracking: Specとの同期状態を追跡
        ObservedGeneration int64 `json:"observedGeneration,omitempty"`
    
        // 4. Timestamp: デバッグ用
        LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
    }
    

    1.3 Owner Referenceによるガベージコレクション

    設計判断:すべての子リソースにOwner Referenceを設定

    func (r *Reconciler) createDeployment(ctx context.Context, app *MyApp) error {
        dep := &appsv1.Deployment{...}
    
        // Owner Referenceを設定(必須)
        if err := ctrl.SetControllerReference(app, dep, r.Scheme); err != nil {
            return err
        }
    
        return r.Create(ctx, dep)
    }
    

    Owner Reference設定の効果:

  • 自動削除: 親リソース(MyApp)削除時、子リソース(Deployment, Service)も自動削除
  • ライフサイクル連動: Kubernetesの標準的なガベージコレクション機構を活用
  • kubectl describeでの可視化: 親子関係が明確に表示される
  • 注意点:

    // ❌ 間違い:namespace間のOwner Referenceは不可
    // MyApp (namespace: app-ns)
    // Deployment (namespace: default)  // これはエラーになる
    
    // ✅ 正しい:同一namespace内のみ
    // MyApp (namespace: app-ns)
    // Deployment (namespace: app-ns)
    
    // クラスタースコープリソースの場合
    // ClusterRole → RoleBinding (異なるnamespace) は可能
    

    1.4 Idempotency(冪等性)の保証

    設計原則:Reconcile関数は何度実行しても同じ結果になるべき

    func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
        // ❌ 間違い:毎回リソースを作成しようとする
        dep := &appsv1.Deployment{...}
        if err := r.Create(ctx, dep); err != nil {
            return ctrl.Result{}, err  // AlreadyExistsエラーで失敗
        }
    
        // ✅ 正しい:既存チェック → 作成 or 更新
        found := &appsv1.Deployment{}
        err := r.Get(ctx, types.NamespacedName{...}, found)
        if err != nil {
            if apierrors.IsNotFound(err) {
                // 存在しない → 作成
                return r.Create(ctx, dep)
            }
            return ctrl.Result{}, err
        }
    
        // 存在する → 必要なら更新
        if !reflect.DeepEqual(found.Spec, dep.Spec) {
            found.Spec = dep.Spec
            return r.Update(ctx, found)
        }
    
        return ctrl.Result{}, nil  // 変更なし
    }
    

    Idempotencyのベストプラクティス:

  • Server-Side Apply (SSA): Kubernetes 1.18+で導入された宣言的な更新方法
  • import "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    
    func (r *Reconciler) reconcileDeployment(ctx context.Context, app *MyApp) error {
        dep := &appsv1.Deployment{...}
    
        // Server-Side Applyを使用(推奨)
        result, err := controllerutil.CreateOrUpdate(ctx, r.Client, dep, func() error {
            // 望ましい状態を設定
            dep.Spec.Replicas = &app.Spec.Replicas
            dep.Spec.Template.Spec.Containers[0].Image = app.Spec.Image
    
            // Owner Referenceを設定
            return ctrl.SetControllerReference(app, dep, r.Scheme)
        })
    
        if err != nil {
            return err
        }
    
        log.Info("Deployment reconciled", "result", result)
        // result: Created, Updated, Unchanged
    
        return nil
    }
    

    ---

    2. Reconciliation Loopの詳細動作

    2.1 Reconciliation Loopのライフサイクル

    ┌──────────────────────────────────────────────────────────┐
    │                    Watch Events                          │
    │  (Create, Update, Delete on MyApp, Deployment, Service)  │
    └─────────────────────┬────────────────────────────────────┘
                          │
                          ▼
                ┌─────────────────┐
                │   Workqueue     │  ← Rate Limiting
                │  (key: ns/name) │  ← Deduplication
                └────────┬────────┘
                         │
                         ▼
              ┌──────────────────────┐
              │  Reconcile(ctx, req) │
              └──────────┬───────────┘
                         │
            ┌────────────┴────────────┐
            │                         │
            ▼                         ▼
       Success                    Error
       return                     return
       (Result{}, nil)            (Result{}, err)
            │                         │
            ▼                         ▼
       Remove from queue         Retry with
                                 exponential backoff
    

    2.2 Watchの仕組み

    SetupWithManagerの詳細:

    func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
        return ctrl.NewControllerManagedBy(mgr).
            For(&examplev1.MyApp{}).              // Primary Watch: MyApp自体の変更
            Owns(&appsv1.Deployment{}).           // Secondary Watch: DeploymentのOwner参照
            Owns(&corev1.Service{}).              // Secondary Watch: ServiceのOwner参照
            Watches(                              // Custom Watch: 任意のリソース
                &source.Kind{Type: &corev1.ConfigMap{}},
                handler.EnqueueRequestsFromMapFunc(r.findObjectsForConfigMap),
                builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
            ).
            WithEventFilter(predicate.GenerationChangedPredicate{}). // Filter無駄なイベント
            WithOptions(controller.Options{
                MaxConcurrentReconciles: 3,       // 並列実行数
            }).
            Complete(r)
    }
    

    各Watchの動作:

  • For (Primary Watch):
- MyAppリソース自体の変更を監視 - Create, Update, Deleteすべてのイベントをキャプチャ

  • Owns (Secondary Watch):
- Owner Referenceが設定された子リソースの変更を監視 - 子リソースの変更 → 親リソース(MyApp)のReconcileが呼ばれる

// 例:Deploymentが手動で削除された場合
// 1. Deploymentの削除イベント発生
// 2. Owner ReferenceからMyAppを特定
// 3. MyAppのReconcileが呼ばれる
// 4. Reconcile内でDeploymentが存在しないことを検出
// 5. Deploymentを再作成

  • Custom Watches:
- 任意のリソースを監視し、関連するMyAppをキューに追加

func (r *Reconciler) findObjectsForConfigMap(cm client.Object) []reconcile.Request {
    // このConfigMapを参照しているMyAppを検索
    var myAppList MyAppList
    r.List(context.Background(), &myAppList)

    var requests []reconcile.Request
    for _, app := range myAppList.Items {
        // app.Spec.ConfigMapRefと一致するか確認
        if app.Spec.ConfigMapRef == cm.GetName() {
            requests = append(requests, reconcile.Request{
                NamespacedName: types.NamespacedName{
                    Name:      app.Name,
                    Namespace: app.Namespace,
                },
            })
        }
    }
    return requests
}

2.3 Requeueの戦略

Reconcile関数の戻り値パターン:

// パターン1: 成功、再実行不要
return ctrl.Result{}, nil

// パターン2: 成功、一定時間後に再実行
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil

// パターン3: 成功、すぐに再実行(キューの末尾に追加)
return ctrl.Result{Requeue: true}, nil

// パターン4: エラー、Exponential Backoffで再実行
return ctrl.Result{}, fmt.Errorf("failed to reconcile: %w", err)

// パターン5: エラーだが再実行不要(Status subresourceに記録)
myApp.Status.Conditions = append(myApp.Status.Conditions, ...)
r.Status().Update(ctx, myApp)
return ctrl.Result{}, nil  // エラーを返さない

Requeueのユースケース:

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    myApp := &MyApp{}
    if err := r.Get(ctx, req.NamespacedName, myApp); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // ケース1: 外部APIの状態を定期的にチェック
    if myApp.Spec.ExternalServiceEnabled {
        status, err := r.checkExternalService(myApp)
        if err != nil {
            // 一時的なエラー → 30秒後に再試行
            return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
        }
        // 定期的な監視継続
        return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
    }

    // ケース2: Deploymentのロールアウト完了を待つ
    deployment := &appsv1.Deployment{}
    if err := r.Get(ctx, req.NamespacedName, deployment); err != nil {
        return ctrl.Result{}, err
    }

    if deployment.Status.UpdatedReplicas != *deployment.Spec.Replicas {
        // ロールアウト中 → 10秒後に再チェック
        return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
    }

    // ケース3: 恒久的なエラー(ユーザー設定ミス)
    if myApp.Spec.Replicas > 100 {
        // Statusに記録し、エラーを返さない(無限リトライを避ける)
        meta.SetStatusCondition(&myApp.Status.Conditions, metav1.Condition{
            Type:    "Invalid",
            Status:  metav1.ConditionTrue,
            Reason:  "ReplicasExceedsLimit",
            Message: "Replicas must be <= 100",
        })
        r.Status().Update(ctx, myApp)
        return ctrl.Result{}, nil
    }

    return ctrl.Result{}, nil
}

---

3. パフォーマンス最適化

3.1 Cacheの活用

controller-runtimeのCache機構:

// Getは常にCacheから読み取る(デフォルト)
myApp := &MyApp{}
err := r.Get(ctx, req.NamespacedName, myApp)
// → APIサーバーへのHTTPリクエストは発生しない
// → Informerが保持しているCacheから取得

// Listもキャッシュを使用
var myAppList MyAppList
err := r.List(ctx, &myAppList)
// → namespace内のすべてのMyAppをCacheから取得

Cache最適化のベストプラクティス:

// 1. 必要なNamespaceのみキャッシュ
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
    Cache: cache.Options{
        DefaultNamespaces: map[string]cache.Config{
            "production": {},
            "staging":    {},
        },
    },
})

// 2. 特定のリソースのみキャッシュ
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
    Cache: cache.Options{
        ByObject: map[client.Object]cache.ByObject{
            &corev1.Pod{}: {
                // Labelセレクタでフィルタリング
                Label: labels.SelectorFromSet(labels.Set{
                    "app": "myapp",
                }),
            },
        },
    },
})

// 3. Cacheを使わずAPI Serverに直接問い合わせ(特殊ケース)
directClient, err := client.New(cfg, client.Options{})
realTimeMyApp := &MyApp{}
err = directClient.Get(ctx, req.NamespacedName, realTimeMyApp)
// → APIサーバーへの直接リクエスト(Cacheをバイパス)

3.2 Rate Limitingの設定

デフォルトのRate Limiter:

// workqueue.DefaultControllerRateLimiter()の内部実装
rateLimiter := workqueue.NewMaxOfRateLimiter(
    // 1. Item-level exponential backoff
    workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second),

    // 2. Bucket rate limiter (全体のスループット制限)
    &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
)

カスタムRate Limiterの設定:

func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&examplev1.MyApp{}).
        WithOptions(controller.Options{
            // 並列実行数を制限
            MaxConcurrentReconciles: 5,

            // カスタムRate Limiter
            RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(
                100*time.Millisecond,  // 初回リトライ: 100ms
                60*time.Second,        // 最大リトライ間隔: 60s
            ),
        }).
        Complete(r)
}

Rate Limitingのシナリオ別設定:

シナリオ 推奨設定 理由
**高頻度更新** (例: メトリクス収集) `MaxConcurrentReconciles: 10`, 短いbackoff スループット重視
**重い処理** (例: バックアップ) `MaxConcurrentReconciles: 1-3`, 長いbackoff API負荷軽減
**外部API連携** カスタムRate Limiter (外部APIのレート制限に合わせる) 外部サービス保護

3.3 Predicates(イベントフィルタリング)

不要なReconcileを防ぐ:

import "sigs.k8s.io/controller-runtime/pkg/predicate"

func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&examplev1.MyApp{}).
        WithEventFilter(predicate.Funcs{
            // Createイベント: 常に処理
            CreateFunc: func(e event.CreateEvent) bool {
                return true
            },

            // Updateイベント: Generationが変わった時のみ処理
            UpdateFunc: func(e event.UpdateEvent) bool {
                oldGen := e.ObjectOld.GetGeneration()
                newGen := e.ObjectNew.GetGeneration()

                // Generation変更なし = Status更新のみ → Skip
                if oldGen == newGen {
                    return false
                }

                return true
            },

            // Deleteイベント: 常に処理
            DeleteFunc: func(e event.DeleteEvent) bool {
                return true
            },

            // Generic: 常に処理
            GenericFunc: func(e event.GenericEvent) bool {
                return true
            },
        }).
        Complete(r)
}

組み込みPredicatesの活用:

// 1. GenerationChangedPredicate: Spec変更時のみ
predicate.GenerationChangedPredicate{}

// 2. ResourceVersionChangedPredicate: ResourceVersion変更時のみ
predicate.ResourceVersionChangedPredicate{}

// 3. AnnotationChangedPredicate: Annotation変更時のみ
predicate.AnnotationChangedPredicate{}

// 4. LabelChangedPredicate: Label変更時のみ
predicate.LabelChangedPredicate{}

// 5. 複数のPredicateを組み合わせ
predicate.And(
    predicate.GenerationChangedPredicate{},
    predicate.NewPredicateFuncs(func(object client.Object) bool {
        // カスタムロジック: 特定のLabelを持つリソースのみ
        return object.GetLabels()["reconcile"] == "enabled"
    }),
)

3.4 Batch処理とDebouncing

頻繁な更新をまとめる:

type MyAppReconciler struct {
    client.Client
    Scheme *runtime.Scheme

    // Debounce用のMap
    lastReconcile sync.Map  // map[types.NamespacedName]time.Time
}

func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // Debouncing: 最後のReconcileから5秒以内なら Skip
    if lastTime, ok := r.lastReconcile.Load(req.NamespacedName); ok {
        if time.Since(lastTime.(time.Time)) < 5*time.Second {
            return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
        }
    }

    // Reconcile処理
    // ...

    // 最終実行時刻を記録
    r.lastReconcile.Store(req.NamespacedName, time.Now())

    return ctrl.Result{}, nil
}

---

4. よくある間違いと回避策

4.1 Status更新による無限ループ

問題のコード:

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    myApp := &MyApp{}
    r.Get(ctx, req.NamespacedName, myApp)

    // 毎回Statusを更新 → Watch発火 → 再度Reconcile → 無限ループ
    myApp.Status.LastReconcile = metav1.Now()
    r.Status().Update(ctx, myApp)

    return ctrl.Result{}, nil
}

解決策1: 変更がある場合のみ更新

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    myApp := &MyApp{}
    r.Get(ctx, req.NamespacedName, myApp)

    // 元のStatusを保存
    originalStatus := myApp.Status.DeepCopy()

    // Reconciliationロジック
    r.reconcileDeployment(ctx, myApp)

    // Statusが変わった場合のみ更新
    if !reflect.DeepEqual(originalStatus, &myApp.Status) {
        myApp.Status.LastReconcile = metav1.Now()
        if err := r.Status().Update(ctx, myApp); err != nil {
            return ctrl.Result{}, err
        }
    }

    return ctrl.Result{}, nil
}

解決策2: GenerationChangedPredicateを使用

func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&MyApp{}).
        WithEventFilter(predicate.GenerationChangedPredicate{}). // Status更新はSkip
        Complete(r)
}

4.2 リソースリークの防止

問題:Owner Reference設定忘れ

// ❌ 間違い
func (r *Reconciler) createService(ctx context.Context, app *MyApp) error {
    svc := &corev1.Service{
        ObjectMeta: metav1.ObjectMeta{
            Name:      app.Name,
            Namespace: app.Namespace,
        },
        Spec: corev1.ServiceSpec{...},
    }

    // Owner Reference未設定 → 親削除時に子が残る
    return r.Create(ctx, svc)
}

解決策:必ずOwner Referenceを設定

// ✅ 正しい
func (r *Reconciler) createService(ctx context.Context, app *MyApp) error {
    svc := &corev1.Service{
        ObjectMeta: metav1.ObjectMeta{
            Name:      app.Name,
            Namespace: app.Namespace,
        },
        Spec: corev1.ServiceSpec{...},
    }

    // Owner Reference設定(必須)
    if err := ctrl.SetControllerReference(app, svc, r.Scheme); err != nil {
        return fmt.Errorf("failed to set owner reference: %w", err)
    }

    return r.Create(ctx, svc)
}

検証方法:

# 子リソースのOwner Referenceを確認
kubectl get deployment my-app -o jsonpath='{.metadata.ownerReferences}'

# 期待される出力
[{"apiVersion":"example.com/v1","kind":"MyApp","name":"my-app","uid":"...","controller":true}]

4.3 Contextのキャンセル未対応

問題:長時間実行される処理でContextを確認しない

// ❌ 間違い
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    for i := 0; i < 1000; i++ {
        // Contextのキャンセルを確認しない
        time.Sleep(1 * time.Second)
        r.processItem(i)
    }
    return ctrl.Result{}, nil
}

解決策:定期的にContext確認

// ✅ 正しい
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    for i := 0; i < 1000; i++ {
        // Contextがキャンセルされていないか確認
        select {
        case <-ctx.Done():
            return ctrl.Result{}, ctx.Err()
        default:
            // 処理続行
        }

        time.Sleep(1 * time.Second)
        if err := r.processItem(ctx, i); err != nil {
            return ctrl.Result{}, err
        }
    }
    return ctrl.Result{}, nil
}

func (r *Reconciler) processItem(ctx context.Context, i int) error {
    // 外部API呼び出し時も必ずContextを渡す
    req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com", nil)
    resp, err := http.DefaultClient.Do(req)
    // ...
    return nil
}

4.4 並行実行の競合

問題:共有状態への安全でないアクセス

// ❌ 間違い
type Reconciler struct {
    client.Client
    cache map[string]string  // 複数のGoroutineから同時アクセス → Race Condition
}

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    r.cache[req.Name] = "processing"  // Race!
    // ...
    return ctrl.Result{}, nil
}

解決策1: sync.Mapを使用

// ✅ 正しい
type Reconciler struct {
    client.Client
    cache sync.Map  // thread-safe
}

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    r.cache.Store(req.Name, "processing")
    // ...
    return ctrl.Result{}, nil
}

解決策2: Mutexで保護

type Reconciler struct {
    client.Client
    mu    sync.Mutex
    cache map[string]string
}

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    r.mu.Lock()
    r.cache[req.Name] = "processing"
    r.mu.Unlock()
    // ...
    return ctrl.Result{}, nil
}

---

5. 発展的学習への道筋

5.1 Advanced Topics

1. Webhooks(Admission Controllers)

// Validating Webhook: リソース作成・更新時のバリデーション
func (r *MyApp) ValidateCreate() error {
    if r.Spec.Replicas > 100 {
        return fmt.Errorf("replicas must be <= 100")
    }
    return nil
}

// Mutating Webhook: リソースの自動修正
func (r *MyApp) Default() {
    if r.Spec.Port == 0 {
        r.Spec.Port = 8080  // デフォルト値設定
    }
}

2. Finalizers(削除前処理)

const myAppFinalizer = "myapp.example.com/finalizer"

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    myApp := &MyApp{}
    if err := r.Get(ctx, req.NamespacedName, myApp); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 削除処理
    if !myApp.ObjectMeta.DeletionTimestamp.IsZero() {
        if controllerutil.ContainsFinalizer(myApp, myAppFinalizer) {
            // クリーンアップ処理(例: 外部リソースの削除)
            if err := r.cleanupExternalResources(ctx, myApp); err != nil {
                return ctrl.Result{}, err
            }

            // Finalizerを削除
            controllerutil.RemoveFinalizer(myApp, myAppFinalizer)
            if err := r.Update(ctx, myApp); err != nil {
                return ctrl.Result{}, err
            }
        }
        return ctrl.Result{}, nil
    }

    // Finalizerを追加
    if !controllerutil.ContainsFinalizer(myApp, myAppFinalizer) {
        controllerutil.AddFinalizer(myApp, myAppFinalizer)
        if err := r.Update(ctx, myApp); err != nil {
            return ctrl.Result{}, err
        }
    }

    // 通常のReconcile処理
    return r.reconcile(ctx, myApp)
}

3. Multi-tenancy対応

// Namespace-scopedなOperatorで複数テナントを管理
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
    // 特定のNamespaceのみ監視
    return ctrl.NewControllerManagedBy(mgr).
        For(&MyApp{}).
        WithEventFilter(predicate.NewPredicateFuncs(func(object client.Object) bool {
            // "tenant-"で始まるnamespaceのみ処理
            return strings.HasPrefix(object.GetNamespace(), "tenant-")
        })).
        Complete(r)
}

5.2 推奨リソース

公式ドキュメント:

実践的な学習:

  • 既存Operatorのコード読解:
- prometheus-operator - cert-manager - external-secrets-operator

  • ハンズオンチュートリアル:
- Kubebuilder Tutorial - Operator SDK Tutorial - KubeCon の Operator関連セッション

  • コミュニティ参加:
- Kubernetes Slack (#kubebuilder, #operator-sdk) - CNCF Operator SIG - GitHub Discussionsでの質問

5.3 次のステップ

レベル別学習計画:

初級(1-3ヶ月):

  • [ ] Kubebuilderで簡単なOperatorを実装
  • [ ] Envtestでのテスト作成
  • [ ] Kindでのローカルデバッグ
  • [ ] CRD設計のベストプラクティス理解

中級(3-6ヶ月):

  • [ ] Webhooks(Validating/Mutating)の実装
  • [ ] Finalizersを使った削除前処理
  • [ ] Prometheus Metricsの統合
  • [ ] E2Eテストの作成

上級(6-12ヶ月):

  • [ ] 複雑なステートフルアプリケーションの管理
  • [ ] Multi-cluster対応
  • [ ] OSSへのコントリビューション
  • [ ] カスタムSchedulerの実装
  • ---

    まとめ

    Kubernetes Operatorの実装は、以下の重要なポイントを押さえることで、本番環境で安定稼働するものになります:

  • Idempotency(冪等性): 何度実行しても同じ結果
  • Owner Reference: ライフサイクルの自動管理
  • Status Subresource: 望ましい状態と現在の状態の分離
  • パフォーマンス最適化: Cache, Rate Limiting, Predicates
  • エラーハンドリング: 一時的エラーと恒久的エラーの区別

これらの原則を理解し、実践することで、エンタープライズグレードのOperatorを開発できるようになります。