Kubernetes Operator - 講義

概要

Kubernetes Operatorは、Kubernetesのカスタムリソース定義(CRD)とコントローラーを組み合わせて、複雑なアプリケーションのライフサイクルを自動化する強力なパターンです。この技術は、クラウドネイティブエコシステムにおいて、インフラストラクチャのコード化(Infrastructure as Code)を次のレベルに引き上げ、運用の自動化と一貫性を実現します。

Operatorパターンは、Kubernetesの「宣言的な設定」という哲学を拡張し、アプリケーション固有の知識をコードとして表現します。これにより、データベースのバックアップ、証明書の更新、スケーリング戦略の実装など、従来は手動で行っていた複雑な運用タスクを自動化できます。

学習目標

この講義を通じて、以下のスキルを習得します:

  • Kubernetes基礎の深い理解 - Pod, Deployment, Service, ConfigMap, Secretなどの基本リソースと、それらの相互作用
  • カスタムリソース定義(CRD) - 独自のKubernetesリソースの設計と実装
  • コントローラーパターン - Reconciliation loopの実装と、宣言的な状態管理
  • Operator SDK/Kubebuilder - 効率的なOperator開発のためのツールチェーン
  • 本番運用ノウハウ - 監視、ロギング、障害対応、パフォーマンスチューニング
  • テスト駆動開発(TDD) - Envtest, Kind, E2Eテストを用いた品質保証
  • GitOps統合 - CI/CDパイプラインとの連携、バージョン管理戦略
  • 前提知識

    この講義を最大限に活用するために、以下の知識を前提とします:

  • Go言語: 中級レベル(Goroutine, Channel, Interface, Context の理解)
  • Kubernetes: 基本概念(Pod, Deployment, Service, Namespace, Label/Selector)
  • Docker/コンテナ: イメージビルド、レジストリ操作、基本的なトラブルシューティング
  • YAML: Kubernetes マニフェストの読み書き
  • Git: バージョン管理の基本操作
  • ---

    1. 技術の歴史と発展

    1.1 Kubernetes Operatorパターンの誕生

    Kubernetes Operatorパターンは、2016年にCoreOS(現Red Hat)によって正式に提唱されました。当時、Kubernetesはステートレスアプリケーションの管理には優れていましたが、データベースやメッセージキューなどのステートフルアプリケーションの運用には課題がありました。

    背景にあった課題:

  • 複雑な運用ロジックの自動化不足: データベースのマスター選出、バックアップ、リストアなどの操作を、標準的なKubernetesリソースだけでは表現できなかった
  • アプリケーション固有の知識の欠如: Kubernetesのコントローラーは汎用的で、特定のアプリケーションのベストプラクティスを理解していなかった
  • Day 2 Operations(運用フェーズ)の自動化: デプロイだけでなく、アップグレード、スケーリング、障害復旧などの継続的な運用タスクを自動化する必要があった
  • 1.2 etcd-operator: 最初の実用的Operator

    CoreOSは、分散Key-Valueストアであるetcdの運用を自動化するetcd-operatorを最初の実装例として公開しました。これにより、以下のような複雑な運用タスクが自動化されました:

    apiVersion: "etcd.database.coreos.com/v1beta2"
    kind: "EtcdCluster"
    metadata:
      name: "example-etcd-cluster"
    spec:
      size: 3
      version: "3.2.13"
    

    このシンプルな宣言だけで、Operatorが以下を自動実行:

  • 3ノードのetcdクラスター作成
  • 障害時の自動復旧
  • ローリングアップデート
  • バックアップとリストアの管理
  • 1.3 技術の進化と標準化

    2017-2018年: ツールチェーンの発展

  • Operator Framework (Red Hat): Operator SDK, Operator Lifecycle Manager (OLM), OperatorHubの登場
  • Kubebuilder (Kubernetes SIG): Kubernetes公式のOperator開発フレームワーク
  • API Machinery: controller-runtimeライブラリの成熟化
  • 2019年以降: エコシステムの拡大

  • OperatorHub.io: 200以上のOperatorが公開
  • Operator Capability Levels: Operatorの成熟度を5段階で評価する基準の確立
