Skip to content
Published on

Go だけじゃない — Kopf、Metacontroller、Shell Operator で Operator を作る

Authors

はじめに — Operator は Go の専有物ではない

「Operator を作る」と言うと、ほぼ反射的に Go と kubebuilder が思い浮かびます。実際 Operator SDK と kubebuilder は Go エコシステムを中心に発展し、controller-runtime という強力なライブラリも Go 用です。そのため多くのチームが「うちは Go をよく知らないので Operator は無理」と先に諦めてしまいます。

しかしこれは事実ではありません。Operator の本質は「Kubernetes API を watch し reconcile するコントローラ」であって特定の言語ではありません。API サーバーは言語を問わず誰にでも同じ watch/REST インターフェースを提供します。したがって Python でも、シェルスクリプトでも、ほとんどコードを書かずとも Operator を作れます。

本記事は Go/kubebuilder 以外の代表的な選択肢を実際のコードとともに比較し、それぞれがどの状況に合うかの判断基準を示します。

全体像 — 非 Go Operator フレームワークの3分岐

非 Go の選択肢は大きく3つに分かれます。

1) 他の言語で reconcile を直接書く
   - Kopf (Python): デコレータベース、最も Go-Operator に近い体験

2) 宣言的フック (コード最小化)
   - Metacontroller: 状態を JSON in/out 関数に委ね、言語非依存

3) シェル/設定ベース
   - shell-operator: フックをシェルスクリプトで
   - Ansible Operator: reconcile を Ansible プレイブックで

核心の違いは「reconcile ロジックをどこに、どんな形で収めるか」です。Kopf は Python 関数に、Metacontroller は外部 webhook 関数に、shell/Ansible Operator はスクリプト/プレイブックに収めます。

Kopf — Python で作る Operator

Kopf(Kubernetes Operator Pythonic Framework)は Python 開発者にとって最も自然な選択です。デコレータでイベントハンドラを登録する方式が直感的で、Go-Operator の reconcile モデルと概念的に近いです。

次は仮想の CRD(kind: Database)に反応して ConfigMap を作る簡単な Kopf ハンドラです。

import kopf
import kubernetes


@kopf.on.create('example.com', 'v1', 'databases')
def create_fn(spec, name, namespace, logger, **kwargs):
    size = spec.get('size', 1)
    logger.info(f"Database {name} 作成要求 — size={size}")

    api = kubernetes.client.CoreV1Api()
    cm = kubernetes.client.V1ConfigMap(
        metadata=kubernetes.client.V1ObjectMeta(name=f"{name}-config"),
        data={"size": str(size)},
    )
    api.create_namespaced_config_map(namespace=namespace, body=cm)

    # status に反映する値を返すと Kopf が自動で記録する。
    return {"provisioned": True, "size": size}


@kopf.on.update('example.com', 'v1', 'databases')
def update_fn(spec, status, name, namespace, logger, **kwargs):
    new_size = spec.get('size', 1)
    logger.info(f"Database {name} 更新 — 新 size={new_size}")
    # 変更 reconcile ロジック...


@kopf.on.delete('example.com', 'v1', 'databases')
def delete_fn(name, logger, **kwargs):
    logger.info(f"Database {name} 整理")
    # finalizer の整理ロジックは Kopf が自動管理する。

Kopf の強みは次の通りです。

  • デコレータベースの明確なイベントモデル: on.create/on.update/on.delete/on.timer などで意図が鮮明です。
  • finalizer、リトライ、バックオフ、状態管理の自動化: Go で自前で配線せねばならない多くの部分を Kopf が処理します。
  • 豊かな Python エコシステム: データ処理、外部 API 連携、機械学習まで Python ライブラリをそのまま引き込めます。

注意点は、CRD 自体は別途定義して適用せねばならないことです。Kopf はハンドラを扱いますが CRD スキーマ生成を代わりにはしません。また Python ランタイムの特性上、Go に比べてメモリ使用とコールドスタートが重いです。

Metacontroller — コードなしで宣言的フックで

Metacontroller は発想が異なります。reconcile ループ全体を Metacontroller が運営し、ユーザーは「現在の状態(JSON)を受け取り、あるべき子リソース(JSON)を返す関数」だけを提供します。この関数は単純な HTTP エンドポイントなので、どの言語でも書けます。

