Operatorパターンとカスタムリソースの設計
DBやキューの運用知識をコードに閉じ込め、宣言だけで自動運用できる。CRDでAPIを拡張しカスタムコントローラで冪等に収束させるOperatorの原理を解説。
- 1.Operatorは CRD(API拡張)と カスタムコントローラ(収束ループ)の二点セット。運用 Runbook を状態機械としてコード化する。
- 2.コントローラは『望ましい状態と実際の状態の差分を埋める』調整(reconcile)を回し続ける。命令の列ではなく、何度実行しても同じ結果になる冪等な設計が必須。
- 3.イベント駆動ではなく『現在の状態を毎回観測してあるべき姿へ収束させる』level-triggered。取りこぼしや再起動に強い。
Operatorとは何を自動化するのか
Kubernetes は標準で Deployment や Service を「望ましい状態」として宣言すると、コントローラが収束させてくれます。しかし PostgreSQL のフェイルオーバー、Kafka のパーティション再分散、証明書のローテーションといった「アプリ固有の運用知識」までは知りません。これらは従来、人間が Runbook(手順書)を見ながら手で実行していました。
Operator は、その運用知識をソフトウェアに埋め込んだものです。「熟練の運用者が、対象アプリの状態を監視し、必要な操作を判断・実行する」というループを、Kubernetes のコントローラとして常駐させます。人が kubectl を叩く代わりに、コントローラが 24 時間アプリの面倒を見る、という発想です。
Operator は二つの部品から成ります。
- CRD(CustomResourceDefinition):Kubernetes の API を拡張し、
PostgresClusterのような独自リソース種別を追加する。 - カスタムコントローラ:その独自リソースを監視し、宣言された状態へ実体を収束させる収束ループ。
CRD:APIを拡張する
CRD は「新しいリソース種別の定義」そのものです。これを登録すると、kube-apiserver はその種別の CRUD・バリデーション・etcd への永続化・watch を標準リソースと同じ仕組みで受け付けるようになります。つまり利用者は kubectl get postgrescluster や kubectl apply -f を、組み込みリソースと区別なく使えます。
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: postgresclusters.db.example.com
spec:
group: db.example.com
scope: Namespaced
names:
kind: PostgresCluster
plural: postgresclusters
versions:
- name: v1
served: true
storage: true # etcd に保存する版(複数版なら1つだけ true)
schema:
openAPIV3Schema: # ここで型・必須・範囲を定義(apiserver が検証)
type: object
properties:
spec:
type: object
properties:
replicas: { type: integer, minimum: 1 }
version: { type: string }
required: ["replicas", "version"]
設計の要は spec と status の分離です。spec は利用者が書く「望ましい状態(こうあってほしい)」、status はコントローラが書く「観測された実際の状態(いま実際こう)」。利用者は spec だけを触り、status は読むだけ。この役割分担が Operator の規律の土台になります。
独自 API を足す方法は CRD のほかに Aggregated API Server(独自の apiserver を別プロセスで動かし apiserver に集約させる)もあります。CRD は etcd と apiserver にそのまま乗るので実装が圧倒的に軽い反面、独自のストレージ・複雑な変換・サブリソースの自由な追加はできません。大半の Operator は CRD で十分です。
カスタムコントローラ:収束ループの原理
コントローラの中核は reconcile(調整)関数です。引数はただ一つ、対象リソースの識別子(名前空間 と 名前)。この関数は「今この瞬間のあるべき姿」を計算し、実体をそこへ寄せます。
reconcile(name):
desired = apiserver から spec を読む # 望ましい状態
actual = 実際の Pod/PVC/Secret 等を観測 # 実際の状態
diff = desired と actual の差分
diff があれば必要な操作だけを適用する # create / update / delete
status を観測結果で上書きする
return # 必要なら「N秒後に再実行」を要求
ここで決定的に重要なのが level-triggered(状態駆動)であって edge-triggered(イベント駆動)ではないという点です。
| 観点 | edge-triggered(イベント駆動) | level-triggered(状態駆動) |
|---|---|---|
| 反応の対象 | 発生した変化イベントそのもの | 現在の状態(あるべき姿との差) |
| イベント取りこぼし | 致命的(処理が抜け落ちる) | 次の観測で吸収され問題にならない |
| コントローラ再起動 | 途中の文脈を失う | 現状を再観測すれば復旧できる |
| 重複通知 | 二重処理の危険 | 差分がなければ何もしない(安全) |
Kubernetes のコントローラは watch でイベントをきっかけ(トリガー)にしますが、受け取ったイベントの中身では判断しません。トリガーを受けたら改めて現在の状態を丸ごと観測し直し、あるべき姿との差分から行動を決めます。だからイベントを取りこぼしても、apiserver と通信が一時的に切れても、コントローラが再起動しても、次の reconcile で現状を見れば正しく収束できます。これが Operator が運用に耐える理由です。
冪等な調整の作法
reconcile は同じ入力に対して何度呼ばれても同じ結果に収束しなければなりません(冪等性)。watch の再送、リーダー切り替え、リトライなどで reconcile は容易に重複実行されるからです。具体的な作法は次の通りです。
- 作成は「あれば作らない」で書く:
createを無条件に呼ぶと二回目で「既に存在」エラーになる。「期待する子リソースが無ければ作る/あれば spec を期待値へ更新する」という形(apply 的な発想)にする。名前を決定的に(例<cluster>-0)すれば重複生成を避けられる。 - 生成済みリソースの帰属を明示する:作った Pod や PVC に OwnerReference を付ける。親リソースが消えたとき子も連鎖削除(garbage collection)され、孤児が残らない。
- 副作用は観測で確かめる:外部 API 呼び出しなど巻き戻せない操作は、「やったかどうか」を毎回 status や外部状態の観測で確認してから判断する。フラグを内部メモリだけに持つと再起動で失われる。
- エラー時は requeue で再試行:失敗したら部分的な状態のまま return し、バックオフ付きで再キューする。次の reconcile が差分を見て続きを進める。リトライ戦略の考え方は /devops/retry-backoff-jitter/ と同じ発想。
「DB を作る → 待つ → ユーザーを作る → 待つ」という命令の列をそのまま書くと、途中で落ちた瞬間に状態が宙ぶらりんになり、再実行で破綻します。代わりに「いまどの段階か」を status に記録し、reconcile を呼ぶたびに status を見て「次に一歩だけ進める」設計にします。reconcile は手順書ではなく、状態機械の遷移関数です。
状態機械として設計する
Operator は本質的に有限状態機械です。status.phase のようなフィールドに現在地(Pending → Provisioning → Running → Degraded …)を持たせ、reconcile はその状態と観測結果から「次に進めるべき一歩」だけを実行して return します。一回の reconcile で全部やり切ろうとしないのがコツです。
複雑な収束は Condition の配列で表すのが定番です。Ready Progressing Degraded などの条件ごとに True/False/Unknown と理由を持たせ、利用者や上位の自動化がそれを読んで判断できるようにします。これは Pod や Deployment の status と同じ語彙で、エコシステムと噛み合います。
コントローラを冗長化(複数レプリカ)すると、二つのインスタンスが同じリソースを同時に reconcile して競合します。これを防ぐため Operator は通常リーダー選出を行い、能動的に reconcile するのは常に一つだけにします。その原理は分散システムの単一リーダー問題そのもので、スプリットブレインの注意点は /devops/leader-election-split-brain/ が詳しい。
どこまで自動化するか(成熟度)
Operator は「インストールできる」だけでは価値が薄く、どこまで運用を肩代わりするかで段階があります。一般に、デプロイ → 設定の維持 → バックアップ・復旧 → 障害時の自動修復 → 性能に応じた自動チューニング、と高度化します。とくに自動修復は強力ですが、GitOps の自己修復と同様に諸刃の剣で、人が緊急対応中の状態まで「ドリフト」として巻き戻す危険があります(GitOps の収束モデルとの関係は /devops/gitops/ を参照)。
障害を誤検知した Operator が再起動や再作成を連打し、健全なノードまで巻き込んで状況を悪化させる「自動化事故」は実在します。フェイルオーバーや削除のような不可逆操作には、レート制限・連続失敗時の停止(一定回数で人手にエスカレーション)・PodDisruptionBudget の尊重などの安全装置を必ず入れます。Operator はシステムの信頼性を上げる前提のものですが、設計を誤ると単一障害点にもなります(信頼性の見積もりは /devops/reliability-math/)。
まとめ
- 二点セット:CRD で API を拡張し、カスタムコントローラで収束させる。spec(望ましい状態)と status(観測結果)を分離するのが規律の土台。
- level-triggered:イベントの中身でなく現在の状態を毎回観測して差分を埋める。取りこぼし・再起動・重複に強い。
- 冪等な調整:reconcile は手順書ではなく状態機械の遷移関数。何度呼ばれても同じ結果に収束し、一歩ずつ進める。
- 安全装置を内蔵:リーダー選出で多重起動を防ぎ、自動操作には上限を設ける。運用知識をコード化するからこそ、その判断の質がそのまま本番の信頼性になる。
DevOps/インフラ Article
Operatorパターンとカスタムリソースの設計を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Kubernetes
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 5
導入後に効く点
コントローラは『望ましい状態と実際の状態の差分を埋める』調整(reconcile)を回し続ける。命令の列ではなく、何度実行しても同じ結果になる冪等な設計が必須。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「Kubernetes / Operator」に近いか確認する。
- 強みである「Operatorは CRD(API拡張)と カスタムコントローラ(収束ループ)の二点セット。運用 Runbook を状態機械としてコード化する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。