1. Basic Install(基本インストール) 2. Seamless Upgrades(シームレスアップグレード) 3. Full Lifecycle(完全なライフサイクル管理) 4. Deep Insights(詳細な監視と分析) 5. Auto Pilot(完全な自動運用)

2020年代: クラウドネイティブの標準パターンへ

Kubernetes Operatorは、CNCF(Cloud Native Computing Foundation)エコシステムの中核技術として確立され、以下のような広範な用途で使われるようになりました:

  • データベース管理(PostgreSQL, MySQL, MongoDB)
  • メッセージング(Kafka, RabbitMQ)
  • 監視(Prometheus, Grafana)
  • セキュリティ(cert-manager, Vault)
  • マルチクラスター管理(Crossplane, ArgoCD)

---

2. 実社会での活用事例

2.1 Prometheus Operator

提供元: CoreOS/Red Hat 用途: Kubernetes上でのPrometheus監視スタックの管理

実装の特徴:

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: prometheus
spec:
  replicas: 2
  serviceMonitorSelector:
    matchLabels:
      team: frontend

企業での活用:

  • Spotify: 数千のマイクロサービスの監視を、Prometheus Operatorで一元管理
  • GitLab: SaaSプラットフォームの全体的な可観測性(Observability)を実現
  • Adobe: マルチテナント環境での監視設定の自動化
  • 技術的メリット:

  • ServiceMonitor CRD: サービスディスカバリーの自動化
  • AlertManager統合: アラートルールの宣言的管理
  • 動的な設定リロード: Prometheusの再起動なしで設定変更

2.2 cert-manager

提供元: Jetstack(現Venafi) 用途: TLS証明書の自動発行・更新

実装の特徴:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
spec:
  secretName: example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - example.com
  - www.example.com

企業での活用:

  • Shopify: 数万のマーチャント向けカスタムドメインのTLS証明書を自動管理
  • Zalando: マイクロサービス間mTLS(mutual TLS)の完全自動化
  • Bloomberg: 内部CA統合による企業証明書管理
  • 技術的メリット:

  • Let's Encrypt統合: ACME protocolによる無料証明書の自動取得
  • 証明書更新の自動化: 有効期限30日前に自動更新
  • 複数CA対応: Vault, Venafi, AWS PCAなど

2.3 ArgoCD Application Controller

提供元: Intuit(CNCF卒業プロジェクト) 用途: GitOpsによる継続的デリバリー

実装の特徴:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: guestbook
spec:
  source:
    repoURL: https://github.com/argoproj/argocd-example-apps.git
    targetRevision: HEAD
    path: guestbook
  destination:
    server: https://kubernetes.default.svc
    namespace: guestbook
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

企業での活用:

  • Intuit: 数千のマイクロサービスのデプロイ自動化
  • Red Hat: OpenShift GitOpsの基盤技術
  • Tesla: 車両向けOTAアップデートのインフラストラクチャ管理
  • 技術的メリット:

  • Git as Single Source of Truth: すべての変更がGitで追跡可能
  • 自動同期とセルフヒーリング: ドリフト検出と自動修復
  • マルチクラスター対応: 単一のArgoCDで複数クラスター管理

2.4 Crossplane

提供元: Upbound(CNCF incubatingプロジェクト) 用途: クラウドリソースのKubernetes API化

実装の特徴:

apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance
metadata:
  name: production-db
spec:
  forProvider:
    region: us-west-2
    dbInstanceClass: db.t3.medium
    engine: postgres
    engineVersion: "13.7"
    masterUsername: adminuser
    allocatedStorage: 100
  writeConnectionSecretToRef:
    name: db-conn-secret

企業での活用:

  • GitLab: マルチクラウド環境のインフラストラクチャ管理統一
  • Upbound: 自社のManaged Crossplane製品の基盤
  • VSHN: スイスのマネージドKubernetesプロバイダーでの全自動インフラ
  • 技術的メリット:

  • Composition: 複数のクラウドリソースを抽象化した独自APIの作成
  • マルチクラウド対応: AWS, Azure, GCP, Alibabaなど
  • Infrastructure as Code: Terraform的な機能をKubernetes APIで提供