Metacontroller の2つの核心コントローラは次の通りです。

  • CompositeController: 親 CR を見て子リソースの集合を作り維持します(例: 自作 CR -> Deployment + Service)。
  • DecoratorController: 既存リソースに追加の子や変更を付け足します。

CompositeController の動作フローはこうです。

[Metacontroller]
  親 CR + 現在の子たちを JSON にまとめて
        |  POST (sync 要求)
        v
[ユーザーの webhook 関数]  (Python/JS/何でも)
  desired な子リソースのリスト(JSON)を返す
        |
        v
[Metacontroller]
  返された desired 状態で子リソースを生成/修正/削除

sync フックが受け取る要求と返す応答はすべて単純な JSON です。例えば Python で書いた sync フックの核心はこうです。

from flask import Flask, request, jsonify

app = Flask(__name__)


@app.route('/sync', methods=['POST'])
def sync():
    observed = request.get_json()
    parent = observed['parent']
    replicas = parent['spec'].get('replicas', 1)
    name = parent['metadata']['name']

    # あるべき子リソース(Deployment)を JSON で構成して返す
    desired_deployment = {
        "apiVersion": "apps/v1",
        "kind": "Deployment",
        "metadata": {"name": name},
        "spec": {
            "replicas": replicas,
            "selector": {"matchLabels": {"app": name}},
            "template": {
                "metadata": {"labels": {"app": name}},
                "spec": {"containers": [{"name": "app", "image": "nginx"}]},
            },
        },
    }

    return jsonify({"status": {"replicas": replicas}, "children": [desired_deployment]})

Metacontroller の魅力は、reconcile の難しい部分(watch、キュー、リトライ、ガベージコレクション、所有関係)をすべて Metacontroller が引き受けることです。ユーザーは純粋関数(「この入力に対するあるべき出力」)だけを書きます。関数型の考えに慣れているなら非常にきれいです。欠点は、Metacontroller 自体をクラスタにインストール・運用せねばならないこと、そして複雑な命令型の運用ロジック(逐次作業、外部システム連携)には表現力が足りないことです。

shell-operator と Ansible Operator — スクリプト/プレイブックベース

shell-operator

shell-operator は「イベントが発生したらシェルスクリプト(または任意の実行ファイル)を実行」する単純で強力なフレームワークです。各フックは自分がどのイベントに反応するかの設定(JSON)を出力し、実際のイベントが来ると本体が実行されます。

#!/usr/bin/env bash

if [[ $1 == "--config" ]]; then
  # このフックが購読するイベントを宣言
  cat <<EOF
configVersion: v1
kubernetes:
  - apiVersion: v1
    kind: ConfigMap
    executeHookOnEvent: ["Added", "Modified"]
EOF
else
  # 実際のイベント処理: バインディングコンテキストがファイルパスで渡される
  echo "ConfigMap イベントを検知、kubectl で後続作業を実行"
  # kubectl ... のようなコマンドで reconcile
fi

shell-operator は運用自動化をすでにシェル/kubectl でやっているチームに馴染みます。ただし冪等性、エラー処理、リトライをすべてスクリプト作者が責任を負うので、複雑になるほど管理が難しくなります。

Ansible Operator

Operator SDK は Go 以外に Ansible ベースの Operator も支援します。CR の変化を watch すると、それに対応する Ansible プレイブックが実行されてあるべき状態を実現します。すでに Ansible でインフラを管理していた組織が、その資産をそのまま Kubernetes の reconcile へ移すのに良いです。冪等性は Ansible モジュールが相当部分保証してくれるのが利点です。

Kopf をさらに深く — 周期実行と冪等性

Kopf は単発のイベントハンドラだけでなく、周期的な reconcile も支援します。外部の状態(例: クラウド資源、SaaS API)を周期的に点検してクラスタを合わせるのに有用です。

import kopf


@kopf.timer('example.com', 'v1', 'databases', interval=60.0)
def reconcile_periodically(spec, status, name, logger, **kwargs):
    # 60秒ごとに呼ばれ、desired state を外部と照合する。
    desired = spec.get('size', 1)
    current = status.get('observedSize')
    if current != desired:
        logger.info(f"ドリフト検知: 現在={current}, 目標={desired}")
        # 外部システムを desired に合わせるロジック
        return {"observedSize": desired}
    # 変化がなければ何も返さず status 更新を避ける (冪等)

