Envoy Proxyによるgateway構築のアーキテクチャパターン
Envoy Proxyを中核に据えてAPI Gatewayを構築する際のアーキテクチャパターンを整理する。EnvoyはIstioのデータプレーンとして知られるが、それ自体はLyft発の独立した汎用プロキシであり、Kubernetesやサービスメッシュなしで単体利用できる。この「素材としての自由度」がEnvoyの最大の特徴であり、同時に「どこまで自分で組むか」という設計判断を突きつけてくる。
本記事は次の3部で構成する。各パターンは**「どの課題を解くためのものか」**を明示しながら進める。
- 設定管理・制御プレーンのパターン — 誰がどうやってEnvoyに設定を配るか
- 認証・認可の実装パターン — どの層で何を判定するか
- 認可サービスを作らないOSSゲートウェイとの比較 — Kong / APISIX等との構造的な違い
前提:EnvoyがGatewayとして提供するもの
Envoyは単なるリバースプロキシではなく、Gatewayに必要な部品をフィルタチェーンとして標準装備している。リクエストは以下のようにフィルタを順に通過し、各フィルタが検証・変換・判定を行う。
図:Envoyのフィルタチェーン。リクエストは各フィルタを順に通過し、検証・判定・変換が行われる
主要な部品:
- jwt_authnフィルタ: JWKS取得・キャッシュ、署名・exp / aud / iss検証を組み込みで実施。検証済みクレームは後続フィルタへ引き渡せる
- RBACフィルタ: ルート×scopeの突き合わせなど、宣言的なアクセス制御
- ext_authzフィルタ: リクエストごとに外部の認可サービスへgRPC / HTTPで問い合わせる公式インターフェース
- Lua / WASMフィルタ: プロキシプロセス内でカスタムロジックをインライン実行
- mTLS、レートリミット、可観測性(アクセスログ、メトリクス、トレーシング)
つまり「トークンの標準検証は組み込み、属性を使うカスタム判定は拡張層」という役割分担が最初から用意されている。この構造を理解しておくと、後述する認可パターンの選択が「どのフィルタに判定を置くか」という問いに還元できる。
第1部:設定管理・制御プレーンのパターン
Envoy単体利用で最初にぶつかる課題は、プロキシ本体の性能でも機能でもなく設定管理である。
課題: Envoyの設定(ルート、クラスタ、フィルタ)をどうやって配布・更新するか。変更のたびに再デプロイするのか、動的に配るのか。動的に配るなら、その配布サーバーは誰が作るのか。
Envoyには設定の受け取り方が2系統ある。起動時にYAMLを読む静的設定と、xDS APIという標準プロトコル経由で外部サーバーから動的に受け取る方式だ。Istioのistiodがやっているのは後者である。
まず基本形として、xDSによる動的配信がどういう構造かを押さえておく。コントロールプレーン(Istioならistiod)が設定の真実を持ち、各Envoy——Gateway Podも、アプリPodのsidecarも——へxDSで配信する。Envoy自身は設定を持たない「空の器」であり、この分離があるからプロキシ再起動なしのポリシー変更が成立する。
図:xDSの基本形。コントロールプレーンが全Envoy(Gateway Pod・アプリPodのsidecar)へ設定を動的配信する
この基本形の上に、4つの実践パターンが乗る。
図:制御プレーンの4パターン。設定を「誰が」「どうやって」Envoyに届けるかが違い
パターンA:静的設定(YAML直書き)
課題: とにかくEnvoyをGatewayとして動かしたい。Kubernetesはない(ECS、EC2)。
解決: EnvoyのYAML設定をリポジトリ管理し、変更のたびにCI/CDでコンテナを入れ替える。ECSタスクとしてEnvoyを動かしNLB配下でL7ゲートウェイにする構成は普通に成立する。
- 向く場面: ルーティングやポリシーの変更頻度が低い(週次以下)、チームが少数
- 残る課題: 変更のたびにデプロイが走る。ルート追加を各チームにセルフサービスさせたくなった瞬間に破綻し始める。「設定変更 = インフラデプロイ」という結合が、組織が拡大するほど効いてくる
パターンB:xDS制御プレーン自作
課題: パターンAでは変更のたびにタスク入れ替えが必要。動的に設定を配りたいが、Kubernetesは使えない。
解決: istiodに相当する配布サーバーを自前で持ち、xDS経由でEnvoyに設定を流す。プロキシ再起動なしにルートやポリシーを変更できる。
- 現実: Kubernetes外での既製の選択肢は乏しい。go-control-planeでの自作かConsul程度で、自作は「小さなistiodを保守し続ける」覚悟が要る
- 残る課題: 制御プレーン自体の可用性・テスト・バージョン管理をすべて自分で背負う。動的変更が本当に頻繁なら、その前にALB / API Gateway / VPC Lattice / ECS Service ConnectといったAWSマネージド代替を先に検討すべきで、自作xDSは最終手段
パターンC:Envoy Gateway(Kubernetes上の公式OSS制御プレーン)
課題: 動的配信は欲しいが、xDSサーバーを自作したくない。かといってIstioのメッシュ全体(sidecar注入、東西mTLS、istiod運用)までは要らない。
解決: EKSなどKubernetes上なら、公式OSSのEnvoy Gatewayが中間解になる。Gateway API準拠の制御プレーンが付属し、HTTPRouteなどのCRDを書くだけでxDS配信される。xDS自作問題が消え、ext_authzやWASMの差し込み口も整備済み。
- 向く場面: 南北トラフィック(外部→内部)のGatewayだけが欲しい場合。Istioより明確に軽い
- 残る課題: 守備範囲は南北のみ。サービス間(東西)の通信は無防備のまま
パターンD:Istio Ingress Gateway(メッシュの一部として)
課題: Gatewayだけ守っても、内部でサービス間が無認証・平文なら「検証点を1つ持つ境界防御」にすぎない。NIST SP 800-207の言う「ネットワーク上の位置を信頼の根拠にしない」を満たしたい。
解決: Istioを導入し、南北(Ingress Gateway)と東西(sidecar / ambient)の両方をEnvoyで統一する。Ingress Gatewayはクラスター内で動くただのEnvoy Pod(Deployment)で、istiodからxDSで設定を受け取る。エッジで付与したユーザー情報ヘッダーを内部サービスが信用できるのは、東西がmTLS + サービスID認可で守られており「そのヘッダーは正規のGatewayからしか来ない」ことが保証されるからだ。メッシュなしで同じことをやるとヘッダー偽装し放題になる。
図:Istio構成の全体像。NLBはAWS管理、Ingress GatewayとアプリPodはクラスター内。Gateway以降のホップはすべてmTLS + サービスID認可で守られる
- 向く場面: L7の細かいサービス間認可、マルチテナントガバナンスが必要な組織
- 残る課題: istiodの運用、sidecarのリソースオーバーヘッド、Istioアップグレード時のPod再起動オペレーション。プラットフォームチームの体制(目安として最低6人規模)が前提になる
第1部まとめ:課題と解決の対応
| 課題 | 解決パターン |
|---|---|
| まず動かしたい・変更頻度低・非K8s | A: 静的設定 + CI/CD |
| 動的変更が頻繁・非K8s | まずマネージド代替(ALB / API Gateway)→ 最終手段でB: xDS自作 |
| K8s上で南北Gatewayだけ欲しい | C: Envoy Gateway |
| 東西を含むゼロトラスト全体 | D: Istio |
第2部:認証・認可の実装パターン
Gatewayの本丸は認可である。Envoyの設計思想はPEP / PDP分離——プロキシはPEP(Policy Enforcement Point、強制点)に徹し、PDP(Policy Decision Point、判定点)は状況に応じて内蔵・外付けを選ぶ——にある。これはXACML時代からある古典パターンをプロキシに持ち込んだものだ。
図:PEP / PDP分離の基本形。Envoyは強制点に徹し、判定は認可サービスへ、consentやトークンの真実は認可サーバーへ委ねる
まず全体の見取り図として、認可は3層に分かれることを押さえたい。
図:認可の3層構造。下の層ほど判定に必要なデータが「ビジネス寄り」になる
| 層 | 判断すること | 判定に必要なデータ |
|---|---|---|
| エッジ(Gateway) | エンドユーザーのJWT検証・「誰か」の確認 | トークンと公開鍵(JWKS) |
| メッシュ | どのサービスがどこと話せるか・mTLS | サービスID(証明書) |
| アプリケーション | このユーザーがこのリソースを触れるか | 所有関係・共有設定などのビジネスデータ |
エンドポイント単位の粗いガードレールはGateway / メッシュ、リソース個体単位のビジネス認可はアプリ層。この分担を崩して「Gatewayで全部やろう」とすると、判定に必要なビジネスデータがGatewayからは構造的に見えないため、ポリシーのメンテコストが爆発する。リソース単位の権限グラフが複数サービスにまたがる場合はZanzibar系(SpiceDB / OpenFGA)の検討領域だが、それはGatewayの外の話だ。
以下、エッジ層(Gateway)の実装パターンを課題起点で見ていく。判定に必要な情報の所在が使い分けの本質である。
図:認可実装パターンの選択フロー。分岐の軸は「判定に必要な情報の所在」
パターン1:組み込みフィルタで完結(JWT + RBAC)
課題: エンドユーザーのJWTを検証し、scopeとルートを突き合わせたい。余計な外部依存もレイテンシも増やしたくない。
解決: jwt_authnフィルタでJWKSローカル検証が完結する。JWKSは公開鍵なのでキャッシュでき、認可サーバーが登場するのは発行時とJWKS取得時のみ。scope×ルートの粗い突き合わせは組み込みRBACで宣言的に書ける。外部依存ゼロ、レイテンシ最小、障害モードの考慮も不要。
使い分けの原則: 設定(IstioならCRD)で表現できる判定はここに寄せるのがデフォルト。特に東西トラフィックはJWT前提でステートレス検証が完結するため、認可の9割はこの層で足りる。IstioのRequestAuthentication + AuthorizationPolicyはまさにこの標準形である。
パターン2:インライン拡張(Lua / WASM)
課題: FAPI固有のチェック——cnfクレームとmTLSクライアント証明書の突き合わせ等——はjwt_authnの守備範囲外。しかし判定自体はリクエストの中身だけで完結するのに、そのためだけに外部サービスを立てたくない。
解決: リクエスト + 事前配布できる設定だけで完結するステートレスな判定なら、Lua / WASMの小さなインラインフィルタで書ける。プロキシプロセス内で実行されるため外部依存は増えない。
- WASMはproxy-wasm規格でRust(本命)、C++、TinyGo等で開発できる。OPAのRegoをWASMにコンパイルする合わせ技もある
- 残る課題(運用上の重さ): デバッグ困難、バグるとプロキシごと道連れ、モジュールの配布・バージョン管理を自前で設計する必要がある。「書ける」と「運用し続けられる」の間には距離がある
パターン3:ext_authzで外部委譲
課題: 判定にリクエスト時点の外部状態が必要——introspection、consent状態、権限DB、失効リスト。これらはプロキシに事前配布できず、インラインでは原理的に判定できない。
解決: ext_authzフィルタでリクエストごとに外部の認可サービスへ問い合わせる。プロキシはPEPに徹し、判定はPDPに委ねる。
図:ext_authzの流れ。①〜④の順に進み、Deniedの場合は③で止まりクライアントへ403等が返る
設計上の要点:
- I/Fは1メソッド契約: gRPCなら
Check(CheckRequest) → CheckResponse。認可サービスの実装言語は自由で、OPAをext_authzサーバーにする構成も定番 - エンドポイント知識はポリシーへ寄せる: パスパターン×必要scope / consentの対応表はI/FではなくPDP側のポリシーデータとして持つ(OPAならRego)。I/Fを「事実を渡す→可否が返る」に保つと、APIが増えても壊れない
- 可搬性: IstioのAuthorizationPolicy CUSTOMアクションも内部的にext_authzを呼ぶため、ECS / EKS間やゲートウェイ差し替えでも認可サービスを使い回せる
- レイテンシ対策: 判定結果の短TTLキャッシュ + 認可サービスのsidecar / 同一ノード配置(localhost越しでミリ秒未満)
- 残る課題(障害モードの二択): 外部PDP停止時にfail-close(全deny、PDPがSPOF化)かfail-open(素通し、セキュリティ低下)かを明示的に選ばされる。インライン化(パターン1・2)はこの二択自体を消す——「fail-close相当の安全性を、可用性を犠牲にせず得られる」のがインラインの本質的な利点だ
実務では2段重ねが多い:インライン層で明らかにダメなもの(署名不正、exp切れ、scope不足)を安く弾き、通過分のみ外部判定へ送る。
パターン4:Phantom Token(エッジでのトークン交換)
課題: トークンがopaque(参照トークン)だと、検証のたびにintrospectionが必要になる。全リクエストが「Gateway → 認可サーバー」の直列依存を持ち、可用性は掛け算で劣化、認可サーバーがシステム全体のスループット上限になる。かといってJWTに全面移行すると、失効の即時性(同意取り消しが期限まで効かない)を失う。
解決: Phantom Tokenパターンで直列依存を「エッジの1回」に局所化する。エッジでopaqueをJWTに交換し、内部は全部JWTのローカル検証で自己完結させる。
図:Phantom Tokenの流れ。③④はトークンごとの初回とTTL切れ時のみ発生し、キャッシュヒット時は②→⑤で完結する
設計上の要点:
- 呼び出しは1回で済む: introspectionの応答自体をJWTにしてもらう(RFC 9701: JWT Response for OAuth Token Introspection)。検証と取得は同じ1往復の同じ出来事
- エッジでの署名検証は不要: mTLS / TLSの信頼チャネルで認可サーバーから直接受領しているため。署名検証が働くのは内部側(各サービスsidecarのjwt_authn)で、「エッジ + 内部再検証」構成と自然に噛み合う
- キャッシュ: opaqueトークン単位のTTLキャッシュで、実呼び出しは「トークンごと初回 + TTL切れ時」に圧縮される
- もう1つの系統として**Token Exchange(RFC 8693)**がある。こちらは「新しいトークンの発行」であり、audを宛先サービスに絞るなど積極的な変換に向く。単なるopaque→JWT変換ならRFC 9701が最短
- 残る課題: 認可サーバーのRFC 9701 / RFC 8693対応は製品差がある。未対応だと自前でJWTを組み立てる = 鍵管理を抱えて一気に重くなるため、認可サーバーの選定段階で要件に入れること
トークン戦略の判断軸:失効SLA
introspection(opaque)とJWTの選択は、突き詰めると失効SLA——同意取り消しが効くまでの許容遅延——で決まる。introspectionの直列依存は、リアルタイム失効確認の代償だったのだ。
| 失効SLA | 戦略 |
|---|---|
| 数分の遅延を許容できる | 短命JWT(5〜15分)。失効遅延をトークン寿命に押し込む。FAPI文脈でも広く受容されている |
| 数分許容 + 準リアルタイムで縮めたい | 短命JWT + 失効イベントの非同期配布(denylistプッシュ、Shared Signals / CAEP) |
| 即時失効が必須 | introspection維持 + アダプタのsidecar配置 + 短TTLキャッシュ + 認可サーバー冗長化。またはPhantom Tokenで直列依存をエッジに局所化 |
第2部まとめ:課題と解決の対応
| 課題 | 解決 |
|---|---|
| JWT検証とscope制御を低レイテンシで | パターン1: jwt_authn + RBAC(組み込み) |
| 組み込みで書けないがステートレスな判定 | パターン2: Lua / WASMインライン |
| 外部状態(consent・権限DB)が必要な判定 | パターン3: ext_authz + 独立PDP(sidecar配置 + 短TTLキャッシュ + fail動作の明示的選択) |
| opaqueトークンの直列依存で可用性が劣化 | パターン4: Phantom Token(RFC 9701)でエッジに局所化 |
| 失効の即時性とローカル検証の両立 | 失効SLAで決める(短命JWT / イベント配布 / introspection維持) |
| リソース単位のビジネス認可 | Gatewayでやらない。アプリ層(必要ならZanzibar系)へ |
第3部:認可サービスを作らないOSSゲートウェイとの比較
ここまでのEnvoy構成は、パターン3・4で「認可サービス(アダプタ)を書く」ことを前提にしていた。実体はintrospection問い合わせ + 証明書バインド検証 + ヘッダー伝播程度の数百行規模のグルーコードとはいえ、独立コンポーネントを1つ増やすことに変わりはない。
一方、Kong、APISIX、Tyk、KrakenDといったOSSゲートウェイは、認証・認可をプラグインとして同梱しており、カスタムサービスを書かずに設定だけで済ませられる。JWT検証、OIDC連携、API key、ACL、レートリミットは設定を書けば即動く。
課題: カスタム認可サービスを書いて運用するコストと、同梱プラグインの制約に縛られるコスト、どちらを取るか。
構造の違い:PEP / PDP分離 vs 同居
この選択は思想の違いでもある。
図:PEP / PDPの3つの配置。分離・同居・そして両者の利点を取る「論理分離・物理同居」
- Envoy + ext_authz: PEPとPDPを分離する。認可ロジックはゲートウェイの外の独立したサービス
- Kong / APISIX等: PEPとPDPが同居する。認可ロジックはゲートウェイプロセス内のプラグイン(Lua等)として実行される
同居型は歴史的にはむしろ主流で、ホップ削減とシンプルさが利点だ。だが同居には3つの結合が付いてくる:
- 変更ライフサイクルの結合 — 認可ロジックの修正 = ゲートウェイの再デプロイ。認可ポリシーの変更頻度が高いほど痛い
- 障害・リソースの結合 — 認可プラグインのバグ / 負荷がデータプレーン全体を道連れにする。認可の障害分離ができない
- チーム責務の結合 — 認可を書くチーム(セキュリティ / 認証基盤)のコードが、プラットフォームチームの心臓部に入る。デプロイ権限とレビュー責任が絡まる
なおIstioの場合、この結合はCRDによって緩和されている。ポリシーは宣言的設定としてistiodがxDSで動的配布するためプロキシ再デプロイ不要(結合1の解消)、CRDはnamespace単位でKubernetes RBACにより所有権を分離できる(結合3の解消)。「設定で表現できる判定はインライン、書けないものだけ外へ」という第2部の原則は、この緩和があるから成立する。
比較表
| 観点 | Envoy + カスタム認可(ext_authz) | OSSゲートウェイ同梱プラグイン(Kong / APISIX等) |
|---|---|---|
| 初期コスト | 高い。アダプタ実装(数百行〜)+ 制御プレーン選定が必要 | 低い。JWT検証、OIDC、key-auth、ACLは設定のみで即動く |
| カスタム認可の自由度 | 高い。言語自由、外部状態参照も設計に組み込める | プラグインSDKの制約内(Kong: Lua / Go / WASM、APISIX: Lua / WASM)。凝った要件は結局カスタムプラグイン開発になり、初期コストの差は縮む |
| 可搬性 | ext_authzで書いた認可サービスはゲートウェイ非依存。Istio・Envoy Gatewayと同一契約が通り、ECS / EKS間や将来のゲートウェイ差し替えでも使い回せる | プラグインはそのゲートウェイ専用。乗り換え時に書き直し |
| 障害分離 | 認可サービスの障害時にfail-open / fail-closeを選べる。データプレーンは生き残る | プラグイン障害はゲートウェイと運命共同体 |
| 変更ライフサイクル | 認可サービスは独立デプロイ。ゲートウェイに触らず認可を更新できる | プラグイン更新 = ゲートウェイ再デプロイ(動的リロード対応の範囲はプラグイン種別による) |
| 運用対象 | Envoy + 制御プレーン + 認可サービスの3点。多い | ゲートウェイ + 管理DB(KongならPostgreSQL、APISIXならetcd)。少ない |
| 管理UI・開発者ポータル | なし。必要なら別途 | 同梱 or 商用版で提供されることが多い |
| メッシュとの統合 | Istio / Envoy Gatewayと同一データプレーン技術で、南北と東西を一貫した仕組みにできる | ゲートウェイは南北専用。東西は別技術になる |
どちらを選ぶか
OSSゲートウェイ同梱型が向く場合:
- 認可要件が標準パターン(JWT検証、scope制御、API key、IP制限、レートリミット)の組み合わせで表現できる
- 認可のための独自データ(consent、契約ティア、独自権限モデル)を参照する必要がない
- 小さいチームで、認可サービスという追加コンポーネントの運用を抱えたくない
- 管理UIやAPIカタログなど周辺機能に価値を感じる
Envoy + カスタム認可が向く場合:
- FAPI対応や独自のconsent検証など、リクエスト時点の外部状態を参照する判定が必要
- 同じ認可ロジックを複数の場所(エッジGateway、メッシュ、将来の別ゲートウェイ)で使い回したい
- 認可ロジックの変更をゲートウェイのデプロイから独立させたい(変更頻度が高い、所有チームが別)
- すでにIstio / Envoy Gatewayを使っており、データプレーン技術を統一したい
中間解:論理分離・物理同居。同居型のホップ削減と分離型の独立性を両取りする構成として、PDP(OPA等)をゲートウェイのsidecarとして同一Pod / タスクに置く方法がある。ホップコストはlocalhost越しでほぼゼロ、変更ライフサイクルと障害の分離は維持できる。認可ロジックをゲートウェイプロセスに内包する前に、まずこれを検討すると後悔が少ない。OPAの場合、ポリシー配布はBundle APIでGitOps管理でき、「実行基盤はプラットフォームチーム、ポリシーの中身はセキュリティ / サービスオーナー」という所有権分離も成立する。
なお公平のために言えば、KongやAPISIXにもext_authz相当の外部委譲プラグイン(forward-auth系)はあるため、「外部PDPに委譲できるのはEnvoyだけ」ではない。ただしEnvoyのext_authzはIstio・Envoy Gatewayとエコシステム全体で同一契約が通る点で、可搬性の実利が一段大きい。
全体まとめ:課題→解決の一覧と判断フロー
本記事で扱った課題と解決を一枚にまとめる。
| # | 課題 | 解決 |
|---|---|---|
| 1 | 設定をどう配るか(非K8s・低頻度) | 静的YAML + CI/CD |
| 2 | 設定をどう配るか(K8s・南北のみ) | Envoy Gateway(Gateway API) |
| 3 | 設定をどう配るか(東西込み) | Istio(istiod + CRD) |
| 4 | 標準的なトークン検証 | jwt_authn + RBAC(組み込み・外部依存ゼロ) |
| 5 | ステートレスだが組み込みで書けない判定 | Lua / WASMインライン |
| 6 | 外部状態が必要な判定 | ext_authz + 独立PDP(sidecar配置 + キャッシュ + fail動作の明示) |
| 7 | opaqueトークンの直列依存 | Phantom Token(RFC 9701)でエッジに局所化 |
| 8 | 失効の即時性 | 失効SLAで戦略選択(短命JWT / イベント配布 / introspection) |
| 9 | エッジ通過後のヘッダー偽装 | メッシュのmTLS + サービスID認可(エッジ単体では防げない) |
| 10 | ビジネス認可のデータがGatewayから見えない | アプリ層に残す。グラフが複数サービスにまたがるならZanzibar系 |
| 11 | 認可サービスを書く / 書かないの分岐 | 標準パターンで足りるなら同梱プラグイン型、外部状態・可搬性が要るならext_authz、迷ったら論理分離・物理同居 |
最後に選定の全体フローを図にする。
図:選定の全体フロー。上段でゲートウェイの構成を決め、下段で認可の実装方式を決める
Envoyでのgateway構築は「完成品を買う」のではなく「強制点という部品を組む」行為だ。その分、認可アーキテクチャの設計判断——どの層で何を判定するか、判定に必要なデータはどこにあるか、障害時にどちらへ倒すか——から逃げられない。逆に言えば、その判断を明示的に設計したいシステム(金融、マルチテナント、規制産業)にとっては、この自由度こそが選定理由になる。標準パターンで足りるうちは同梱プラグイン型で軽く始め、外部状態を参照する判定や可搬性の要求が現れた時点でext_authzへ移行する——その移行パスが存在すること自体が、PEP / PDP分離という設計の価値である。
