Read OSS

ハイブリッドモード:コントロールプレーン、データプレーン、設定同期

上級

前提知識

  • 記事1〜5(データベース層までの全体アーキテクチャ)
  • WebSocketプロトコルの基礎知識
  • 分散システムの概念(結果整合性など)への理解

ハイブリッドモード:コントロールプレーン、データプレーン、設定同期

本番環境のKongは、単一ノードで動かすケースはほとんどありません。ハイブリッドモードアーキテクチャは役割を明確に分離します。コントロールプレーン(CP)はAdmin APIとデータベースを通じて設定を管理し、データプレーン(DP)はCPから受け取った設定を使ってトラフィックをプロキシします。この分離により、データプレーンはデータベース接続が不要になり、隔離されたネットワークセグメントで稼働させることも、独立してスケールすることも可能になります。

本記事では、CP/DP間の通信パイプラインを初期化時のロール検出から、WebSocketベースの設定プッシュ、さらに新しいインクリメンタル同期システムまで順を追って解説します。

ハイブリッドモードのアーキテクチャとロール検出

第2回で触れたとおり、ロール検出はKong.init()の早い段階で行われます。201〜218行目のシンプルな設定チェックがその実体です。

is_data_plane = function(config) return config.role == "data_plane" end
is_control_plane = function(config) return config.role == "control_plane" end

role設定(KONG_ROLE環境変数またはkong.confで指定)は、初期化フロー全体を決定します。

  • コントロールプレーン:Postgresへ接続し、Admin APIを起動する。ルータービルドはスキップ(トラフィックをプロキシしないため)。DP向けのWebSocketサーバーを起動する。
  • データプレーン:DBレスモード(LMDB)で動作し、Admin APIはスキップ。CPへのWebSocketクライアントを起動し、トラフィックをプロキシする。
  • トラディショナル:Postgresへ接続し、Admin APIとプロキシの両方を実行する。

クラスタリングモジュールはKong.init()701〜713行目で初期化されます。

if is_http_module and (is_data_plane(config) or is_control_plane(config)) then
  kong.clustering = require("kong.clustering").new(config)
  if config.cluster_rpc then
    kong.rpc = require("kong.clustering.rpc.manager").new(config, kong.node.get_id())
    if config.cluster_rpc_sync then
      kong.sync = require("kong.clustering.services.sync").new(db, is_control_plane(config))
    end
  end
end
flowchart TD
    subgraph "Control Plane"
        A[Admin API] --> B[(PostgreSQL)]
        B --> C[Config Export]
        C --> D[WebSocket Server]
    end
    subgraph "Data Plane 1"
        E[WebSocket Client] --> F[Declarative Config Loader]
        F --> G[(LMDB)]
        G --> H[Router + Plugins]
        H --> I[Proxy Traffic]
    end
    subgraph "Data Plane 2"
        J[WebSocket Client] --> K[Declarative Config Loader]
        K --> L[(LMDB)]
        L --> M[Router + Plugins]
        M --> N[Proxy Traffic]
    end
    D <-->|mTLS| E
    D <-->|mTLS| J

コントロールプレーン:WebSocketによる設定ブロードキャスト

コントロールプレーンモジュールはkong/clustering/init.luaの80行目でインスタンス化されます。

function _M:init_cp_worker(basic_info)
  events.init()
  self.instance = require("kong.clustering.control_plane").new(self)
  self.instance:init_worker(basic_info)
end