2.5 Strimzi Kafka Operator

提供元: Red Hat/CNCF Sandbox 用途: Kubernetes上でのApache Kafka運用

実装の特徴:

apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
  name: my-cluster
spec:
  kafka:
    version: 3.4.0
    replicas: 3
    listeners:
      - name: plain
        port: 9092
        type: internal
        tls: false
      - name: tls
        port: 9093
        type: internal
        tls: true
    storage:
      type: persistent-claim
      size: 100Gi
  zookeeper:
    replicas: 3
    storage:
      type: persistent-claim
      size: 10Gi

企業での活用:

  • Aiven: マネージドKafkaサービスの基盤技術
  • Red Hat: OpenShift Streamsでの採用
  • 欧州の金融機関: コンプライアンス要件を満たすKafka運用
  • 技術的メリット:

  • 完全なKafkaライフサイクル管理: ブローカー、Zookeeper、Kafka Connectなど
  • セキュリティ設定の自動化: TLS、SASL、OAuth統合
  • ローリングアップデート: ダウンタイムなしのバージョンアップ
  • 2.6 その他の注目すべきOperator

    Operator 提供元 用途 採用企業例
    **Elasticsearch Operator** Elastic Elasticsearch/Kibanaクラスター管理 Walmart, Tinder
    **PostgreSQL Operator** Zalando PostgreSQL HA構成の自動化 Zalando, HelloFresh
    **Vault Operator** Banzai Cloud HashiCorp Vault管理 金融機関、ヘルスケア企業
    **Istio Operator** Google/IBM サービスメッシュ管理 eBay, AutoTrader
    **Velero** VMware バックアップとディザスタリカバリ Adobe, GitHub

    ---

    3. なぜ学ぶ価値があるか

    3.1 クラウドネイティブ時代の必須スキル

    現代のソフトウェア開発において、Kubernetes Operatorは以下の理由で必須スキルとなっています:

    1. インフラストラクチャのプログラマブル化

    従来の「インフラチームが手動で構築」から「開発者が宣言的に記述」へのパラダイムシフト:

    // 従来: シェルスクリプトで手動構築
    ./setup-database.sh --replicas=3 --backup-enabled
    
    // Operator時代: 宣言的な定義
    apiVersion: db.example.com/v1
    kind: Database
    spec:
      replicas: 3
      backupEnabled: true
    

    2. マルチクラウド・ハイブリッドクラウド戦略

    単一のKubernetes APIで、あらゆるクラウドプロバイダーのリソースを管理できるスキルは、企業のベンダーロックイン回避戦略において重要です。

    3. SREとDevOpsの融合

    Operatorは、Site Reliability Engineering(SRE)のプラクティスをコードとして表現します:

  • Toil(定型作業)の削減: 自動化できる運用タスクを完全に排除
  • Error Budget管理: 宣言的な設定で一貫性を保ち、人的ミスを削減
  • Observability: メトリクス、ログ、トレースを統合的に管理

3.2 SREとしてのキャリアパス

Kubernetes Operatorのスキルは、以下のようなキャリアパスを開きます:

レベル1: Platform Engineer(年収800万〜1200万円)

  • 社内向けPaaS(Platform as a Service)の構築
  • 開発チームのセルフサービス化支援
  • Operatorを用いた標準化されたデプロイパイプライン構築

レベル2: Senior SRE(年収1200万〜1800万円)

  • 大規模Kubernetesクラスターの設計と運用
  • カスタムOperatorの開発とメンテナンス
  • インシデント対応の自動化とポストモーテム文化の確立

レベル3: Staff/Principal Engineer(年収1800万〜2500万円+)

  • クラウドネイティブアーキテクチャの技術戦略策定
  • OSSへの貢献(Kubernetes, CNCF projects)
  • 組織横断的な技術標準の策定

