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の場合でも、specstatusを持っています。

# kubectl get po nginx -o yaml
apiVersion: v1
kind: Pod
metadata:
...
...
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 applyAppClusterSpec を変更した (例: 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):AppClusterSpec (望ましい状態) と、観測した実際状態 (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(親)リソースの指示に基づき、PodService(子)リソースを作成します。この時、子のリソースのメタデータに「私の所有者 (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(状態の種類)、StatusTrue/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 KubebuilderOperator SDK (開発フレームワーク)

controller-runtime は強力な「エンジン(ライブラリ)」ですが、それ自体はプロジェクトのディレクトリ構成や MakefileDockerfile を生成してはくれません。
KubebuilderOperator SDK は、controller-runtime を利用した開発を開始するための「雛形(スキャフォールディング)生成ツール」です。
役割:initcreate api といった CLI コマンドを通じて、Operator 開発に必要なプロジェクト一式(ディレクトリ、CRD の YAML、RBAC 定義、MakefileDockerfile など)を自動生成します。
controller-runtime との関係: KubebuilderOperator 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 の
手動セットアップと管理
ManagerReconcile 関数を
登録し、ロジックを実装
CLI コマンドで雛形を生成し、
Reconcile 関数にロジックを実装
利用関係(なし)client-go を利用するcontroller-runtime を利用する

5. Operator SDK を使った Operator 開発(実例付き)

これまでの理論を踏まえ、いよいよ Operator SDK を使って、AppCluster という Kind を持つ Operator を開発していきます。

5.1 プロジェクト目標と環境準備

プロジェクト目標:AppCluster というカスタムリソースを作成し、以下の機能を実現します。
環境準備: Go 言語、kubectl、Operator SDK がインストールされていることを確認します。

$ go version
go version go1.24.5 linux/amd64

$ kubectl version --client
Client Version: v1.33.3

$ operator-sdk version
operator-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 マニフェスト)、MakefileDockerfile など、プロジェクトの基本的な骨組みを生成します。

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 AppCluster
func (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/v1
kind: AppCluster
metadata:
labels:
app.kubernetes.io/name: appcluster
name: appcluster-sample
spec:
# 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 manager
2025-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 が起動し、AppClusterPod の監視を開始しました。

6.2 AppCluster リソースの作成

別のターミナルを開き、準備したサンプル CR を kubectl apply します。

$ kubectl apply -f config/samples/demo_v1_appcluster.yaml
appcluster.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 pods
NAME READY STATUS RESTARTS AGE
appcluster-sample-pod-fpdzg 1/1 Running 0 19s
appcluster-sample-pod-l8tf4 1/1 Running 0 19s

size: 2 に従って、2つの Pod が作成されました。

$ kubectl get appcluster appcluster-sample -o yaml

出力(抜粋):

spec:
size: 2
status:
ready: 2 # ✅ Status が正しく 2 に更新されている

6.5 自己修復(Self-Healing)のテスト

Operator の真価である「自己修復」をテストします。

6.5.1 Pod の手動削除

kubectl delete で Pod のうち1つを強制的に削除します。

$ kubectl delete pod appcluster-sample-pod-fpdzg
pod "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-sample
NAME READY STATUS RESTARTS AGE
appcluster-sample-pod-l8tf4 1/1 Running 0 55s
appcluster-sample-pod-wqbv7 1/1 Running 0 12s # ✅ 新しく作成された Pod

6.6 ガベージコレクション(GC)のテスト

OwnerReference が正しく機能しているか(自動お片付け)を確認します。

6.6.1 AppCluster の削除

親である AppCluster リソースを削除します。

$ kubectl delete appcluster appcluster-sample
appcluster.demo.demo.io "appcluster-sample" deleted

6.6.2 Pod の自動クリーンアップを確認\

Kubernetes のガベージコレクターが OwnerReference を検知し、関連するすべての Pod を自動で削除します。

$ kubectl get pods -l app=appcluster-sample
No 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 イメージをビルドし、レジストリに PUSH
make 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 initcreate api でプロジェクトの雛形を作成。
api/.../types.goSpecStatus を定義。
internal/controller/.../controller.goReconcile 関数(Get → Observe → Diff & Act → Update Status)を実装。
RBAC マーカー//+kubebuilder:rbac)で Pod への操作権限を付与。
OwnerReferenceSetControllerReference)を設定し、自動ガベージコレクション(GC)を実現。
SetupWithManager.Owns(&corev1.Pod{}) を設定し、Pod 削除時の自動「自己修復(Self-Healing)」を実現。

最後に、make run で Operator をローカル実行し、kubectl applyAppCluster を作成すると Pod が自動生成されること、Pod を手動で削除しても即座に再作成されること、そして AppCluster を削除すると関連する Pod も一掃されることを確認しました。
このチュートリアルを通じて、Kubernetes の宣言的APIの裏側で動くコントローラーモデルの強力さと、面倒な手動運用を自動化する Operator 開発の基本的な流れを完全に習得できたはずです。

ブログの著者欄

何 鵬挙

システム本部 データ&AIチーム(北九州)

2024より中途でGMOインターネットグループ株式会社に入社。LLM・AIシステムの実用化・導入、及びインフラ設計・構築に従事

採用情報

関連記事

KEYWORD

TAG

もっとタグを見る

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック