Read OSS

Kong Gateway アーキテクチャ:API ゲートウェイが Nginx の内側に宿る仕組み

中級

前提知識

  • リバースプロキシと API ゲートウェイの基本的な理解
  • Nginx の概念(ワーカープロセス、設定ディレクティブ、upstream/downstream)への慣れ
  • Lua の基本的な構文(テーブル、メタテーブル、require/モジュールシステム)

Kong Gateway アーキテクチャ:API ゲートウェイが Nginx の内側に宿る仕組み

多くの API ゲートウェイは、HTTP トラフィックをプロキシするスタンドアロンアプリケーションです。しかし Kong は違います。Nginx の前に置かれるだけでなく、Nginx の内側に宿っています。Kong のリクエスト処理ロジックはすべて、Nginx ワーカープロセス内で Lua コードとして実行され、Nginx のリクエストライフサイクルの適切なタイミングで呼び出されます。このアーキテクチャにより、Nginx のイベントループが持つ高いパフォーマンスを活かしながら、完全にプログラム可能なプラグインシステムという柔軟性を実現しています。

全 7 回シリーズの第 1 回となるこの記事では、Kong が Nginx に組み込まれる仕組み、Lua ハンドラと Nginx フェーズのマッピング、コードベースの構成、そして後続のリクエスト処理すべてが依存するランタイム環境のブートストラップについて解説します。

OpenResty:Lua アプリケーションサーバーとしての Nginx

Kong は Nginx を直接使用しません。Nginx に lua-nginx-module をバンドルしたディストリビューションである OpenResty を使用します。これにより、Nginx ワーカープロセス内で Lua コードを実行できます。OpenResty は Nginx の内部フェーズを *_by_lua_block ディレクティブとして公開しており、リクエスト処理の特定のタイミングで Lua コードを実行するフックとして機能します。

これは後付けのスクリプト機能ではありません。lua-nginx-module はコルーチンベースの cosocket API を提供しており、Nginx のイベントループを離れることなく、データベースクエリや HTTP 呼び出し、DNS ルックアップといったノンブロッキング I/O を Lua コードから実行できます。upstream API を呼び出したりデータベースにクエリを投げたりする Kong プラグインはすべて、この cosocket レイヤーを通じて動作します。そのため、1 つのワーカープロセスで何千もの同時接続を処理できます。

重要なポイントは、Kong の Lua コードは Nginx プロセスと並んで実行されるのではなく、内側で実行されるということです。プロセス間通信のオーバーヘッドもシリアライゼーションの境界もありません。ngx グローバルはどこでも利用でき、共有メモリゾーン(lua_shared_dict)によってワーカー間で高速にデータを共有できます。

flowchart TD
    subgraph "Nginx Master Process"
        A[Master: manages workers]
    end
    subgraph "Nginx Worker Process 1"
        B[Event Loop]
        C[lua-nginx-module]
        D[Kong Lua Code]
        B --> C --> D
    end
    subgraph "Nginx Worker Process N"
        E[Event Loop]
        F[lua-nginx-module]
        G[Kong Lua Code]
        E --> F --> G
    end
    A --> B
    A --> E
    H[lua_shared_dict: kong_db_cache] <-.-> D
    H <-.-> G

フェーズ駆動型ライフサイクル:Kong が Nginx に組み込まれる仕組み

Kong が Nginx に組み込まれる起点が、kong/templates/nginx_kong.lua の Nginx テンプレートです。各 *_by_lua_block ディレクティブがグローバルな Kong テーブルのメソッドに処理を委譲しています:

init_by_lua_block {
    Kong = require 'kong'
    Kong.init()
}
init_worker_by_lua_block {
    Kong.init_worker()
}

server ブロックのさらに下、145〜163 行目では、リクエスト処理フェーズも同様に接続されています:

rewrite_by_lua_block { Kong.rewrite() }
access_by_lua_block { Kong.access() }
header_filter_by_lua_block { Kong.header_filter() }
body_filter_by_lua_block { Kong.body_filter() }
log_by_lua_block { Kong.log() }

balancer は upstream ブロック内の 85 行目balancer_by_lua_block を通じて接続されています。フェーズの対応関係をまとめると次の通りです:

Nginx ディレクティブ Kong ハンドラ 責務
init_by_lua_block Kong.init() スキーマの読み込み、DB 接続、ルーターの構築
init_worker_by_lua_block Kong.init_worker() タイマーのセットアップ、キャッシュのウォームアップ、クラスタリング
ssl_certificate_by_lua_block Kong.ssl_certificate() 動的 TLS 証明書の選択
rewrite_by_lua_block Kong.rewrite() ワークスペースの検出、グローバルプラグインの実行
access_by_lua_block Kong.access() ルーティング、認証、プラグインの実行
balancer_by_lua_block Kong.balancer() upstream ターゲットの選択、リトライ
header_filter_by_lua_block Kong.header_filter() レスポンスヘッダーの操作
body_filter_by_lua_block Kong.body_filter() レスポンスボディの変換
log_by_lua_block Kong.log() ロギング、メトリクス、クリーンアップ

これらすべてのハンドラは、コードベース全体の中心となるファイル kong/init.lua で定義されています。

flowchart LR
    A[Client Request] --> B[rewrite]
    B --> C[access]
    C --> D[balancer]
    D --> E[Upstream]
    E --> F[header_filter]
    F --> G[body_filter]
    G --> H[log]
    H --> I[Client Response]

ディレクトリ構成とモジュールマップ

kong/ ディレクトリは責務ごとに整理されています。コードベースを読み解くうえで、このマップを把握しておくことは欠かせません:

ディレクトリ 目的
kong/cmd/ CLI コマンド(startstopreloadmigrations
kong/conf_loader/ 設定の読み込み、検証、マージ
kong/db/ データベースレイヤー:スキーマ、DAO、ストラテジ(Postgres、LMDB)
kong/runloop/ コアリクエスト処理:ハンドラ、ルーター、バランサー、プラグインイテレータ
kong/router/ ルートマッチングエンジン(トラディショナルおよびエクスプレッションベース)
kong/pdk/ Plugin Development Kit — プラグイン向けの安定した API サーフェス
kong/plugins/ バンドルされたプラグイン実装(45 個)
kong/clustering/ ハイブリッドモード:CP/DP 通信、設定同期
kong/llm/ AI ゲートウェイ:LLM プロバイダードライバーとアダプター
kong/api/ Admin API エンドポイントの生成とルーティング
kong/tools/ ユーティリティモジュール(gzip、文字列、UUID、時間など)
kong/templates/ Nginx 設定テンプレートとデフォルト設定

バンドルされたプラグインは kong/constants.lua でリストアップされており、jwtkey-auth から新しい ai-proxyai-prompt-guard まで 45 個のプラグイン名が並んでいます。このリストが初期化時のプラグイン検出を駆動します:

local plugins = {
  "jwt", "acl", "correlation-id", "cors", "oauth2",
  -- ... 36 more plugins ...
  "ai-proxy", "ai-prompt-decorator", "standard-webhooks", "redirect"
}

ヒント: constants.lua はブックマークしておく価値があります。プラグインリストの他に、CORE_ENTITIES(スキーマの読み込み順序は重要で、依存関係のあるものが先になります)、ENTITY_CACHE_STORE マッピング、プロトコル定義、クラスタリング定数なども定義されています。

グローバル Kong オブジェクトとランタイムパッチ

リクエスト処理を始めるにあたって、Kong には 2 つのものが必要です。共有名前空間として機能するグローバルな kong オブジェクトと、Lua/ngx の動作を調整するランタイムパッチです。

グローバル kong オブジェクトは kong/global.lua_GLOBAL.new() を通じて生成されます:

function _GLOBAL.new()
  return {
    version = KONG_VERSION,
    version_num = KONG_VERSION_NUM,
    configuration = nil,
  }
end

この素のテーブルは初期化の過程で段階的に埋められていきます。リクエストが処理される時点では、kong.dbkong.cachekong.core_cachekong.worker_eventskong.cluster_eventskong.dnskong.router などがすべてこのオブジェクトにアタッチされています。PDK の名前空間(kong.requestkong.responsekong.service など)は、161 行目_GLOBAL.init_pdk() を通じて初期化されます。

sequenceDiagram
    participant init.lua
    participant global.lua
    participant PDK
    init.lua->>global.lua: _GLOBAL.new()
    global.lua-->>init.lua: bare kong table {version, version_num}
    init.lua->>global.lua: _GLOBAL.init_pdk(kong, config)
    global.lua->>PDK: PDK.new(config, kong)
    PDK-->>global.lua: attaches kong.request, kong.response, etc.
    init.lua->>init.lua: kong.db = DB.new(config)
    init.lua->>init.lua: kong.dns = dns_client

kong/globalpatches.lua のランタイムパッチは、最初の require 時に一度だけ適用されます。特に注目すべきパッチが 3 つあります:

  1. init フェーズでのブロッキングスリープ(51〜83 行目):ngx.sleep は設計上ノンブロッキングですが、init_worker 中はコルーチンベースのスリープが使えません。Kong はこれを LuaSocket のブロッキング socket.sleep() に置き換えています。ただし、ngx.get_phase()init または init_worker を返す場合にのみ適用されます。

  2. cJSON の精度cjson_safe.encode_number_precision(16) により、浮動小数点数が JSON のラウンドトリップで精度を失わないようにしています。

  3. Protobuf のデフォルト値pb.option("decode_default_array") により、protobuf メッセージ内の空の配列が JSON エンコード時に nil ではなく [] としてデコードされます。

ヒント: globalpatches ファイルでは _G._KONG も設定されます。これは _NAME_VERSION だけを持つ最小限のテーブルです。_G._KONG(グローバルメタデータマーカー)と kong PDK オブジェクトを混同しないようにしましょう。

リクエストライフサイクルの概要

アーキテクチャの全体像が見えたところで、1 つの HTTP リクエストの流れを大まかに追ってみましょう。クライアントが Kong のプロキシポートに GET /api/users を送信する場合を考えます:

  1. rewrite フェーズKong.rewrite()):リクエストコンテキスト(ngx.ctx)のセットアップ、タイミングの記録、ワークスペースの検出、グローバルスコープのプラグインの実行を行います。

  2. access フェーズKong.access()):runloop の access.before() がルーターを実行し、リクエストを Route と Service にマッチさせます。プラグインは優先度順に実行され、認証プラグインがクレデンシャルを検証し、レートリミッターがクォータを確認し、トランスフォーマーがリクエストを変換します。upstream の接続情報を使って balancer の準備も行います。

  3. balancer フェーズKong.balancer()):具体的な upstream ターゲット(IP:port)を選択し、DNS 解決、タイムアウトの設定、コネクションプーリングの管理を行います。リトライ時にはこのフェーズが再実行されます。

  4. Upstream:Nginx が選択した upstream ターゲットにリクエストを転送します。

  5. header filter フェーズKong.header_filter()):upstream からのレスポンスヘッダーを処理します。プラグインはヘッダーの追加、削除、変更が可能です。

  6. body filter フェーズKong.body_filter()):レスポンスボディをストリーミングチャンクで処理します。チャンク形式のレスポンスでは複数回呼び出される場合があります。

  7. log フェーズKong.log()):すべてのタイミングデータが確定します。ロギングプラグイン(http-log、file-log、datadog)がリクエストレコードをシリアライズして送信します。コンテキストが解放されます。