レベル4: Consulting/Freelance(プロジェクトベース)

  • 大手企業のKubernetes移行支援(月額150万〜300万円)
  • Operator開発のトレーニング提供
  • アーキテクチャレビューとベストプラクティス策定
  • 3.3 技術的スキルの横展開

    Operatorの学習を通じて得られる副次的スキル:

  • 分散システム設計: Consensus algorithm, CAP定理, Eventual consistency
  • Go言語の実践的活用: Concurrency pattern, Testing, Performance tuning
  • Kubernetes内部機構の深い理解: API Server, Controller Manager, Scheduler
  • クラウドネイティブパターン: Circuit breaker, Retry, Backoff, Rate limiting

---

4. 市場価値と求人トレンド

4.1 年収相場(日本市場、2024年時点)

ジュニアレベル(1-3年経験)

  • Kubernetes基礎 + Operator使用経験: 600万〜850万円
  • 簡単なOperator実装経験あり: 700万〜950万円

ミドルレベル(3-6年経験)

  • Operator開発・運用経験: 900万〜1400万円
  • 複数のOperator本番運用: 1000万〜1600万円

シニアレベル(6年以上)

  • カスタムOperator設計・実装: 1300万〜2000万円
  • OSSコントリビューション実績: 1500万〜2500万円

外資系企業・グローバルリモート

  • Staff Engineer相当: 2000万〜3500万円
  • Principal Engineer相当: 3000万〜5000万円+ストックオプション
  • 4.2 求人市場のトレンド

    2023年の求人分析(Indeed, LinkedIn調査):

  • 需要の高い技術スタック
- Kubernetes + Go: 前年比 +45% - Operator Framework/Kubebuilder: 前年比 +62% - ArgoCD + GitOps: 前年比 +78% - Crossplane: 前年比 +120%(急成長)

  • 業界別需要
- SaaS企業: 最も高い需要(マルチテナント基盤の構築) - 金融・保険: コンプライアンス対応のインフラ自動化 - Eコマース: ピーク時のオートスケーリング実装 - 製造業: エッジコンピューティングとIoT基盤

  • 地理的な需要
- 東京: 全体の65% - 大阪・福岡: 20% - フルリモート: 15%(増加傾向)

4.3 スキルセットの組み合わせ価値

以下の組み合わせは特に市場価値が高い:

技術スタック 市場価値 典型的な年収レンジ
Kubernetes Operator + AWS EKS ★★★★★ 1000万〜2000万円
Operator + ArgoCD + Terraform ★★★★★ 1100万〜2200万円
Operator + Istio + Observability ★★★★☆ 1000万〜1800万円
Operator + Crossplane + Multi-cloud ★★★★★ 1200万〜2500万円
Operator + Security (OPA, Vault) ★★★★★ 1100万〜2000万円

---

5. 実務での運用考慮点

5.1 本番環境でのOperator運用

デプロイ戦略:

  • 段階的ロールアウト
   # Development環境で2週間
   # Staging環境で1週間
   # Production環境へCanary deployment
   

  • バージョン管理
- Operator自体のバージョン管理(Semantic Versioning) - CRD APIバージョンの互換性管理(v1alpha1 → v1beta1 → v1)

  • High Availability構成
   spec:
     replicas: 3  # 複数レプリカでの冗長化
     leaderElection:
       enabled: true  # リーダー選出の有効化
   

5.2 障害対応と復旧手順

典型的な障害シナリオと対処法:

シナリオ1: Reconciliation loopの無限ループ

// 問題のあるコード
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 毎回Statusを更新 → 無限ループ発生
    obj.Status.LastUpdate = time.Now()
    r.Status().Update(ctx, obj)
    return ctrl.Result{}, nil
}

// 修正後
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    if obj.Status.Condition != "Ready" {
        obj.Status.Condition = "Ready"
        obj.Status.LastUpdate = time.Now()
        return ctrl.Result{}, r.Status().Update(ctx, obj)
    }
    return ctrl.Result{}, nil  // 変更なければ何もしない
}

シナリオ2: リソースリーク

// Owner Referenceを設定し忘れると、親リソース削除時に子が残る
func (r *Reconciler) createDeployment(ctx context.Context, app *MyApp) error {
    dep := &appsv1.Deployment{...}

    // これを忘れるとリソースリーク
    if err := ctrl.SetControllerReference(app, dep, r.Scheme); err != nil {
        return err
    }

    return r.Create(ctx, dep)
}