ここで重要な原則が 冪等性 です。timer ハンドラは60秒ごとに呼ばれるので、毎回無条件に外部 API を呼んだり status を更新すると、負荷と無限更新ループを招きます。「すでに目標状態なら何もしない」をコードに明示せねばなりません。この原則は Go-Operator とまったく同じで、言語が変わっても reconcile の本質は変わらないことを示します。

Kopf はこのほかにも子リソースの watch(on.event)、サブハンドラ、エラー時の指数バックオフ、並行性制御などを提供します。Python チームがプロダクション級の Operator を作るのに十分な機能セットです。

デプロイ方法の比較 — 各フレームワークはどうクラスタに乗るか

作った Operator を実際にクラスタへ乗せる方式もフレームワークごとに異なります。

  • Kopf: ハンドラコードを収めたコンテナイメージを作り Deployment で起動します。CRD は別途 YAML で適用します。RBAC も自分で定義して付けねばなりません。
  • Metacontroller: まず Metacontroller 自体をクラスタにインストールした後、sync フック(Web サーバー)を Deployment で起動し、CompositeController リソースで「どの親を見てどのフックを呼ぶか」を宣言します。
  • shell-operator: shell-operator ベースイメージにフックスクリプトを載せたイメージを作り Deployment で起動します。
  • Ansible Operator: Operator SDK がプレイブックを収めたイメージとマニフェストを生成してくれます。
共通構造 (どのフレームワークでも)
  [コンテナイメージ]  <- reconcile ロジック(コード/スクリプト/プレイブック)
        +
  [CRD]             <- ユーザーが宣言する語彙
        +
  [RBAC]            <- コントローラが扱うリソースの権限
        +
  [Deployment]      <- コントローラを実際に実行

言語とフレームワークが違っても「イメージ + CRD + RBAC + 実行ワークロード」という骨格は同じです。この共通構造を理解すれば、どのフレームワークに出会っても素早く適応できます。

比較テーブル — 一目で見る選択肢

項目Go/kubebuilderKopf(Python)Metacontrollershell-operatorAnsible Operator
主言語GoPython言語非依存(JSON)シェル/任意Ansible YAML
学習曲線低(Ansible 経験時)
表現力非常に高中(宣言的)低~中
性能/リソース最良普通普通軽量普通
冪等性の責任開発者一部自動フレームワーク開発者Ansible モジュール
エコシステム成熟度最も成熟成熟安定・ニッチ安定・ニッチ成熟
適した状況プロダクション標準Python チーム、ML 連携単純な合成パターン軽量な運用自動化Ansible 資産あり

この表から導かれる単純な指針はこうです。プロダクション標準が必要で長期保守が見込まれるなら、Go が依然として第一候補です。しかしチームの力量、自動化の複雑度、既存資産によっては、非 Go の選択肢がはるかに速く合理的な道になります。

プロトタイピング vs プロダクション

ツール選択で最も重要な質問の1つは「これはプロトタイプか、プロダクションか」です。

  • プロトタイピング段階: アイデア検証と素早い反復が目的なら、Kopf や Metacontroller が圧倒的に速いです。数時間で動く Operator を作れ、CRD 設計の妥当性を素早く確認できます。
  • プロダクション段階: 数千のリソースを扱い、SLA を保証し、数年保守せねばならないなら、性能とエコシステムの成熟度が重要になります。このとき Go の controller-runtime が持つ性能、メモリ効率、豊富なツールが有利です。

賢い戦略の1つは「Kopf でプロトタイプを素早く作り、CRD スキーマと reconcile ロジックの価値を検証した後、本当に大規模・長寿命が確定したら Go で再実装」することです。CRD スキーマ自体は言語非依存なので、ユーザーインターフェース(CR)を変えずに内部実装だけ交換できます。

言語選択の基準