kong/clustering/control_plane.luaモジュールは、DPからの接続を受け付けるWebSocketサーバーを管理しています。処理の流れは次のとおりです。

  1. DPがCPのクラスターリスナーへWebSocket接続を試みる
  2. CPはmTLSを通じてDPのクライアント証明書を検証する(57〜62行目validate_client_cert
  3. CPはCPとDPのバージョン間でプラグイン・フィルターの互換性を確認する
  4. CPは現在の設定をエクスポートし、圧縮ペイロードとして送信する
  5. Admin APIやマイグレーションによって設定が変更されると、CPは接続中の全DPへ更新済み設定をプッシュする

エクスポート処理では、データベースから全エンティティを宣言的設定フォーマットにシリアライズし、古いDPバージョン向けの互換性変換を適用したうえで、gzipでペイロードを圧縮します。67行目の関数がその処理を担っています。

local function handle_export_deflated_reconfigure_payload(self)
  local ok, p_err, err = pcall(self.export_deflated_reconfigure_payload, self)
  return ok, p_err or err
end

CPは各DPとのping/pongハートビートを維持しています(constants.luaCLUSTERING_PING_INTERVALにより30秒間隔)。ハートビートが途絶えたDPはclustering_data_planesエンティティ上でオフラインとしてマークされ、Admin APIのGET /clustering/data-planesエンドポイントから確認できます。

sequenceDiagram
    participant DP as Data Plane
    participant CP as Control Plane
    participant DB as PostgreSQL

    DP->>CP: WebSocket connect + mTLS cert
    CP->>CP: validate_client_cert()
    CP->>CP: Check plugin compatibility
    CP->>DB: Export all entities
    CP->>CP: Serialize + gzip compress
    CP->>DP: RECONFIGURE payload
    DP->>DP: Apply declarative config
    DP->>CP: PONG (heartbeat)
    
    Note over CP: Admin API changes config
    CP->>DB: Write changes
    CP->>CP: Re-export config
    CP->>DP: RECONFIGURE (updated)
    DP->>DP: Rebuild router + plugins

データプレーン:設定の受信と適用

データプレーン側の実装はkong/clustering/data_plane.luaにあります。DPはクラスター証明書を使ってCPに接続するWebSocketクライアントを生成します。

function _M.new(clustering)
  local self = {
    declarative_config = kong.db.declarative_config,
    conf = clustering.conf,
    cert = clustering.cert,
    cert_key = clustering.cert_key,
  }
  return setmetatable(self, _MT)
end

DPがRECONFIGUREメッセージを受信すると、次の処理が実行されます。

  1. 解凍inflate_gzipでgzipペイロードを展開する
  2. パース:JSONをLuaテーブルに変換する
  3. バリデーション:宣言的設定スキーマに対して検証する
  4. ロード:宣言的設定パイプラインを通じてLMDBへ書き込む(第5回で解説したファイルベースのDBレス設定と同じパイプライン)
  5. リビルド:ルーターとプラグインイテレーターを再構築する

kong/runloop/handler.luaの再設定ハンドラーは、処理時間をログに記録します。

local reconfigure_time = get_monotonic_ms() - reconfigure_started_at
if ok then
  log(INFO, "declarative reconfigure took ", reconfigure_time,
            " ms on worker #", worker_id)
end

各ワーカーは独立して再設定イベントを処理します。949行目events.register_events(reconfigure_handler)呼び出しがワーカーイベントへのハンドラー登録を行っています。あるワーカーがWebSocket経由でCPから設定を受信すると、イベントが発行され、全ワーカーのリビルドがトリガーされます。

ヒント: DPは設定のハッシュ値を保持しており、受信ペイロードと照合します。ハッシュが一致した場合は再設定をスキップするため、CPが同一の設定をプッシュしても(CPの再起動後など)、不要なルーター再構築が発生しません。

RPCフレームワークとインクリメンタル同期

従来のCP→DP同期には制約があります。設定変更のたびに設定全体を送信しなければならない点です。ルートが数千件あるデプロイ環境では、変更のたびに数メガバイト規模のペイロードが発生することになります。

kong/clustering/rpc/manager.luaの新しいRPCフレームワークは、WebSocket上のJSON-RPC v2を使ってCPとDP間の双方向通信を実現します。RPCマネージャーはクライアント接続とケイパビリティネゴシエーションを管理します。

function _M.new(conf, node_id)
  local self = {
    clients = {},
    client_capabilities = {},
    node_id = node_id,
    conf = conf,
    cluster_cert = assert(clustering_tls.get_cluster_cert(conf)),
    cluster_cert_key = assert(clustering_tls.get_cluster_cert_key(conf)),
    callbacks = callbacks.new(),
  }

RPCフレームワークの上に構築されたkong/clustering/services/sync/init.luaインクリメンタル同期システムは、差分ベースの設定更新を可能にします。

function _M.new(db, is_cp)
  local strategy = strategy.new(db)
  local self = {
    db = db,
    strategy = strategy,
    rpc = rpc.new(strategy),
    is_cp = is_cp,
  }
  if is_cp then
    self.hooks = require("kong.clustering.services.sync.hooks").new(strategy)
  end
  return setmetatable(self, _MT)
end

同期システムはCP側でDAOフックを使って変更を追跡します。エンティティの作成・更新・削除が発生すると、フックがその差分を記録します。DPは定期的にkong.sync.v2 RPCを呼び出し、最後に確認したバージョン以降の変更のみを取得します。

41〜80行目のDP側では、RPCの準備完了イベントを登録し、同期を開始します。

worker_events.register(function(capabilities_list)
  for _, v in ipairs(capabilities_list) do
    if v == "kong.sync.v2" then
      has_sync_v2 = true
      break
    end
  end
end, "clustering:jsonrpc", "connected")
flowchart LR
    subgraph "Full Sync (v1)"
        A[CP] -->|Entire config payload| B[DP]
    end
    subgraph "Incremental Sync (v2/RPC)"
        C[CP] -->|Delta: +route, -plugin, ~service| D[DP]
        D -->|"kong.sync.v2 RPC: last_version=42"| C
    end

インクリメンタル同期はcluster_rpccluster_rpc_syncの設定オプションで制御されます。両方が有効な場合、Kongは従来のフル設定プッシュの代わりにRPCフレームワークを使って同期を行います。

セキュリティ:プレーン間のmTLS

CP/DP間の通信はすべてmutual TLS(mTLS)で保護されています。cluster_certcluster_cert_key設定オプションで指定した証明書が、クライアント認証とサーバー認証の両方に使われます。CPはDPの証明書を自身のCAに対して検証し、DPも同様にCPの証明書を検証します。

検証処理はkong/clustering/init.luaで行われます。

function _M:validate_client_cert(cert_pem)
  cert_pem = cert_pem or ngx_var.ssl_client_raw_cert
  return validate_client_cert(self.conf, self.cert, cert_pem)
end

この相互認証により、認可されたデータプレーンだけがコントロールプレーンから設定を受け取れるようになります。設定にはAPIキーやアップストリームの認証情報といった機密データを含む可能性があるため、この仕組みは不可欠です。

第7回では、Kongの最新の主要サブシステムである、複数プロバイダーにまたがるLLMリクエストのプロキシと変換を実現するAIゲートウェイ機能を探っていきます。