ハイブリッドモード:コントロールプレーン、データプレーン、設定同期
前提知識
- ›記事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サーバーを管理しています。処理の流れは次のとおりです。
- DPがCPのクラスターリスナーへWebSocket接続を試みる
- CPはmTLSを通じてDPのクライアント証明書を検証する(57〜62行目の
validate_client_cert) - CPはCPとDPのバージョン間でプラグイン・フィルターの互換性を確認する
- CPは現在の設定をエクスポートし、圧縮ペイロードとして送信する
- 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.luaのCLUSTERING_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メッセージを受信すると、次の処理が実行されます。
- 解凍:
inflate_gzipでgzipペイロードを展開する - パース:JSONをLuaテーブルに変換する
- バリデーション:宣言的設定スキーマに対して検証する
- ロード:宣言的設定パイプラインを通じてLMDBへ書き込む(第5回で解説したファイルベースのDBレス設定と同じパイプライン)
- リビルド:ルーターとプラグインイテレーターを再構築する
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_rpcとcluster_rpc_syncの設定オプションで制御されます。両方が有効な場合、Kongは従来のフル設定プッシュの代わりにRPCフレームワークを使って同期を行います。
セキュリティ:プレーン間のmTLS
CP/DP間の通信はすべてmutual TLS(mTLS)で保護されています。cluster_certとcluster_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ゲートウェイ機能を探っていきます。