選択を左右する実質的な要因は次の通りです。

  1. チームの力量: チームが Python に長け Go 経験がないなら、Go へ無理に行くより Kopf でうまく作った Operator の方が運用上安全です。保守する人が読んで直せるコードが最高のコードです。
  2. 外部連携の性質: reconcile の中で機械学習推論、複雑なデータ変換、特定 SaaS の SDK 呼び出しが必要なら、その SDK が整った言語(よく Python)が有利です。
  3. 自動化の複雑度: 単純な「親 -> 子」合成なら Metacontroller でコードをほとんど書かずに済みます。逆に逐次ステップと状態機械が複雑なら命令型言語(Go/Python)が良いです。
  4. 既存資産: すでに Ansible でインフラを回しているなら、Ansible Operator が資産再利用の面で合理的です。

性能とリソース — 言語選択の隠れたコスト

非 Go フレームワークの魅力は開発速度ですが、ランタイムコストも併せて見るべきです。

  • メモリフットプリント: Go コントローラは通常数十 MB と軽量です。Python(Kopf)はインタプリタとライブラリのため重く、扱うオブジェクト数が増えるとキャッシュメモリも一緒に増えます。
  • コールドスタート: Go バイナリは即座に起動します。Python はインポートと初期化に時間がかかります。ほとんどの Operator は長期実行なのでコールドスタートが致命的になることは稀ですが、再起動が頻繁な環境では差を感じます。
  • スループット: 数万のリソースを素早く reconcile せねばならない大規模なら、Go の効率が明確な利点です。Metacontroller や webhook ベースは HTTP の往復が挟まり遅延が加わります。
  • 運用コンポーネント数: Metacontroller は自身 + ユーザーフックという2つのコンポーネントを起動せねばなりません。shell-operator も同様に別ランタイムです。「自分のロジックは軽くても、その上のフレームワークが重いかもしれない」点を忘れないでください。

これらのコストは小規模・プロトタイプではほぼ無視できます。しかし大規模プロダクションで累積すると無視できなくなります。だからこそ「プロトタイプは速いツールで、大規模プロダクションは Go で」という典型的な経路が合理的なのです。

移行 — フレームワーク間の移動

非 Go から Go へ(またはその逆へ)移すのは思ったより滑らかです。核心は CRD という契約はそのままにして実装だけを変える ことです。

  1. CR スキーマを固定する。 ユーザーが見るインターフェース(apiVersion、kind、spec フィールド)は移行中変わってはいけません。
  2. 等価性テストを準備する。 同じ CR 入力に対して旧実装と新実装が同一の子リソース/状態を作るか比較します。
  3. コントローラを交換する。 1つのクラスタで1時点には1つのコントローラだけが該当 CRD を reconcile すべきです。2つの実装が同時に同じ CR を触ると衝突します。
  4. 段階的移行。 可能なら新実装を別ネームスペース/別 CRD バージョンで検証してから切り替えます。

エコシステムの成熟度 — 現実的な視点

各ツールの成熟度は正直に評価する必要があります。

  • Go/kubebuilder/controller-runtime: 最も活発で、Kubernetes コアと歩調を合わせて発展します。ほぼすべての商用 Operator がこのスタックです。長期的に最も安全な選択です。
  • Kopf: Python エコシステムで事実上標準の Operator フレームワークとして安定しています。Python チームに検証された道です。
  • Metacontroller: 発想が優雅で安定していますが、適用範囲が「宣言的合成」というニッチに特化しています。万能ツールではありません。
  • shell-operator/Ansible Operator: 特定の運用パターンで非常に実用的ですが、複雑な状態管理には限界が明確です。

成熟度を見るときは「プロジェクトが生きているか、ユースケースが自分の問題に似ているか、問題が起きたとき参考にする資料とコミュニティがあるか」を併せて見るのが良いです。