sequenceDiagram
    participant Client
    participant Rewrite as Kong.rewrite()
    participant Access as Kong.access()
    participant Balancer as Kong.balancer()
    participant Upstream
    participant HdrFilter as Kong.header_filter()
    participant BodyFilter as Kong.body_filter()
    participant Log as Kong.log()

    Client->>Rewrite: GET /api/users
    Rewrite->>Access: workspace set
    Access->>Access: Router match → Route + Service
    Access->>Access: Plugins execute (auth, rate-limit, ...)
    Access->>Balancer: balancer_data prepared
    Balancer->>Upstream: IP:port selected
    Upstream-->>HdrFilter: 200 OK + headers
    HdrFilter-->>BodyFilter: modified headers
    BodyFilter-->>Log: body chunks streamed
    Log-->>Client: response delivered

各フェーズハンドラは kong/init.lua で確認できるパターンに従っています。タイミングマーカーのセットアップ、runloop.<phase>.before() の呼び出し、プラグインのイテレーション、runloop.<phase>.after() の呼び出し、タイミングの記録という流れです。この「サンドイッチパターン」が Kong の拡張性の骨格を成しています。runloop がインフラを提供し、プラグインが振る舞いを提供する構造です。

次回の内容

この記事では基盤となる知識を整理しました。Kong は Nginx ワーカープロセス内で動作する Lua コードであり、リクエスト処理のあらゆるフェーズにフックしています。第 2 回では、kong start と入力した瞬間からブートシーケンスを完全に追います。CLI のディスパッチ、設定の読み込み、Nginx テンプレートのレンダリング、そしてトラフィックを処理するための準備を整える init/init_worker フェーズまでを解説します。