ChibihamChibiham
Envoy Proxyによるgateway構築のアーキテクチャパターン
🛡️

Envoy Proxyによるgateway構築のアーキテクチャパターン

Envoy Proxyを中核に据えてAPI Gatewayを構築する際のアーキテクチャパターンを整理する。EnvoyはIstioのデータプレーンとして知られるが、それ自体はLyft発の独立した汎用プロキシであり、Kubernetesやサービスメッシュなしで単体利用できる。この「素材としての自由度」がEnvoyの最大の特徴であり、同時に「どこまで自分で組むか」という設計判断を突きつけてくる。

本記事は次の3部で構成する。各パターンは**「どの課題を解くためのものか」**を明示しながら進める。

  1. 設定管理・制御プレーンのパターン — 誰がどうやってEnvoyに設定を配るか
  2. 認証・認可の実装パターン — どの層で何を判定するか
  3. 認可サービスを作らないOSSゲートウェイとの比較 — Kong / APISIX等との構造的な違い

前提:EnvoyがGatewayとして提供するもの

Envoyは単なるリバースプロキシではなく、Gatewayに必要な部品をフィルタチェーンとして標準装備している。リクエストは以下のようにフィルタを順に通過し、各フィルタが検証・変換・判定を行う。

Envoyのフィルタチェーン構成図 図: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による設定配信の基本形 図:xDSの基本形。コントロールプレーンが全Envoy(Gateway Pod・アプリPodのsidecar)へ設定を動的配信する

この基本形の上に、4つの実践パターンが乗る。

制御プレーンの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構成の全体像 図:Istio構成の全体像。NLBはAWS管理、Ingress GatewayとアプリPodはクラスター内。Gateway以降のホップはすべてmTLS + サービスID認可で守られる

  • 向く場面: L7の細かいサービス間認可、マルチテナントガバナンスが必要な組織
  • 残る課題: istiodの運用、sidecarのリソースオーバーヘッド、Istioアップグレード時のPod再起動オペレーション。プラットフォームチームの体制(目安として最低6人規模)が前提になる

第1部まとめ:課題と解決の対応

課題解決パターン
まず動かしたい・変更頻度低・非K8sA: 静的設定 + 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分離の基本形 図:PEP / PDP分離の基本形。Envoyは強制点に徹し、判定は認可サービスへ、consentやトークンの真実は認可サーバーへ委ねる

まず全体の見取り図として、認可は3層に分かれることを押さえたい。

認可の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のリクエストフロー 図: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のフロー 図: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つの配置パターン 図:PEP / PDPの3つの配置。分離・同居・そして両者の利点を取る「論理分離・物理同居」

  • Envoy + ext_authz: PEPとPDPを分離する。認可ロジックはゲートウェイの外の独立したサービス
  • Kong / APISIX等: PEPとPDPが同居する。認可ロジックはゲートウェイプロセス内のプラグイン(Lua等)として実行される

同居型は歴史的にはむしろ主流で、ホップ削減とシンプルさが利点だ。だが同居には3つの結合が付いてくる:

  1. 変更ライフサイクルの結合 — 認可ロジックの修正 = ゲートウェイの再デプロイ。認可ポリシーの変更頻度が高いほど痛い
  2. 障害・リソースの結合 — 認可プラグインのバグ / 負荷がデータプレーン全体を道連れにする。認可の障害分離ができない
  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動作の明示)
7opaqueトークンの直列依存Phantom Token(RFC 9701)でエッジに局所化
8失効の即時性失効SLAで戦略選択(短命JWT / イベント配布 / introspection)
9エッジ通過後のヘッダー偽装メッシュのmTLS + サービスID認可(エッジ単体では防げない)
10ビジネス認可のデータがGatewayから見えないアプリ層に残す。グラフが複数サービスにまたがるならZanzibar系
11認可サービスを書く / 書かないの分岐標準パターンで足りるなら同梱プラグイン型、外部状態・可搬性が要るならext_authz、迷ったら論理分離・物理同居

最後に選定の全体フローを図にする。

選定の全体フローチャート 図:選定の全体フロー。上段でゲートウェイの構成を決め、下段で認可の実装方式を決める

Envoyでのgateway構築は「完成品を買う」のではなく「強制点という部品を組む」行為だ。その分、認可アーキテクチャの設計判断——どの層で何を判定するか、判定に必要なデータはどこにあるか、障害時にどちらへ倒すか——から逃げられない。逆に言えば、その判断を明示的に設計したいシステム(金融、マルチテナント、規制産業)にとっては、この自由度こそが選定理由になる。標準パターンで足りるうちは同梱プラグイン型で軽く始め、外部状態を参照する判定や可搬性の要求が現れた時点でext_authzへ移行する——その移行パスが存在すること自体が、PEP / PDP分離という設計の価値である。