落とし穴集

  • 言語が易しくても reconcile が易しいわけではない: Python だからコードが短くても、冪等性・競合・無限ループといったコントローラ本来の難題はそのまま残ります。フレームワークが一部を隠してくれるだけです。
  • 状態(status)更新の無限ループ: ハンドラが status を更新し、その更新がまたハンドラを起こすパターンはすべてのフレームワークで起こり得ます。変更が本当に必要なときだけ使うようガードを置いてください。
  • Metacontroller フックの純粋性: sync フックは副作用なしに「入力 -> あるべき出力」だけを計算すべきです。フックの中で直接 kubectl でリソースを作ると Metacontroller のモデルと衝突します。
  • シェル/Ansible の冪等性の欠落: スクリプトが毎回同じ結果を保証できないと、reconcile のたびに副作用が累積します。「すでにあれば何もしない」を明示的に実装してください。
  • 運用負担の移転: Metacontroller や shell-operator は、それ自体がクラスタにインストール・運用すべきもう1つのコンポーネントです。「自分の Operator は軽くても、それを動かすフレームワークは重いかもしれない」ことを忘れないでください。
  • CRD 管理責任の分散: 非 Go フレームワークの多くは CRD を自動生成してくれません。CRD YAML を別途作成・バージョン管理せねばならず、スキーマ検証(OpenAPI v3)も自分で面倒を見る必要があります。CRD を忘れると、コントローラは watch する対象がなく、静かに何もしません。
  • RBAC 欠落による沈黙の失敗: 権限が不足すると watch や create が拒否されますが、フレームワークによってはこのエラーがログの奥深くに埋もれ、「なぜ reconcile が動かないのか」としばらく迷うことになります。デプロイ直後にコントローラログで forbidden メッセージがないかをまず確認してください。

Metacontroller DecoratorController — 既存リソースに付け足す

先の CompositeController が「親 CR -> 子リソース」を扱うのに対し、DecoratorController は発想が少し異なります。既存リソース(ビルトインまたは別の CR)に追加の振る舞いを「装飾(decorate)」します。例えば「特定ラベルが付いた Deployment ができたら、常にペアとなる Service を一緒に作る」といった規則を、ほぼコードなしで実装できます。

DecoratorController の使用フロー
  対象: ラベル app-type=web が付いた Deployment
        |
        v
  Metacontroller が sync フックを呼ぶ (該当 Deployment を JSON で渡す)
        |
        v
  フックが「この Deployment に付随する Service」の desired JSON を返す
        |
        v
  Metacontroller が Service を生成/維持

このパターンは前の記事で扱った「CRD なしのコントローラ」のアイデアと出会います。つまり DecoratorController を使えば、新しい CRD を作らずとも、既存リソースに対する自動化を宣言的フックで実装できます。Go コントローラを書くより参入障壁がはるかに低いです。

適合シナリオ — 具体的な事例で見る

各フレームワークが輝く実際の状況を描いてみると、選択がぐっと楽になります。

Kopf が合う場合

社内の機械学習プラットフォームチームが「TrainingJob という CR ができたらデータセットを検証し、外部のフィーチャーストア API を呼んでメタデータを取得した後、適切な Job を作る」という自動化を望んでいます。このロジックは Python のデータライブラリと社内 SDK(やはり Python)に深く依存します。チームの主力言語も Python です。このとき Go へ行くと SDK を再ラップするのに時間を浪費します。Kopf が明らかな正解です。

Metacontroller が合う場合

「うちの会社の標準マイクロサービス CR(kind: Microservice)を作ると、常に Deployment + Service + HPA + ServiceMonitor のセットが一緒にできるべき」という要求があります。ロジックは純粋に「この spec -> これらの子」です。命令型の手順も外部呼び出しもありません。CompositeController の sync フックで入力 spec を受け取り desired な子リストを返せば終わりです。Go コントローラを新たに書く理由がありません。

shell-operator が合う場合

運用チームがすでにすべての自動化を kubectl と bash で行っており、「特定のネームスペースに Secret が追加されたら外部の秘密管理システムに同期」という小さな自動化が1つだけ必要です。新しい言語を学んだりビルドパイプラインを作る余裕がありません。shell-operator で既存のスクリプト資産をほぼそのままイベント駆動に変えられます。

Ansible Operator が合う場合

すでに数百の Ansible プレイブックでオンプレミスインフラを管理していた組織が、その運用知識を Kubernetes の reconcile へ移したいと考えています。プレイブックをほぼ再利用しながら CR ベースでトリガーでき、学習コストが最も低いです。

これらの事例の共通の教訓は 「文脈がツールを決める」 ことです。同じ「Operator を作る」という目標でも、チームの言語・既存資産・ロジックの性質によって最適なツールはまったく異なります。

決定チェックリスト — うちのチームは何を選ぶべきか

