プラグインシステム:解決・実行・イテレーターの設計思想
前提知識
- ›第1〜3回(アーキテクチャ、起動処理、リクエストパイプライン)
- ›LuaのメタテーブルとクロージャーへのⅠ理解
- ›APIゲートウェイのプラグイン概念(認証、レート制限など)に対する基礎知識
プラグインシステム:解決・実行・イテレーターの設計思想
Kongというプロジェクトが存在する根本的な理由は、プラグインシステムにあります。プラグインのないAPIゲートウェイはただのリバースプロキシに過ぎません。プラグインシステムがあってこそ、Kongは認証・レート制限・ロギング・リクエスト変換・AIプロキシングといった機能を、独立して設定可能なモジュールとして組み合わせられる「プログラマブルなプラットフォーム」へと変貌します。
この記事では、プラグインがどのように発見・ロードされるか、イテレーターが特定のリクエストにどのプラグインを適用するかをどう判断するか、そして8段階の優先度解決システムが正しい設定をどう選択するかを見ていきます。具体例としてkey-authとrate-limitingプラグインを使用します。
プラグインのディスカバリーとロード
プラグインのディスカバリーは、kong/constants.lua内のBUNDLED_PLUGINSマップから始まります。ここには45個のプラグイン名が列挙されています。Kong.init()の実行時、このリストがロード処理の起点となります。
assert(db.plugins:load_plugin_schemas(config.loaded_plugins))
スキーマのロードはkong/db/schema/plugin_loader.luaで行われます。各プラグインに対してload_subschemaがkong.plugins.<name>.schemaをrequireし、MetaSubSchemaに対してバリデーションを行い、pluginsエンティティのサブスキーマとして登録します。
function plugin_loader.load_subschema(parent_schema, plugin, errors)
local plugin_schema = "kong.plugins." .. plugin .. ".schema"
local ok, schema = load_module_if_exists(plugin_schema)
-- validate against MetaSubSchema
ok, err_t = MetaSchema.MetaSubSchema:validate(schema)
-- register as subschema
ok, err = Entity.new_subschema(parent_schema, plugin, schema)
return schema
end
つまり、各プラグインの設定スキーマはデータベース上のpluginsエンティティのサブスキーマとして扱われます。POST /pluginsに{ "name": "key-auth", "config": { ... } }を送ると、configフィールドはkey-auth固有のスキーマに対してバリデーションされますが、保存先は共通のpluginsテーブルです。
flowchart TD
A[constants.lua: BUNDLED_PLUGINS] --> B[db.plugins:load_plugin_schemas]
B --> C{For each plugin}
C --> D["require kong.plugins.<name>.schema"]
D --> E[MetaSubSchema:validate]
E --> F[Entity.new_subschema]
F --> G[Plugin config validated<br>against its own schema]
C -->|Next plugin| C
collecting/collectedイテレーターパターン
kong/runloop/plugins_iterator.luaのプラグインイテレーターは、Kongのパフォーマンスの要となる2モードの実行モデルを実装しています。
collectingフェーズ(HTTPの場合はaccess、streamの場合はpreread)では、現在のリクエストに適用するプラグインを解決し、実行リストを構築します。collectedフェーズ(header_filter、body_filter、log、response)では、そのリストを再解決なしにそのまま再生します。
フェーズの分類は29〜65行目で定義されています。
NON_COLLECTING_PHASES = {
"certificate", "rewrite", "response",
"header_filter", "body_filter", "log",
}
COLLECTING_PHASE = "access"
この設計には重要な理由があります。accessフェーズでは、マッチしたRoute・Service・認証済みConsumerという情報がすでに揃っています。これがプラグイン設定の解決に必要な情報です。一方、後続のフェーズ(header_filter、body_filterなど)では再解決は不要であり、無駄なコストになります。accessで実行したプラグインをそのまま後続フェーズで使い回せばよいのです。
372〜411行目のcollectingイテレーターが実際の処理を担います。各プラグインに対してload_configuration_through_combosを呼び出して適用可能な設定を見つけ、そのプラグインが実装している各ダウンストリームフェーズに対してプラグインと設定のペアを記録します。
for j = 1, DOWNSTREAM_PHASES_COUNT do
local phase = DOWNSTREAM_PHASES[j]
if handler[phase] then
local n = collected[phase][0] + 2
collected[phase][0] = n
collected[phase][n] = cfg
collected[phase][n - 1] = plugin
end
end
収集されたデータはctx.pluginsに格納されます。これはプールから確保されるテーブルで、各ダウンストリームフェーズごとにサブテーブルを持ちます。各サブテーブルは[plugin, config, plugin, config, ...]というフラットな配列で、要素数はインデックス[0]に格納されます。
sequenceDiagram
participant Access as access phase (collecting)
participant Iterator as plugins_iterator
participant HeaderFilter as header_filter (collected)
participant BodyFilter as body_filter (collected)
participant Log as log (collected)
Access->>Iterator: get_collecting_iterator(ctx)
Iterator->>Iterator: For each loaded plugin...
Iterator->>Iterator: load_configuration_through_combos()
Iterator->>Iterator: If config found, record in ctx.plugins
Note over Iterator: ctx.plugins.header_filter = [plugin1, cfg1, plugin2, cfg2]
Note over Iterator: ctx.plugins.log = [plugin1, cfg1, plugin2, cfg2]
Iterator-->>Access: yield (plugin, config) for access handler
HeaderFilter->>Iterator: get_collected_iterator("header_filter", ctx)
Iterator-->>HeaderFilter: replay ctx.plugins.header_filter
BodyFilter->>Iterator: get_collected_iterator("body_filter", ctx)
Iterator-->>BodyFilter: replay ctx.plugins.body_filter
Log->>Iterator: get_collected_iterator("log", ctx)
Iterator-->>Log: replay ctx.plugins.log
補足:
rewriteがcollectingイテレーターではなくグローバルイテレーターを使う理由が気になるかもしれません。rewriteフェーズはルーティング処理の前に実行されるため、Routeがまだマッチしていません。Route・Service・Consumerに紐づいた設定を解決する術がないため、グローバルプラグイン(Route・Service・Consumerのいずれにも紐付けられていないプラグイン)だけがrewriteで実行されます。
8段階の設定解決
プラグインをリクエストに適用する際、複数のプラグインインスタンスが存在する可能性があるため、その設定を解決する必要があります。rate-limitingプラグインがグローバル、特定のService、さらに特定のRoute+Consumerの組み合わせに設定されている場合、より具体的な設定が優先されます。
215〜267行目のlookup_cfg関数が、この8段階の優先度ルックアップを実装しています。
| 優先度 | 組み合わせ | 具体性 |
|---|---|---|
| 1 | Route + Service + Consumer | 最も具体的 |
| 2 | Route + Consumer | |
| 3 | Service + Consumer | |
| 4 | Route + Service | |
| 5 | Consumer のみ | |
| 6 | Route のみ | |
| 7 | Service のみ | |
| 8 | グローバル(紐付けなし) | 最も汎用的 |
各組み合わせは85行目のbuild_compound_keyによって複合キーを生成します。
local function build_compound_key(route_id, service_id, consumer_id)
return format("%s:%s:%s", route_id or "", service_id or "", consumer_id or "")
end
ルックアップは最初にマッチした時点で短絡評価されます。Route+Service+Consumerの設定が存在すれば、Route+Consumerは確認されません。グローバル設定はフォールバックとして機能し、より具体的な設定が見つからない場合にのみ適用されます。
282〜291行目のload_configuration_through_combos関数はさらに一段階の仕組みを加えています。プラグインハンドラーはno_route・no_service・no_consumerフラグを宣言することで、ルックアップ前にその次元をフィルタリングできます。これはまれなケースですが、特定のスコープレベルをオプトアウトしたい特殊なプラグインのために用意されています。
flowchart TD
A[Request Context] --> B{Route + Service + Consumer?}
B -->|Found| Z[Use this config]
B -->|Not found| C{Route + Consumer?}
C -->|Found| Z
C -->|Not found| D{Service + Consumer?}
D -->|Found| Z
D -->|Not found| E{Route + Service?}
E -->|Found| Z
E -->|Not found| F{Consumer only?}
F -->|Found| Z
F -->|Not found| G{Route only?}
G -->|Found| Z
G -->|Not found| H{Service only?}
H -->|Found| Z
H -->|Not found| I{Global?}
I -->|Found| Z
I -->|Not found| J[Plugin does not apply]
プラグインハンドラーの構造と優先度順序
Kongのプラグインはすべて同じ構造に従っています。key-authという名前のプラグインはkong/plugins/key-auth/ディレクトリに配置され、以下のファイルで構成されます。
handler.lua— フェーズメソッド(access・header_filter・logなど)schema.lua— 設定のバリデーションスキーマdaos.lua(任意) — カスタムデータベースエンティティapi.lua(任意) — Admin API拡張
ハンドラーはPRIORITYとVERSIONフィールド、そしてNginxのフェーズ名をメソッド名として持つテーブルをエクスポートする必要があります。kong/plugins/key-auth/handler.luaの例を見てみましょう。
local KeyAuthHandler = {
VERSION = kong_meta.version,
PRIORITY = 1250,
}
PRIORITYは実行順序を制御し、値が大きいプラグインほど先に実行されます。これは非常に重要な設計です。認証プラグイン(key-authは1250、jwtは1250、basic-authは1100)が認可プラグイン(aclは950)より先に実行され、さらにレート制限プラグイン(rate-limitingは910)より先に実行される順序が保証されます。
key-authプラグインは262〜273行目に示すように、accessフェーズのみを実装しています。
function KeyAuthHandler:access(conf)
if not conf.run_on_preflight and kong.request.get_method() == "OPTIONS" then
return
end
if conf.anonymous then
return logical_OR_authentication(conf)
else
return logical_AND_authentication(conf)
end
end
一方、kong/plugins/rate-limiting/handler.luaはaccessとlogの両フェーズを実装しています。
RateLimitingHandler.VERSION = kong_meta.version
RateLimitingHandler.PRIORITY = 910
rate-limitingプラグインはaccessフェーズでレート制限のチェックと適用を行い、logフェーズでカウンターのインクリメントを非同期に同期します(clusterポリシーでsync rateを使用する場合)。このマルチフェーズパターンはよく見られる手法で、プラグインは早い段階で制御を行い、後処理は遅いフェーズに委ねます。
PDK:Plugin Development Kit APIサーフェス
プラグインはPDK(Plugin Development Kit)、すなわちkong.*名前空間を通じてのみKongと対話します。このAPIサーフェスはkong/pdk/init.luaで定義されています。
local MAJOR_MODULES = {
"table", "node", "log", "ctx", "ip", "client",
"service", "request", "service.request", "service.response",
"response", "router", "nginx", "cluster", "vault",
"tracing", "plugin", "telemetry",
}
各モジュールはkong/pdk/<name>.luaからロードされ、kongグローバルに追加されます。主要な名前空間を以下に示します。
| 名前空間 | 用途 | 例 |
|---|---|---|
kong.request |
受信リクエストの読み取り | kong.request.get_header("Authorization") |
kong.response |
クライアントへのレスポンス送信 | kong.response.exit(403, { message = "Forbidden" }) |
kong.service.request |
アップストリームへのリクエスト変更 | kong.service.request.set_header("X-Custom", "value") |
kong.service.response |
アップストリームのレスポンス読み取り | kong.service.response.get_header("Content-Type") |
kong.client |
Consumer・クレデンシャル情報 | kong.client.authenticate(consumer, credential) |
kong.log |
構造化ロギング | kong.log.err("something failed") |
kong.ctx |
プラグインスコープのコンテキスト | kong.ctx.plugin.my_data = "value" |
kong.cache |
データベースキャッシュ | kong.cache:get(key, opts, loader_fn, ...) |
PDKにはフェーズチェックの仕組みも備わっています。例えばkong.service.request.set_header()をlogフェーズで呼び出すとエラーになります。リクエストはすでに送信済みだからです。このチェックにより、プラグイン開発時の見つけにくいバグを未然に防ぐことができます。
補足: プラグインを書く際、1リクエスト内でフェーズをまたいで保持したいがプラグインインスタンスにスコープされるデータには
kong.ctx.pluginを使いましょう。プラグイン間で共有するデータにはkong.ctx.sharedを使います。モジュールレベルの変数をリクエストごとの状態管理に使ってはいけません。Nginxワーカーは複数のリクエストを並行して処理するためです。
全体像
1つのリクエストに対して、プラグイン実行パイプライン全体がどのように機能するかをまとめます。
flowchart TD
subgraph "init time"
A[Plugin schemas loaded] --> B[Plugin handlers loaded]
B --> C[Plugins sorted by PRIORITY]
end
subgraph "per-request: access phase"
D[Get collecting iterator] --> E[For each plugin in priority order]
E --> F[lookup_cfg: 8-level resolution]
F --> G{Config found?}
G -->|Yes| H[Record for downstream phases]
H --> I[Execute plugin.access]
G -->|No| J[Skip plugin]
I --> E
J --> E
end
subgraph "per-request: downstream phases"
K[Get collected iterator] --> L[Replay recorded plugin+config pairs]
L --> M[Execute plugin.header_filter]
M --> N[Execute plugin.body_filter]
N --> O[Execute plugin.log]
end
第5回では、プラグイン設定・ルート定義・サービスメタデータを格納するデータベース層を掘り下げます。バリデーション・シリアライゼーション・Admin API生成をすべて単一の情報源から担うスキーマシステムの仕組みを見ていきましょう。