シナリオ3: APIサーバーの過負荷

// 悪い実装: 頻繁なリスト操作
func (r *Reconciler) Reconcile(...) (ctrl.Result, error) {
    var podList corev1.PodList
    r.List(ctx, &podList)  // 毎回すべてのPodをリスト
    // ...
    return ctrl.Result{RequeueAfter: 1 * time.Second}, nil  // 1秒ごとに実行
}

// 良い実装: Watchとキャッシュを活用
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&MyApp{}).
        Owns(&corev1.Pod{}).  // Watchを設定
        Complete(r)
}

5.3 監視とアラート

重要なメトリクス:

  • Reconciliation metrics
- controller_runtime_reconcile_total: Reconcile実行回数 - controller_runtime_reconcile_errors_total: エラー回数 - controller_runtime_reconcile_time_seconds: 処理時間

  • Workqueue metrics
- workqueue_depth: キューの深さ(溜まりすぎに注意) - workqueue_retries_total: リトライ回数

  • Custom metrics
   var (
       customResourceCount = prometheus.NewGauge(
           prometheus.GaugeOpts{
               Name: "myoperator_custom_resource_count",
               Help: "Number of custom resources managed",
           },
       )
   )
   

Prometheus + Grafanaでのダッシュボード例:

apiVersion: v1
kind: ServiceMonitor
metadata:
  name: myoperator-metrics
spec:
  selector:
    matchLabels:
      control-plane: controller-manager
  endpoints:
  - port: https
    scheme: https
    bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token

5.4 パフォーマンスチューニング

1. Rate Limiting

// Controller Managerの設定
ctrl.Options{
    RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(
        5*time.Millisecond,  // 基準時間
        1000*time.Second,    // 最大待機時間
    ),
}

2. 並列処理の制御

ctrl.Options{
    MaxConcurrentReconciles: 3,  // 同時実行数を制限
}

3. Cacheの最適化

// 特定のNamespaceのみキャッシュ
cache.Options{
    DefaultNamespaces: map[string]cache.Config{
        "my-namespace": {},
    },
}

---

6. 開発手法:TDD(Test-Driven Development)

6.1 Operatorにおけるテスト戦略

テストピラミッド:

       /\
      /E2E\         <- 少数(本番環境に近い統合テスト)
     /------\
    /  統合  \       <- 中程度(Envtestを使用)
   /----------\
  /  ユニット  \     <- 多数(ロジックの単体テスト)
 /--------------\

6.2 Envtestによる統合テスト

package controller_test

import (
    "context"
    "path/filepath"
    "testing"
    "time"

    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
    "k8s.io/client-go/kubernetes/scheme"
    "k8s.io/client-go/rest"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/envtest"
    logf "sigs.k8s.io/controller-runtime/pkg/log"
    "sigs.k8s.io/controller-runtime/pkg/log/zap"

    myappv1 "example.com/myoperator/api/v1"
    "example.com/myoperator/controllers"
)

var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var ctx context.Context
var cancel context.CancelFunc

func TestControllers(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
    logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

    ctx, cancel = context.WithCancel(context.TODO())

    By("bootstrapping test environment")
    testEnv = &envtest.Environment{
        CRDDirectoryPaths:     []string{filepath.Join("..", "config", "crd", "bases")},
        ErrorIfCRDPathMissing: true,
    }

    var err error
    cfg, err = testEnv.Start()
    Expect(err).NotTo(HaveOccurred())
    Expect(cfg).NotTo(BeNil())

    err = myappv1.AddToScheme(scheme.Scheme)
    Expect(err).NotTo(HaveOccurred())

    k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
    Expect(err).NotTo(HaveOccurred())
    Expect(k8sClient).NotTo(BeNil())

    // Controllerの起動
    k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
        Scheme: scheme.Scheme,
    })
    Expect(err).ToNot(HaveOccurred())

    err = (&controllers.MyAppReconciler{
        Client: k8sManager.GetClient(),
        Scheme: k8sManager.GetScheme(),
    }).SetupWithManager(k8sManager)
    Expect(err).ToNot(HaveOccurred())

    go func() {
        defer GinkgoRecover()
        err = k8sManager.Start(ctx)
        Expect(err).ToNot(HaveOccurred(), "failed to run manager")
    }()
})