最後に、実際の選択を助けるチェックリストです。上から順に答えてみてください。

  1. すでによく管理された外部 Operator があるか? あるなら自分で作らずそれを使ってください。最も安価な選択です。
  2. 新しい CRD が本当に必要か、それともビルトインリソースの自動化で十分か? 後者なら(前の記事の)CRD なしのコントローラや Metacontroller DecoratorController を検討してください。
  3. プロトタイプか、プロダクションか? プロトタイプなら Kopf や Metacontroller で素早く。大規模プロダクションなら Go を真剣に検討。
  4. チームの主力言語は? Python チームなら Kopf が自然です。Ansible 資産があれば Ansible Operator。
  5. ロジックは宣言的か、命令型か? 「入力 -> あるべき出力」できれいに表現できるなら Metacontroller。複雑な逐次手順なら Kopf/Go。
  6. 長期保守の主体が読んで直せるか? これが最も重要です。どんなにかっこいいツールでも、チームが扱えなければ負債です。

この6つの質問を通せば、ほとんどの場合自然に1つか2つの候補に絞られます。核心は「他人が使っているから」ではなく「うちの問題とうちのチームに合うから」を基準に選ぶことです。

一行要約 — 5つのフレームワークの正体

最後に、各ツールを一文で刻んでおきます。

  • Go/kubebuilder: 最も強力で成熟した正統な道。プロダクション標準だが参入コストが高い。
  • Kopf(Python): Go-Operator の体験を Python で最も忠実に再現。Python チームの第一候補。
  • Metacontroller: reconcile の難しさをフレームワークに委ね、ユーザーは純粋関数だけ。宣言的合成の名手。
  • shell-operator: シェル/kubectl 資産をイベント駆動へ引き上げる最も軽い橋。
  • Ansible Operator: 既存の Ansible 運用知識を Kubernetes の reconcile へ移す通路。

この5行を覚えておけば、新しい自動化要求に出会ったとき、頭の中で素早く候補を思い浮かべられます。

エコシステムのトレンド — 2026年時点で

非 Go ツールを導入する前に、エコシステムの現在地を押さえておく必要があります。

  • Go スタックの継続的進化: kubebuilder と controller-runtime は Kubernetes コアと歩調を合わせて発展しています。最新ラインは Kubernetes 1.36、Go 1.26 をサポートし、メトリクスエンドポイント保護のための別サイドカー(kube-rbac-proxy)を削除して controller-runtime 内蔵の認証/認可に置き換えるなど、運用の簡素化が進んでいます。自社 Operator を作るならこの流れに追随せねばなりません。
  • 非 Go ツールの安定化: Kopf と Metacontroller は活発な新機能追加より安定性と成熟度に重きを置いた段階です。これは欠点ではなく、基盤技術として信頼できるという信号と読むのが正しいです。
  • ポリシーエンジンとの境界: 単純な検証/変形は Kyverno のようなポリシーエンジンが吸収する傾向です。「これは Operator ではなくポリシーで十分ではないか」を常にまず自問してください。

要約すると、2026年でも「プロトタイプと特殊言語の要求は非 Go で、大規模標準プロダクションは Go で」という大きな構図は維持されています。

おわりに

Operator は Go の専有物ではありません。Kubernetes API はすべての言語に公平に開かれており、Kopf、Metacontroller、shell-operator、Ansible Operator はそれぞれ異なる強みでその扉を通ります。重要なのは「最もかっこいいツール」ではなく「うちのチームが素早く作り長く保守できるツール」です。

素早い検証には Kopf と Metacontroller が、大規模・長寿命のプロダクションには Go が依然として強いです。そして両者の間には CRD という安定した契約があり、必要ならインターフェースを壊さずに実装だけ入れ替えられます。言語ではなく問題の本質(reconcile)をまず理解すれば、ツールは自然と従います。

最後にもう1つ強調したいです。「非 Go フレームワークを使う」ことは「妥協する」という意味ではありません。多くの組織が Kopf で作った Operator を数年間プロダクションで安定運用しており、Metacontroller で標準的なプラットフォーム抽象を構築した事例も多くあります。重要なのは、ツールの限界を正確に知り、その限界が自分の問題に触れない範囲で最も生産的な道を選ぶことです。ツールの名声ではなく、自分のチームの実際の力量と問題の形に合わせてください。それが長く生き残る自動化を作る最も確実な方法です。

参考資料