Kubernetes Operator 開発完全ガイド:client-go から Operator SDK (Kubebuilder) までKubernetes Operator ハンズオン
1. Why Kubernetes & why k8s Operator
1.1 なぜ Kubernetes なのか?
現代のアプリケーション開発において、コンテナ技術(Dockerなど)は「どこでも同じように動く」環境を提供し、開発とデプロイのポータビリティを劇的に向上させました。しかし、本番環境で多数のコンテナを運用しようとすると、新たな問題が発生します。
どのサーバーでどのコンテナを実行するか?トラフィックに応じてコンテナの数をどう増減させるか? (スケーリング)コンテナが停止したらどう自動で復旧させるか? (自己修復)複数のコンテナ間のネットワークをどう設定するか?
Kubernetes (K8s) は、これらの「コンテナオーケストレーション」の問題を解決するためのデファクトスタンダード(事実上の標準)となりました。Kubernetes は、インフラ全体を「宣言的API (Declarative API)」で管理可能にします。開発者が「(望ましい状態として)Webサーバーのコンテナが3つ欲しい」と YAML で定義すれば、Kubernetes が現在の状態を監視し、自動的に3つのコンテナが稼働する状態を維持・調整してくれます。これにより、アプリケーションのデプロイ(Day 1 オペレーション)の多くが自動化されました。
1.2 なぜ Kubernetes Operator が必要なのか?
Kubernetes は Deployment (ステートレスアプリ用) や StatefulSet (ステートフルアプリ用) といった強力なリソースを提供します。しかし、これら標準機能で自動化できるのは、一般的なアプリケーションのデプロイやスケーリング、ローリングアップデートまでです。複雑なミドルウェア(例:データベース、監視システム、メッセージキュー)の 「運用(Day 2 オペレーション)」には、ドメイン固有の専門知識(SRE や運用担当者の知識)が必要です。例えば、データベースクラスタを Kubernetes 上で運用する場合を考えてみてください。高度なアップグレード: スキーマ変更を伴う場合、単なるローリングアップデートでは対応できず、特定の順序での停止やデータ移行が必要です。バックアップとリストア: 定期的にバックアップを取得し、障害時には特定の手順でリストアする必要があります。自動フェイルオーバー: マスターノードがダウンした際、自動でレプリカをマスターに昇格させる複雑なロジックが必要です。状態に応じた調整: 負荷に応じて、リードレプリカの数を調整する。これらの運用作業は、Kubernetes の標準機能(YAML)だけでは定義できず、人手によるスクリプト実行や手動オペレーションに頼りがちでした。これではミスが発生しやすく、自動化の恩恵が限定的です。Operator = 人間の運用者の「ドメイン知識」をソフトウェアとして Kubernetes に埋め込む仕組みここで Operator が登場します。Operator は、これらの複雑な運用手順(バックアップ、フェイルオーバーなど)をコード化し、Kubernetes のコントローラーとして組み込む仕組みです。Operator により、Kubernetes は「Nginx コンテナを3つ動かす」といった単純なタスクだけでなく、「データベースクラスタのバックアップを取り、マスター障害時には自動でフェイルオーバーする」といった高度な運用(Day 2)まで自律的に実行できるようになります。
2. Operator とは何か?
Operator は Kubernetes のコントローラーモデルの延長線上にあります。Kubernetes は内部的に複数の Controller が動作しており、常に以下のループで動いています:望ましい状態(Spec) ≠ 実際の状態(Status) → 調整(Reconcile)を行うOperator はこの仕組みをユーザー定義のリソース(CRD:Custom Resource Definition)に拡張したものです。 典型的な構成は:CRD(Custom Resource Definition): 独自の API(Kind: DatabaseCluster など)を Kubernetes に追加します。Controller: CR の変更を監視し、「望ましい状態」に近づけるための処理(例:バックアップジョブの実行、フェイルオーバーのための Pod 設定変更)を実行します。
3. Operator を理解するために必要なコア概念
Operator は以下の Kubernetes の基本的な概念の上に成り立っています。
3.1 Spec(Desired State)と Status(Actual State)
Spec: ユーザーが「こうあってほしい」と定義する「望ましい状態」Status: コントローラーが観測した「実際の状態」これはOperatorと関係がある概念だけではなく、Kubernetesにですごく重要な概念ではあります。例えば、一般的なpodの場合でも、spec と statusを持っています。
# kubectl get po nginx -o yamlapiVersion: v1kind: Podmetadata:......spec: container: - image: nginx...status: condition: ... containerStatus: ...
Operator の仕事は、Spec を読み取り、Status をこの Spec に近づけ、結果を Status に書き戻すことです。
3.2 Reconcile ループ
コントローラーが「Spec と Status の差分」を検知して実行する調整処理のこと。このループが Operator の中核です。このループは、Kubernetes が「宣言的API」を維持するための心臓部であり、Operator の「脳」にあたる部分です。
3.2.1 Reconcile ループの基本的な考え方
Reconcile ループの動作は、家庭にある「サーモスタット(自動温度調節器)」に例えると非常に分かりやすいです。
望ましい状態 (Spec): あなたがエアコンのリモコンで設定した「希望温度 25℃」。現在の状態 (Status): センサーが検知した「現在の室温 22℃」。調整 (Reconcile): サーモスタットが「(Spec 25℃) ≠ (Status 22℃)」という差分を検知し、「冷房を停止し、暖房を作動させる」という調整処理を実行します。ループ: 暖房が作動し、室温が 25℃ に達すると、サーモスタットは再び「(Spec 25℃) = (Status 25℃)」を検知し、暖房を停止します。もし室温が下がり始めたら、再びループが作動します。
Operator の Reconcile ループもまったく同じです。
Spec: ユーザーが AppCluster YAML に書いた「size: 3 (Pod が3つ欲しい)」Status: Operator が実際に観測した「Pod の数 1」。Reconcile: Reconcile 関数が起動し、「(Spec 3) ≠ (Status 1)」を検知。不足している Pod を2つ作成 (Create) します。ループ: 次に誰かが手動で Pod を1つ削除しても (Status が 2 になる)、Reconcile ループが再び起動し、size: 3 に戻すために Pod を1つ作成します。
3.2.2 Reconcile ループは「いつ」実行されるのか?
重要なのは、Reconcile ループは「常に動き続けている (e.g., while true)」わけではないという点です。それは非効率的です。Reconcile ループは、イベントをトリガーとして実行されます。controller-runtime (Operator SDK の中核) は、Workqueueと呼ばれる仕組みを使い、以下のイベントが発生したときに、対象リソースの Reconcile をキューに入れます。監視対象リソース(CRD)の変更: ユーザーが kubectl apply で AppCluster の Spec を変更した (例: size を 3 から 5 に変更した)。管理対象リソース(子リソース)の変更: Operator が作成した Pod がユーザーによって手動で削除された、または Pod がクラッシュして停止した。 (Operator は自分が管理する Pod の状態も Watch しています)定期的な再調整 (Periodic Resync): 設定により、何も変更がなくても一定時間ごと(例:10分間ごと)に Reconcile を実行し、現在の状態が本当に Spec と一致しているかを確認することもあります。
3.2.3 Reconcile ループの「中身」
Reconcile 関数が実行されると、開発者は通常、以下のステップをコードとして実装します。Status 更新 (Update Status): 調整後の「実際の状態」を AppCluster リソースの Status フィールドに書き戻します(例:status.readyReplicas = 3)。取得 (Get): Reconcile のトリガーとなった AppCluster リソースの最新の状態を Kubernetes API から取得します。観測 (Observe):AppCluster が管理すべき現在の世界の実際状態を観測します(例:List API を使い、この AppCluster に属する Pod が現在いくつ存在するかを調べる)。比較 (Diff):AppCluster の Spec (望ましい状態) と、観測した実際状態 (Pod の数) を比較します。実行 (Act): 差分があれば、それを埋めるための処理(Create, Update, Delete)を実行します。Pod が足りない → Pod を Create する。Pod が多すぎる → Pod を Delete する。Pod のイメージが古い → Pod を Update (ローリングアップデート) する
3.2.4 設計上の最重要原則: 冪等性 (Idempotency)
Reconcile ループは、ネットワークエラーなどで失敗し、何度もリトライ(再実行)されることを前提に設計しなければなりません。このため、「何度実行しても同じ結果になる」という冪等性(べきとうせい)が極めて重要です。悪い例 (冪等でない): 「"Pod を1つ作る" というイベントを受け取ったから、Pod を1つ作る」 → これではリトライの度に Pod が増え続けてしまいます。良い例 (冪等である): 「望ましい状態は Pod 3つ。現在の状態は Pod 2つ。だから Pod を1つ作る」 → このロジックなら、何度リトライされても、常に「望ましい状態 3」と「現在の状態」を比較するため、結果は必ず「Pod が合計 3つ」という状態に収束します。
3.3 OwnerReference (所有者参照) によるガーベジコレクション
これは、リソース間に「親子の関係」を定義し、自動的なリソース管理(特に削除)を可能にするための Kubernetes の仕組みです。主な機能: ガーベジコレクション (GC) Operator は AppCluster(親)リソースの指示に基づき、Pod や Service(子)リソースを作成します。この時、子のリソースのメタデータに「私の所有者 (Owner) は AppCluster です」という印 (OwnerReference) を付けます。 こうすることで、ユーザーが kubectl delete appcluster my-cluster を実行して親を削除した際に、Kubernetes のガーベジコレクターがそれを検知し、その親に紐づく全ての子リソース(Pod, Service など)を自動的に削除してくれます。 もし OwnerReference がなければ、親を削除しても子は残り続け、「孤児リソース」としてクラスターにゴミが溜まってしまいます。もう一つの役割: 関係性の明示 この親子関係は、Operator 自身が「自分が管理すべきリソース」を識別するためにも使われます。Reconcile ループが「現在の Pod の数」を観測する際、OwnerReference をキーにして「この AppCluster が所有する Pod だけ」を正確にリストアップすることができます。Operator が子リソースを作成する際は、必ず OwnerReference を設定することが必須プラクティスです。
3.4 Event・Condition による状態通知
これらは、Operator が「今何をしているのか」「リソースの健康状態はどうか」を、人間や他のシステムに伝えるための重要な手段です。kubectl describe コマンドで表示される情報の多くは、これらによって提供されます。
Event (イベント): これは「点」の通知であり、何かが起こった瞬間の「ログ」や「通知」のようなものです。 特徴: 一時的であり、一定時間が経過すると(または数が増えすぎると)自動的に消えます。 例: 「Pod 'my-pod-xyz' を作成しました」、「バックアップジョブを開始しました」、「Spec.Size の変更を検知しました」、「イメージの取得に失敗しました」 用途: kubectl describe を実行したときに、リソースの直近の操作履歴を確認するために使います。
Condition (コンディション): これは「線」の状態であり、リソースの現在の「健康状態(Health Status)」を示す「ステータスライト」のようなものです。 特徴: Status フィールドの一部として永続的に保存されます。通常、Type(状態の種類)、Status(True/False/Unknown)、Reason(理由)、Message(詳細)のセットで表現されます。 例: Type: Ready, Status: True (リソースは正常に稼働し、利用可能です) Type: Ready, Status: False, Reason: PodsNotReady (リソースはまだ利用可能ではありません。理由: Podの準備ができていません) Type: Upgrading, Status: True (現在アップグレード作業中です) 用途: ユーザーや他の自動化システム(例: CI/CD パイプライン)が、「このリソースは正常か?」「アップグレードは完了したか?」を確実かつ一貫した方法で判断するために使われます。
4. オペレーター開発の技術レイヤー
Operator を開発する際、私たちは Kubernetes API と対話するための「部品」を選びます。その選択肢は、低レベルで柔軟なものから、高レベルで定型的なものまで、複数のレイヤーに分かれています。
4.1 レイヤー1: client-go (基盤ライブラリ)
これは、Kubernetes 自身が内部で使っている、公式の Go クライアントライブラリです。
役割: Kubernetes API サーバーと通信するためのすべての機能(Get, List, Create, Update, Delete, Watch)を提供します。コントローラーの構成要素: 高度なコントローラーを自前で実装するため、client-go は以下の低レベルなコンポーネント(「部品」)を提供します。 Informer: リソースの変更を Watch し、ローカルキャッシュを更新・維持します。 Lister: API サーバーに負荷をかけず、ローカルキャッシュから高速にリソースを Get/List します Indexer: オブジェクトを特定のキー(例:Namespace)で分類し、高速に検索できるようにします。 Workqueue: 変更イベントを受け取り、Reconcile 処理のキューイング(順序処理、リトライ制御)を行います。課題: 非常に強力で高性能ですが、これらの「部品」をすべて手動でセットアップし、キャッシュの同期を管理し、Workqueue を正しくハンドリングする必要があります。これは非常に複雑で、大量のお決まりのコード(ボイラープレート)が必要になります。例えるなら:client-go は「高性能なエンジン、トランスミッション、タイヤ」などの個別の部品です。これらを使って車(コントローラー)をゼロから自作するようなものです。
4.2 レイヤー2: controller-runtime (中核エンジン)
client-go の複雑さを解決するために登場したのが controller-runtime です。これは Operator 開発の「中核エンジン」となるライブラリです。
役割: client-go の低レベルな「部品」をすべて内部にカプセル化(隠蔽)し、開発者を面倒なセットアップから解放します。提供するもの: Manager: Informer, Lister, Workqueue, キャッシュの管理をすべて自動で行います。 High-Level Client: 開発者が使う Client は、読み取り時は自動でキャッシュ(Lister)を使い、書き込み時は API サーバーに直接送るなど、最適な動作を自動で選択します。 Reconciler インターフェース: 開発者が実装すべきことを、Reconcile 関数(ビジネスロジック)だけに集中させてくれます。例えるなら:controller-runtime は「エンジンや足回りがすべて組み込まれた走行可能なシャーシ」です。開発者は、このシャーシの上にどんなボディを乗せるか、内装をどうするか(=Reconcile 関数の実装)だけを考えればよくなります。
4.3 Kubebuilder と Operator SDK (開発フレームワーク)
controller-runtime は強力な「エンジン(ライブラリ)」ですが、それ自体はプロジェクトのディレクトリ構成や Makefile、Dockerfile を生成してはくれません。Kubebuilder と Operator SDK は、controller-runtime を利用した開発を開始するための「雛形(スキャフォールディング)生成ツール」です。役割:init や create api といった CLI コマンドを通じて、Operator 開発に必要なプロジェクト一式(ディレクトリ、CRD の YAML、RBAC 定義、Makefile、Dockerfile など)を自動生成します。controller-runtime との関係: Kubebuilder と Operator SDK は、どちらも内部で「エンジン」として controller-runtime を利用しています。 したがって、どちらのツールを使っても、開発者が最終的に記述する Reconcile 関数の書き方(ロジック)はほぼ同じになります。両者の違い: Kubebuilder:controller-runtime の「上流」にある、Go 言語での Operator 開発の標準的な雛形ツールです。 Operator SDK: Red Hat が支援するプロジェクトで、Kubebuilder の機能(Go で の雛形生成)を包含するスーパーセットです。Go 以外に、Ansible や Helm を使った Operator 開発にも対応しているのが最大の違いです。
4.4 比較表
比較項目レイヤー1: client-goレイヤー2: controller-runtimeレイヤー3: Kubebuilder / Operator SDK位置づけ低レベル基盤ライブラリ中核エンジン (ライブラリ)高レベル・雛形生成ツール (CLI)抽象度低 (すべて手動)中 (ロジックに集中)高 (プロジェクト全体を自動生成)主な目的K8s API との通信手段を提供client-go の複雑さを抽象化controller-runtime を使った開発プロジェクトを自動生成開発者の主な作業Informer, Lister, Workqueue の手動セットアップと管理Manager に Reconcile 関数を登録し、ロジックを実装CLI コマンドで雛形を生成し、Reconcile 関数にロジックを実装利用関係(なし)client-go を利用するcontroller-runtime を利用する
5. Operator SDK を使った Operator 開発(実例付き)
これまでの理論を踏まえ、いよいよ Operator SDK を使って、AppCluster という Kind を持つ Operator を開発していきます。
5.1 プロジェクト目標と環境準備
プロジェクト目標:AppCluster というカスタムリソースを作成し、以下の機能を実現します。環境準備: Go 言語、kubectl、Operator SDK がインストールされていることを確認します。
$ go versiongo version go1.24.5 linux/amd64$ kubectl version --clientClient Version: v1.33.3$ operator-sdk versionoperator-sdk version: "v1.33.0"
5.2 プロジェクト初期化
プロジェクトディレクトリを作成し、Go モジュールと Operator プロジェクトの骨格を初期化します。
# Go モジュールを初期化go mod init github.com/demo/appcluster-operator# Operator SDK でプロジェクト構造を初期化 (v4 プラグインを使用)operator-sdk init --plugins go/v4 --domain=demo.io --owner="Demo"
このコマンドは、cmd/main.go(プログラム入口)、config/(K8s マニフェスト)、Makefile、Dockerfile など、プロジェクトの基本的な骨組みを生成します。
5.3 API と Controller の作成
AppCluster という Kind の CRD と、そのロジックを担う Controller の雛形コードを自動生成します。
$ operator-sdk create api --group demo --version v1 --kind AppCluster --controller --resource
これにより、以下の2つの重要なファイルが生成されます。
api/v1/appcluster_types.go (CRD の型定義)
internal/controller/appcluster_controller.go (Reconcile ロジックの実装)
5.4 CRD の定義 (types.go)
api/v1/appcluster_types.go を編集し、Spec(望ましい状態)と Status(実際の状態)を定義します。
// AppClusterSpec defines the desired state of AppCluster.type AppClusterSpec struct { // Size is the number of pods to create (desired state) // ユーザーが指定する Pod の数(望ましい状態) // +kubebuilder:validation:Minimum=0 Size int32 `json:"size,omitempty"`}// AppClusterStatus defines the observed state of AppCluster.type AppClusterStatus struct { // Ready is the number of pods that are actually ready (observed state) // 実際に Ready な Pod の数(実際の状態) Ready int32 `json:"ready,omitempty"`}
ポイント:
Spec はユーザーが「どうなってほしいか」を定義します。Status は Operator が「実際どうなっているか」を報告するために使います。+kubebuilder:validation:Minimum=0 のようなマーカー(コメント)は、make manifests 実行時に CRD のバリデーションルール(この場合は size が0以上)を自動生成します。
5.5 Controller ロジックの実装
internal/controller/appcluster_controller.go に、Operator の「脳」となる Reconcile ロジックを実装します。
5.5.1 必要な import の追加
まず、corev1 (Pod) や apierrors (エラーハンドリング) などを import ブロックに追加します。
import ( "context" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // ★ newPodForAppCluster で使用 "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" demov1 "github.com/demo/appcluster-operator/api/v1" // ★ 自身の API を import)
5.5.2 RBAC 権限マーカーの追加
Reconcile 関数の直前に、Operator が Pod を操作するための RBAC 権限をマーカーで定義します。
// (AppCluster への権限はデフォルトで生成される)//+kubebuilder:rbac:groups=demo.demo.io,resources=appclusters,verbs=get;list;watch;create;update;patch;delete//+kubebuilder:rbac:groups=demo.demo.io,resources=appclusters/status,verbs=get;update;patch//+kubebuilder:rbac:groups=demo.demo.io,resources=appclusters/finalizers,verbs=update// ★ Pods に対する権限を追記//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete
重要: この最後の行(pods への権限)がなければ、Operator は権限不足で Pod を作成・削除できません。
5.5.3 Reconcile 関数の実装
Operator の中核となるロジックです。「Get → Observe → Diff & Act → Update Status」のパターンで実装します。
func (r *AppClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) // 1. Get: まず、トリガーとなった AppCluster リソースを取得する var cluster demov1.AppCluster if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil { if apierrors.IsNotFound(err) { // リソースが削除された場合。ガベージコレクションされるのでエラーとして扱わない logger.Info("AppCluster resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } logger.Error(err, "unable to fetch AppCluster") return ctrl.Result{}, err } // 2. Observe: 現在の実際状態 (Pod の数) を観測する var podList corev1.PodList // client.MatchingLabels で、この AppCluster が管理する Pod のみを絞り込む if err := r.List(ctx, &podList, client.InNamespace(req.Namespace), client.MatchingLabels{"app": cluster.Name}); err != nil { logger.Error(err, "unable to list pods") return ctrl.Result{}, err } // 3. Diff & Act: 望ましい状態 (Spec.Size) と現在の状態 (podList) を比較し、調整する if int32(len(podList.Items)) < cluster.Spec.Size { // --- Pod が不足している場合、作成する (Act) --- logger.Info("Reconciling: Pod count is less than spec. Creating Pod.") newPod := r.newPodForAppCluster(&cluster) // ★ 必須: OwnerReference を設定する (GC のため) // これにより、AppCluster (親) が削除されたら、この Pod (子) も自動で GC される if err := ctrl.SetControllerReference(&cluster, newPod, r.Scheme); err != nil { logger.Error(err, "unable to set OwnerReference") return ctrl.Result{}, err } if err := r.Create(ctx, newPod); err != nil { logger.Error(err, "unable to create Pod") return ctrl.Result{}, err } // Pod を1つ作成したら、すぐに Requeue (再実行) を要求 // (残りの Pod を作成するため、冪等性を保ちながらループする) return ctrl.Result{Requeue: true}, nil } else if int32(len(podList.Items)) > cluster.Spec.Size { // --- Pod が多すぎる場合、削除する (Act) --- logger.Info("Reconciling: Pod count is more than spec. Deleting Pod.") podToDelete := podList.Items[0] // (単純な例として最初の1つを削除) if err := r.Delete(ctx, &podToDelete); err != nil { logger.Error(err, "unable to delete Pod") return ctrl.Result{}, err } return ctrl.Result{Requeue: true}, nil } // 4. Update Status: 状態が一致した場合、Status を更新する if cluster.Status.Ready != int32(len(podList.Items)) { cluster.Status.Ready = int32(len(podList.Items)) logger.Info("Updating Status", "Ready Pods", cluster.Status.Ready) if err := r.Status().Update(ctx, &cluster); err != nil { // Status 更新はリトライが必要な場合がある logger.Error(err, "unable to update AppCluster status") return ctrl.Result{}, err } } // 状態が一致 (Spec.Size == len(pods)) していれば、何もせず正常終了 logger.Info("Reconciling: Pod count is equal to spec. No action needed.") return ctrl.Result{}, nil}
5.5.4 補助関数 (Pod 定義) の追加
Reconcile 関数の外(同じファイル内)に、Pod のマニフェストを生成するヘルパー関数を追加します。
// newPodForAppCluster creates a Pod definition for the given AppClusterfunc (r *AppClusterReconciler) newPodForAppCluster(cluster *demov1.AppCluster) *corev1.Pod { labels := map[string]string{ "app": cluster.Name, // ★ List 処理で使う重要なラベル } return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ GenerateName: cluster.Name + "-pod-", // Pod 名の重複を避けるため GenerateName を使用 Namespace: cluster.Namespace, Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "nginx", Image: "nginx:latest", // Nginx イメージを使用 }, }, }, }}
5.5.5 SetupWithManager の更新
func (r *AppClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&demov1.AppCluster{}). // AppCluster の変更を監視 Owns(&corev1.Pod{}). // ★ AppCluster が所有する Pod の変更も監視 Named("appcluster"). Complete(r)}
ポイント: .Owns(&corev1.Pod{}) を追加することで、Pod が手動で削除された時にも Reconcile がトリガーされ、自己修復が可能になります。
5.5.6 CRD の生成とインストール
コードが完成したら、依存関係を更新し、CRD と RBAC のマニフェスト(YAML)を生成します。
# Go の依存関係を整理go mod tidy# マニフェスト (CRD, RBAC) を生成make manifests
生成された CRD マニフェストをクラスタにインストールします。
# CRD をクラスタに適用make install
5.5.7 テスト用 CR の準備
config/samples/demo_v1_appcluster.yaml を編集し、テスト用の AppCluster を定義します。
apiVersion: demo.demo.io/v1kind: AppClustermetadata: labels: app.kubernetes.io/name: appcluster name: appcluster-samplespec: # Pod を 2 つ作成するように指定 size: 2
6. Operator の実行とテスト
いよいよ Operator をローカルで実行し、動作をテストします。
6.1 起動 Operator(ローカル開発モード)
まず、一つのターミナルで make run を実行し、Operator を起動します。
make run
ログ(抜粋):
2025-11-13T07:53:04+01:00 INFO setup starting manager2025-11-13T07:53:04+01:00 INFO Starting EventSource {"controller": "appcluster", "source": "kind source: *v1.Pod"}2025-11-13T07:53:04+01:00 INFO Starting EventSource {"controller": "appcluster", "source": "kind source: *v1.AppCluster"}2025-11-13T07:53:04+01:00 INFO Starting Controller {"controller": "appcluster"}2025-11-13T07:53:04+01:00 INFO Starting workers {"controller": "appcluster", "worker count": 1}
Operator が起動し、AppCluster と Pod の監視を開始しました。
6.2 AppCluster リソースの作成
別のターミナルを開き、準備したサンプル CR を kubectl apply します。
$ kubectl apply -f config/samples/demo_v1_appcluster.yamlappcluster.demo.demo.io/appcluster-sample created
6.3 Reconcile ログの観察
make run を実行しているターミナルに戻り、Operator のログを観察します。
2025-11-13T07:53:12+01:00 INFO Reconciling: Pod count is less than spec. Creating Pod.2025-11-13T07:53:12+01:00 INFO Reconciling: Pod count is less than spec. Creating Pod.2025-11-13T07:53:12+01:00 INFO Updating Status {"Ready Pods": 2}2025-11-13T07:53:12+01:00 INFO Reconciling: Pod count is equal to spec. No action needed.
ログ分析:
size: 2 に対し Pod が 0 だったので、1つ目の Pod を作成 (Creating Pod.) し、Requeue。すぐに 2 回目の Reconcile が走り、Pod が 1 つだったので、2つ目の Pod を作成 (Creating Pod.) し、Requeue。3 回目の Reconcile で Pod が 2 つになったことを確認し、Status.Ready を 2 に更新 (Updating Status)。4 回目の Reconcile で spec.size (2) と len(pods) (2) が一致したので、何もせず終了 (No action needed.)。
6.4 動作確認(Pod と Status)
kubectl で Pod と AppCluster の状態を確認します。
$ kubectl get podsNAME READY STATUS RESTARTS AGEappcluster-sample-pod-fpdzg 1/1 Running 0 19sappcluster-sample-pod-l8tf4 1/1 Running 0 19s
size: 2 に従って、2つの Pod が作成されました。
$ kubectl get appcluster appcluster-sample -o yaml
出力(抜粋):
spec: size: 2status: ready: 2 # ✅ Status が正しく 2 に更新されている
6.5 自己修復(Self-Healing)のテスト
Operator の真価である「自己修復」をテストします。
6.5.1 Pod の手動削除
kubectl delete で Pod のうち1つを強制的に削除します。
$ kubectl delete pod appcluster-sample-pod-fpdzgpod "appcluster-sample-pod-fpdzg" deleted
6.5.2 Operator の自動再作成を観察
make run のログを見ると、Pod の削除を検知(Owns のおかげ)して Reconcile が即座にトリガーされます。
2025-11-13T07:53:55+01:00 INFO Reconciling: Pod count is less than spec. Creating Pod.2025-11-13T07:53:55+01:00 INFO Reconciling: Pod count is equal to spec. No action needed.
Operator が「Pod が 1 つ足りない」と判断し、新しい Pod を作成しました。
6.5.3 Pod の回復を確認
kubectl で確認すると、すぐに新しい Pod が作成され、合計2つの状態が維持されています。
$ kubectl get pods -l app=appcluster-sampleNAME READY STATUS RESTARTS AGEappcluster-sample-pod-l8tf4 1/1 Running 0 55sappcluster-sample-pod-wqbv7 1/1 Running 0 12s # ✅ 新しく作成された Pod
6.6 ガベージコレクション(GC)のテスト
OwnerReference が正しく機能しているか(自動お片付け)を確認します。
6.6.1 AppCluster の削除
親である AppCluster リソースを削除します。
$ kubectl delete appcluster appcluster-sampleappcluster.demo.demo.io "appcluster-sample" deleted
6.6.2 Pod の自動クリーンアップを確認\
Kubernetes のガベージコレクターが OwnerReference を検知し、関連するすべての Pod を自動で削除します。
$ kubectl get pods -l app=appcluster-sampleNo resources found in default namespace.
関連する Pod がすべて自動的にクリーンアップされました。
7. 核心概念のまとめと次のステップ
このチュートリアルで、私たちは強力な Operator パターンの核心に触れました。
7.1 核心概念のまとめ
宣言的API: ユーザーは「どうするか(How)」ではなく「どうあってほしいか(What)」(size: 2)だけを定義します。Reconcile ループ (調停ループ): 「理想 (Spec)」と「現実 (Observe)」の差分を埋め続ける (Diff & Act) という Operator の中核的な動作です。Level-triggered (レベルトリガー): Operator は「何が起きたか(Event)」ではなく、「今どうなっているか(State)」に基づいて動作します。これにより、イベントを見逃しても次のループで必ず自己修復できます。OwnerReference (所有者参照): SetControllerReference で設定した親子関係により、親(AppCluster)を削除するだけで子(Pod)が自動的に削除されるガベージコレクションを実現しました。Owns による監視: SetupWithManager で .Owns(&corev1.Pod{}) を設定することで、子の Pod が削除された際にも Reconcile がトリガーされ、自己修復が可能になりました。
7.2 プロジェクト構造
appcluster-operator/├── api/v1/│ ├── appcluster_types.go # CRD 定義 (Spec, Status)│ └── ...├── internal/controller/│ └── appcluster_controller.go # ★ Reconcile ロジックの核心├── config/│ ├── crd/bases/ # 生成された CRD (YAML)│ ├── rbac/ # 生成された RBAC (Role, RoleBinding YAML)│ └── samples/ # テスト用のサンプル CR├── cmd/main.go # プログラムの開始点 (Manager の起動)├── Makefile # (run, install, deploy などの便利コマンド)└── go.mod # Go の依存関係
7.3 本番環境へのデプロイ
このチュートリアルでは make run を使い、ローカル PC 上で Operator を実行しました(デバッグに便利)。 本番環境では、Operator 自体もコンテナイメージとしてビルドし、Kubernetes クラスター内で Deployment として実行します。
# 1. Docker イメージをビルドし、レジストリに PUSHmake docker-build docker-push IMG=<your-registry/image-name:tag># 2. Operator を Deployment としてクラスタにデプロイmake deploy IMG=<your-registry/image-name:tag>
8. サマリー
本記事では、「なぜ Kubernetes Operator が必要なのか」という根本的な動機から始まり、Operator を支える中核的な理論、そして Operator SDK を利用した実践的な開発手法まで、ステップバイステップで詳細に解説しました。まず、Kubernetes 自身がコンテナの「デプロイ(Day 1)」を自動化する強力なプラットフォームである一方、データベースのバックアップやフェイルオーバーといった複雑な「運用(Day 2)」にはドメイン固有の知識が必要であり、それをソフトウェアとして実装する仕組みが Operator パターンであることを学びました。次に、Operator の「脳」として機能するReconcile ループ(調停ループ)の概念を深く掘り下げました。Operator は「Spec(望ましい状態)」と「Status(実際の状態)」の差分を常に監視し、その差分を埋めるために動作(Act)し続けます。このループは「冪等性(Idempotency)」を担保するように設計する必要があり、controller-runtime がその複雑な仕組み(イベント監視、リトライ)の多くを抽象化してくれることも理解しました。開発ツールのレイヤー構造(低レベルな client-go、中核エンジンの controller-runtime、雛形ツールの Operator SDK)を整理した後、私たちは実際に AppCluster という Operator をゼロから構築しました。実践編では、以下の重要なステップを実行しました。
operator-sdk init と create api でプロジェクトの雛形を作成。api/.../types.go で Spec と Status を定義。internal/controller/.../controller.go で Reconcile 関数(Get → Observe → Diff & Act → Update Status)を実装。RBAC マーカー(//+kubebuilder:rbac)で Pod への操作権限を付与。OwnerReference(SetControllerReference)を設定し、自動ガベージコレクション(GC)を実現。SetupWithManager で .Owns(&corev1.Pod{}) を設定し、Pod 削除時の自動「自己修復(Self-Healing)」を実現。
最後に、make run で Operator をローカル実行し、kubectl apply で AppCluster を作成すると Pod が自動生成されること、Pod を手動で削除しても即座に再作成されること、そして AppCluster を削除すると関連する Pod も一掃されることを確認しました。このチュートリアルを通じて、Kubernetes の宣言的APIの裏側で動くコントローラーモデルの強力さと、面倒な手動運用を自動化する Operator 開発の基本的な流れを完全に習得できたはずです。