var _ = AfterSuite(func() {
    cancel()
    By("tearing down the test environment")
    err := testEnv.Stop()
    Expect(err).NotTo(HaveOccurred())
})

var _ = Describe("MyApp Controller", func() {
    Context("When creating a MyApp resource", func() {
        It("Should create a Deployment", func() {
            ctx := context.Background()
            myApp := &myappv1.MyApp{
                ObjectMeta: metav1.ObjectMeta{
                    Name:      "test-app",
                    Namespace: "default",
                },
                Spec: myappv1.MyAppSpec{
                    Replicas: 3,
                    Image:    "nginx:latest",
                },
            }

            Expect(k8sClient.Create(ctx, myApp)).Should(Succeed())

            // Deploymentが作成されるまで待機
            deployment := &appsv1.Deployment{}
            Eventually(func() error {
                return k8sClient.Get(ctx, client.ObjectKey{
                    Name:      "test-app",
                    Namespace: "default",
                }, deployment)
            }, time.Second*10, time.Millisecond*250).Should(Succeed())

            Expect(*deployment.Spec.Replicas).Should(Equal(int32(3)))
        })
    })
})

6.3 CI/CDパイプライン統合

GitHub Actionsの例:

name: Operator CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'

      - name: Install tools
        run: |
          make install-tools

      - name: Run tests
        run: make test

      - name: Run envtest
        run: make test-integration

      - name: Build Docker image
        run: make docker-build IMG=myoperator:${{ github.sha }}

  e2e:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v3

      - name: Create Kind cluster
        uses: helm/kind-action@v1.8.0

      - name: Deploy Operator
        run: |
          make deploy IMG=myoperator:${{ github.sha }}

      - name: Run E2E tests
        run: make test-e2e

---

7. ソフトスキル:チーム開発での実践

7.1 Operator設計のレビューポイント

設計レビューチェックリスト:

  • API設計
- [ ] CRDのフィールド名は直感的か? - [ ] 必須フィールドと任意フィールドが明確か? - [ ] デフォルト値は適切か? - [ ] Status subresourceは適切に設計されているか?

  • Reconciliation ロジック
- [ ] Idempotent(冪等性)が保証されているか? - [ ] エラーハンドリングは適切か? - [ ] Requeue戦略は適切か? - [ ] Owner Referenceは設定されているか?

  • パフォーマンス
- [ ] 不要なAPI呼び出しがないか? - [ ] Watchは適切に設定されているか? - [ ] Rate limitingは考慮されているか?

7.2 プルリクエストのベストプラクティス

良いPRの例:

## Summary
MyAppリソースに自動スケーリング機能を追加

## Changes
- HorizontalPodAutoscaler (HPA) の自動作成機能を実装
- `spec.autoscaling`フィールドをCRDに追加
- HPAのReconciliationロジックを追加

## Testing
- Envtestで単体テスト追加
- Kindクラスターでの手動E2Eテスト実施
- 負荷テストでスケーリング動作を確認

## Migration Guide
既存のMyAppリソースには影響なし(後方互換性あり)
新規にautoscaling機能を使う場合:

yaml
spec:
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetCPUUtilization: 80


## Checklist
- [x] Tests added/updated
- [x] Documentation updated
- [x] CHANGELOG updated
- [x] API version bumped (if needed)

7.3 ドキュメンテーション戦略

必須ドキュメント:

  • README.md
- Operatorの目的と機能概要 - クイックスタートガイド - インストール手順

  • API Reference
   // MyAppSpec defines the desired state of MyApp
   type MyAppSpec struct {
       // Replicas is the desired number of replicas
       // +kubebuilder:validation:Minimum=1
       // +kubebuilder:validation:Maximum=100
       // +kubebuilder:default=1
       Replicas int32 `json:"replicas,omitempty"`

       // Image is the container image to use
       // +kubebuilder:validation:Required
       Image string `json:"image"`
   }
   

  • Operations Guide
- 監視設定 - アラート設定 - トラブルシューティングガイド

  • Architecture Decision Records (ADR)
   # ADR-001: Reconciliation戦略の選択

   ## Status
   Accepted

   ## Context
   複数のリソース(Deployment, Service, Ingress)を管理する必要がある

   ## Decision
   Single Reconcilerパターンを採用し、すべてのリソースを1つのReconcile関数で管理

   ## Consequences
   - Pros: ロジックが集中し、依存関係が明確
   - Cons: Reconcile関数が大きくなる可能性
   

---

8. よくある失敗と回避策

8.1 Reconciliation loopの無限ループ

問題:

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

    // 毎回タイムスタンプを更新 → Statusの変更 → 再度Reconcileが呼ばれる
    myApp.Status.LastReconcile = metav1.Now()
    r.Status().Update(ctx, &myApp)

    return ctrl.Result{}, nil
}

解決策:

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

    // 実際に状態が変わった時だけStatusを更新
    originalStatus := myApp.Status.DeepCopy()

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

    // 変更があった場合のみ更新
    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
}

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

解決策:

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

8.3 エラーハンドリングの不適切な実装

問題:

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 外部APIへの呼び出しが失敗した場合、何も返さない
    if err := r.callExternalAPI(); err != nil {
        log.Error(err, "external API call failed")
        return ctrl.Result{}, nil  // エラーを無視 → 再試行されない
    }
    return ctrl.Result{}, nil
}

解決策:

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 一時的なエラーの場合はRequeue
    if err := r.callExternalAPI(); err != nil {
        if isTemporaryError(err) {
            log.Error(err, "external API call failed, will retry")
            return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
        }
        // 恒久的なエラーの場合はStatusに記録
        return ctrl.Result{}, fmt.Errorf("permanent error: %w", err)
    }
    return ctrl.Result{}, nil
}

func isTemporaryError(err error) bool {
    // ネットワークエラー、タイムアウトなど
    return errors.Is(err, context.DeadlineExceeded) ||
           errors.Is(err, syscall.ECONNREFUSED)
}

8.4 Watchの設定ミス

問題:

func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&MyApp{}).
        // Ownsを設定していない → 子リソースの変更を検知できない
        Complete(r)
}

解決策:

func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&MyApp{}).
        Owns(&appsv1.Deployment{}).  // Deploymentの変更を検知
        Owns(&corev1.Service{}).     // Serviceの変更を検知
        Watches(
            &source.Kind{Type: &corev1.ConfigMap{}},
            handler.EnqueueRequestsFromMapFunc(r.findObjectsForConfigMap),
            builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
        ).
        Complete(r)
}

func (r *Reconciler) findObjectsForConfigMap(cm client.Object) []reconcile.Request {
    // ConfigMapの変更に関連するMyAppを検索
    var requests []reconcile.Request
    var myAppList MyAppList
    r.List(context.Background(), &myAppList, client.InNamespace(cm.GetNamespace()))

    for _, app := range myAppList.Items {
        if app.Spec.ConfigMapRef == cm.GetName() {
            requests = append(requests, reconcile.Request{
                NamespacedName: types.NamespacedName{
                    Name:      app.Name,
                    Namespace: app.Namespace,
                },
            })
        }
    }
    return requests
}

---

9. まとめ:学習ロードマップ

レベル1: 基礎(1-2ヶ月)

  • Kubernetesの基本リソースを完全に理解
  • 簡単なCRDとControllerを実装(Kubebuilderのチュートリアル)
  • Kindでのローカル開発環境構築

レベル2: 実践(3-6ヶ月)

  • 本番レベルのOperatorを1つ実装
  • Envtestでのテスト駆動開発
  • CI/CDパイプライン統合

レベル3: マスター(6-12ヶ月)

  • 複数のOperatorの本番運用経験
  • OSSへの貢献(バグ修正、機能追加)
  • アーキテクチャ設計と技術選定のリード

この講義を通じて、単なる「Operatorを動かせる」レベルから、「本番環境で運用できる」「アーキテクチャを設計できる」レベルへと成長していきましょう。