Closed13

100日チャレンジ day6 (API Gateway)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/4c1fbbb9569fd9


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

GitHub: https://github.com/lirlia/100day_challenge_backend/tree/main/day6_api_gateway

riddle_tecriddle_tec

https://zenn.dev/yoshiko/articles/my-vibe-coding

のうち取り入れられそうなものを探す

  1. コードベース作成
    2. cli でやってるらしい
    3. 自分はもうテンプレートディレクトリをつくったのでこの辺は省略できてるからいいかな
  2. クライアントの作成と立ち上げ(APIはモック)
    2. いつもはバックエンドつくってからのクライアント作成にしているので、疎通できるレベルのものを作ってもらってる。一応今のところ特に困ってないのでいいかな
riddle_tecriddle_tec

最終的な仕様 (更新版)

1. API ゲートウェイ (/api/gateway/...)

  • 機能:
    • リクエストルーティング: config/routes.json に基づき、/api/gateway/* への受信リクエストを指定されたバックエンドサービス URL (ダミーAPI含む) に転送します。
    • 認証: リクエストヘッダーの Authorization: Bearer <API_KEY> を検証し、無効な場合はエラーを返します。API キーは config/apiKeys.json で管理します。
    • レート制限: API キーごとに設定された単位時間あたりのリクエスト数を制限します。制限超過時は 429 Too Many Requests を返します。レートリミットの状態と設定値はインメモリで管理します。
    • ロギング: 受信リクエストの情報と転送結果をコンソールに出力し、直近のログをインメモリにも保持します。
  • 実装: Next.js Route Handlers (app/api/gateway/[[...slug]]/route.ts) を使用します。(Note: パスを /api/gateway/ に変更し、他の API との衝突を避けます)

2. ダミーバックエンド API

  • 機能:
    • /api/time (GET): 現在時刻 (ISO 8601 形式、ミリ秒含む) を JSON ({ "currentTime": "..." }) で返します。
    • /api/random (GET): 16文字のランダムな英数字文字列を JSON ({ "randomString": "..." }) で返します。
  • 実装: それぞれ独立した Next.js Route Handlers (app/api/time/route.ts, app/api/random/route.ts) として実装します。これらは API Gateway の外側に直接配置されます。

3. 管理・テスト用 UI (/)

  • レイアウト: 単一ページで、画面を左右に分割します。
  • 左側 (リクエストテスト):
    • URL選択: config/routes.json に基づくルーティング可能な API Gateway 経由の URL プレフィックス (例: /api/gateway/time, /api/gateway/random) をプルダウンで選択できます。
    • パス入力: 選択したプレフィックスに続く具体的なパスを入力できます(ダミーAPIの場合は通常不要)。
    • API キー入力: 使用する API キーを入力できます。
    • リクエスト送信: 指定した URL と API キーで GET リクエストをゲートウェイ経由で送信します。
    • 結果表示: ゲートウェイからのレスポンス(ステータスコード、ヘッダー、ボディの概要)を表示します。
  • 右側 (管理):
    • ログ表示: API ゲートウェイがインメモリに保持している直近のログを一覧表示します。
    • レートリミット表示: config/apiKeys.json に基づく各 API キーの現在のレートリミット設定値を表示します。
    • レートリミット更新: API キーを選択し、新しいレートリミット値を入力して設定を更新するフォームを提供します。更新はゲートウェイの動作に即時反映されます。
  • 実装: Next.js Page Component (app/page.tsx) と React (Client Components) を使用し、管理用 API (/api/admin/...) とゲートウェイ API (/api/gateway/...) と通信します。

4. 管理用 API (/api/admin/...)

  • 機能:
    • /api/admin/logs (GET): インメモリに保持されているログを取得します。
    • /api/admin/rate-limit (GET): 現在の全 API キーのレートリミット設定を取得します。
    • /api/admin/rate-limit (POST): 特定の API キーのレートリミット設定を更新します。
  • 実装: Next.js Route Handlers を使用します。

実装計画 (更新版)

  1. プロジェクト初期化:
    • template ディレクトリをコピーして day6_api_gateway プロジェクトを作成します。
    • package.jsonnameday6_api_gateway に変更します。
    • README.md に更新された仕様概要(ダミー API 含む)を記述します。
  2. 設定ファイル定義・作成:
    • ルーティングルール (config/routes.json) の形式を決定し、ダミー API (/api/time, /api/random) へのルーティング設定 を含めてサンプルを作成します (例: /api/gateway/time -> http://localhost:3001/api/time)。
    • API キーと初期レートリミット (config/apiKeys.json) の形式を決定し、サンプルを作成します。
  3. ダミー API 実装:
    • app/api/time/route.ts を作成し、現在時刻を返すロジックを実装します。
    • app/api/random/route.ts を作成し、ランダム文字列を返すロジックを実装します。
  4. データ構造と状態管理の実装 (lib/store.ts 等):
    • インメモリでログ、API キー情報、レートリミットの状態と設定を管理するためのデータ構造と関数を実装します。
  5. API ゲートウェイエンドポイント実装 (app/api/gateway/[[...slug]]/route.ts):
    • リクエスト受信時の処理フロー(認証 → レート制限チェック → ルーティング → 転送 → ロギング)を実装します。
  6. 管理用 API エンドポイント実装:
    • ログ取得 (app/api/admin/logs/route.ts) を実装します。
    • レートリミット設定取得・更新 (app/api/admin/rate-limit/route.ts) を実装します。
  7. UI 画面実装 (app/page.tsx および関連コンポーネント):
    • Tailwind CSS で左右分割レイアウトを作成します。
    • 左側 (リクエストテスト): UI コンポーネントを作成し、API Gateway 経由でダミー API 等に リクエスト送信するロジックを実装します。URL 選択プルダウンには /api/gateway/time/api/gateway/random などが含まれます。
    • 右側 (管理): UI コンポーネントを作成し、管理用 API との連携ロジック(ログ表示、レートリミット表示・更新)を実装します。
  8. 動作確認とテスト:
    • UI からダミー API へのリクエストが正常に動作することを確認します。
    • UI からのログ表示、レートリミット変更が正しく機能することを確認します。
    • curl 等も用いて、認証エラー、レートリミット超過時の挙動を確認します。
riddle_tecriddle_tec

実装計画(作業順序)

  1. プロジェクト初期化:
    • template ディレクトリをコピーして day6_api_gateway プロジェクトを作成します。 (実施済みですが、コマンドエラーがあったため、再度実行します)
    • package.jsonnameday6_api_gateway に変更します。
    • README.md に更新された仕様概要(ダミー API 含む)を記述します。
  2. 設定ファイル定義・作成:
    • ルーティングルール (config/routes.json) の形式を決定し、ダミー API (/api/time, /api/random) へのルーティング設定を含めてサンプルを作成します (例: /api/gateway/time -> http://localhost:3001/api/time)。
    • API キーと初期レートリミット (config/apiKeys.json) の形式を決定し、サンプルを作成します。
  3. ダミー API 実装:
    • app/api/time/route.ts を作成し、現在時刻を返すロジックを実装します。
    • app/api/random/route.ts を作成し、ランダム文字列を返すロジックを実装します。
  4. データ構造と状態管理の実装 (lib/store.ts 等):
    • インメモリでログ、API キー情報、レートリミットの状態と設定を管理するためのデータ構造と関数を実装します。
  5. API ゲートウェイエンドポイント実装 (app/api/gateway/[[...slug]]/route.ts):
    • リクエスト受信時の処理フロー(認証 → レート制限チェック → ルーティング → 転送 → ロギング)を実装します。
  6. 管理用 API エンドポイント実装:
    • ログ取得 (app/api/admin/logs/route.ts) を実装します。
    • レートリミット設定取得・更新 (app/api/admin/rate-limit/route.ts) を実装します。
  7. UI 画面実装 (app/page.tsx および関連コンポーネント):
    • Tailwind CSS で左右分割レイアウトを作成します。
    • 左側 (リクエストテスト): UI コンポーネントを作成し、API Gateway 経由でダミー API 等にリクエスト送信するロジックを実装します。URL 選択プルダウンには /api/gateway/time/api/gateway/random などが含まれます。
    • 右側 (管理): UI コンポーネントを作成し、管理用 API との連携ロジック(ログ表示、レートリミット表示・更新)を実装します。
  8. 動作確認とテスト:
    • UI からダミー API へのリクエストが正常に動作することを確認します。
    • UI からのログ表示、レートリミット変更が正しく機能することを確認します。
    • curl 等も用いて、認証エラー、レートリミット超過時の挙動を確認します。
riddle_tecriddle_tec

本番で作るならもっと考えないといけないこと

では API gateway について本番で使う話をしましょう。

今作ったのをみる限り品質やスケーラビリティを考えると以下が課題かなと

  1. rate limit を外だしする(かつちゃんと計測する、ただ mutex とると遅くなるので best effort になるのかな?)
  2. routing を高速で行う(今回はローカルなので問題ないが、実際は別のところに飛ばすことになるのでなるべく kernel で処理しない方がはやいはず)
  3. 複雑なルーティングやユースケースへの対応(パスの変換、ヘッダの付け替え、プロトコル変換 など)
  4. 認証、認可(mTLSや、oidc / oauth など)周り

それぞれどのように実装するのか?を教えてもらえますか。

riddle_tecriddle_tec

本番環境での API Gateway 運用を考えると、ご指摘の点は非常に重要ですね。私たちが構築したシンプルなゲートウェイは教育目的としては良いですが、本番の要求には耐えられません。それぞれの課題について、一般的な実装アプローチや考え方を説明します。

1. レート制限の外部化と精度

  • 課題: インメモリ管理は単一障害点となり、スケールアウトもできません。また、厳密なカウントは分散環境では複雑です。
  • アプローチ:
    • 分散キャッシュ/データストアの利用: Redis や Memcached のような高速なインメモリデータストアを利用するのが一般的です。
      • キー: APIキーやユーザーID、IPアドレスなど。
      • 値: 一定期間内のリクエストカウント。
      • アトミック操作: Redis の INCR (カウントアップ) と EXPIRE (期間設定) を組み合わせることで、アトミックにカウントと有効期限を管理できます。これにより競合状態を最小限に抑えます。
      • アルゴリズム:
        • 固定ウィンドウ (Fixed Window): 実装は簡単ですが、ウィンドウの境界でリクエストが集中すると制限を超える可能性があります。 (例: 1分間に10回なら、0-59秒で10回許可)
        • スライディングウィンドウログ (Sliding Window Log): より正確ですが、タイムスタンプのリストを保持・集計する必要があり、ストレージと計算コストが高くなります。 (例: 過去60秒間のリクエスト数を常にカウント)
        • スライディングウィンドウカウンター (Sliding Window Counter): 固定ウィンドウとスライディングログのハイブリッド。精度とパフォーマンスのバランスが良いです。
        • トークンバケット (Token Bucket): 一定レートでトークンが補充され、リクエストごとにトークンを消費します。バーストを許容しやすいのが特徴です。Redis のデータ構造や Lua スクリプトで実装可能です。
    • 精度 vs パフォーマンス: 厳密な精度を求めると、共有リソース (Redisなど) へのアクセス競合やレイテンシがボトルネックになる可能性があります。多くの場合、わずかな誤差を許容する「ベストエフォート」な実装(例: アトミックカウンターを利用しつつ、ネットワーク遅延などによる若干のカウントずれは許容する)で十分なケースが多いです。
    • 専用ソリューション/サイドカー: Envoy などのサービスメッシュプロキシは組み込みのレート制限機能(ローカルおよびグローバル)を持っています。専用のレート制限サービスを構築・利用する選択肢もあります。

~

Lua スクリプトの利用: より高度な実装では、上記の「カウントアップ」「有効期限設定」「制限チェック」といった一連のロジックを Redis Lua スクリプトとして Redis サーバー側でアトミックに実行させることがよく行われます。これにより、ネットワーク往復回数を減らし、より効率的かつ堅牢なレートリミットを実現できます。

Redis で「1分間に10回」のような時間枠(TTL: Time To Live)を実現する方法はいくつかありますが、主に以下の考え方に基づいています。

方法1: INCREXPIRE の組み合わせ (基本的な考え方)

  1. キーの定義: APIキーごとにカウンター用のキーを用意します (例: rate_limit:test-key-1min)。
  2. カウントアップ (INCR): リクエストが来たら、そのキーに対して INCR コマンドを実行します。
  3. 最初のアクセスか判定: INCR の戻り値が 1 であれば、それはこの時間枠 (ここでは1分) での最初のアクセスです。
  4. TTL 設定 (EXPIRE): 戻り値が 1 だった場合のみ、そのキーに対して EXPIRE rate_limit:test-key-1min 60 のように、指定した時間(秒単位)の有効期限を設定します。
  5. 制限チェック: INCR の戻り値が設定した制限値 (例: 10) 以下であればリクエストを許可し、超えていれば拒否します。

この方法の課題と改善:

  • アトミック性: INCREXPIRE の間にわずかな時間差があり、その間に別のリクエストが来て INCR が実行されたり、サーバーがクラッシュしたりすると、EXPIRE が実行されずにキーが残り続けてしまう可能性があります。
  • 改善策 (Lua スクリプト): この問題を解決する最も一般的な方法は、Redis Lua スクリプト を使うことです。Lua スクリプトを使うと、「キーをインクリメントし、もし値が1ならTTLを設定し、現在の値を返す」という一連の操作を Redis サーバー上でアトミックに実行できます。これにより、競合状態やTTL設定漏れのリスクをなくせます。
-- Lua スクリプトの例 (イメージ)
local current_count = redis.call('INCR', KEYS[1])
if current_count == 1 then
    redis.call('EXPIRE', KEYS[1], ARGV[1]) -- ARGV[1] に TTL (秒) を渡す
end
return current_count

API Gateway はこの Lua スクリプトを EVAL または EVALSHA コマンドで実行し、返されたカウント値で制限をチェックします。

方法2: キー名に時間枠を含める (固定ウィンドウ)

  1. キーの定義: キー名に現在の時間枠(例: 分)を含めます。例えば、APIキーが test-key-1min で、現在が 22時55分なら、キーは rate_limit:test-key-1min:2255 のようになります。
  2. カウントアップ (INCR): リクエストが来たら、現在の時間枠に対応するキーに対して INCR を実行します。
  3. TTL 設定 (EXPIRE): 各時間枠のキーが初めて作られたとき(INCR の戻り値が 1 のとき)に、EXPIRE rate_limit:test-key-1min:2255 60 のように、次の時間枠が始まるまでの TTL (または少し余裕を持たせた時間) を設定します。
  4. 制限チェック: INCR の戻り値が制限値 (例: 10) 以下であれば許可します。

この方法の課題:

  • 固定ウィンドウ問題: ウィンドウの境界(例: 22:55:59 と 22:56:00)でリクエストが集中すると、1分間の制限を超えてしまう可能性があります(前の1秒で10回、次の1秒で10回リクエストが通るなど)。
  • キーの増加: 時間枠ごとにキーが作られるため、キーの数は増えますが、TTL を適切に設定すれば自動的に削除されます。

まとめ:

実際の本番環境では、アトミック性を保証し、より柔軟な制御を行うために Redis Lua スクリプトを使う方法が最も推奨されます。これにより、INCREXPIRE を安全かつ効率的に組み合わせることができます。

また、より厳密なレート制限が必要な場合は、スライディングウィンドウアルゴリズム(Redis の Sorted Set や Hash を利用)を検討することもありますが、実装はより複雑になります。

固定ウインドウとスライディングウインドウ

はい、その通りです! 鋭いご指摘ありがとうございます。 私の説明が不十分でした。

おっしゃる通り、単純な INCR と最初のアクセス時のみ EXPIRE を設定する方法では、真の「スライディングウィンドウ」は実現できません。これは 「固定ウィンドウ (Fixed Window)」 と呼ばれる方式になります。

ユーザー様のシナリオで固定ウィンドウの問題点:

  • 00:00:01: 1回目のリクエスト。キー rate_limit:test-key-1min が作成され、カウントが 1 になり、TTL が 60秒 (つまり 00:01:01 まで) に設定されます。
  • 00:00:59: 2回目〜10回目のリクエスト。キーは存在し、TTL もまだ有効なので、INCR が実行され、カウントが 10 になります。
  • 00:01:00: 11回目のリクエストを試みます。
    • キー rate_limit:test-key-1min はまだ存在します (00:01:01 まで有効)。
    • INCR を実行するとカウントは 11 になります。
    • 制限 (10回) を超えているため、このリクエストは拒否されます
  • 00:01:01: キー rate_limit:test-key-1min が Redis によって自動的に削除されます。
  • 00:01:01 以降: 12回目のリクエストを試みます。
    • キーが存在しないため、INCR が実行され、カウントが 1 になり、新しい TTL が 60秒設定されます。
    • リクエストは許可されます。

結論:

単純な INCR + EXPIRE では、ウィンドウの開始(最初のアクセス)から TTL が切れるまで、カウンターはリセットされません。00:00:01 のリクエストが 60秒経過した 00:01:01 になっても、キー自体が有効な限り、そのリクエスト分のカウントが自動的に減ることはありません。キーが丸ごと削除されて初めてカウントがリセットされるため、これは「固定ウィンドウ」の動作です。

真のスライディングウィンドウを実現するには?

Redis でより正確なスライディングウィンドウを実現するには、より複雑なデータ構造やロジックが必要になります。代表的な方法は以下の通りです。

  1. Sorted Set を利用する方法 (Sliding Window Log):

    • リクエストごとに、タイムスタンプをスコアとして Sorted Set にメンバーを追加します (ZADD)。メンバーIDはユニークなもの(例: タイムスタンプ + ランダム値)にします。
    • 新しいリクエストを処理する前に、現在の時刻からウィンドウサイズ(例: 60秒)より古いメンバーを Sorted Set から削除します (ZREMRANGEBYSCORE)。
    • 残っているメンバーの数を数え (ZCARD)、これが制限値以下であればリクエストを許可し、新しいタイムスタンプを追加します。
    • 利点: 非常に正確。
    • 欠点: リクエストごとに複数の操作が必要で、データ量も増えるため、パフォーマンスへの影響が大きい。通常 Lua スクリプトでアトミックに実行します。
  2. Hash や複数のキーを利用する方法 (Sliding Window Counter):

    • 現在のタイムスロットと前のタイムスロットのカウンターを保持します。
    • 現在のウィンドウ内でのカウントを、現在のスロットのカウントと、前のスロットのカウントのうち現在のウィンドウに含まれる割合を推定して計算します。
    • 利点: Sorted Set 方式よりはメモリ効率やパフォーマンスが良い。
    • 欠点: 実装が複雑で、完全な精度は得られない(近似値になる)。こちらも Lua スクリプトが使われることが多いです。

したがって、「1分間に10回」という要件に対して、最も簡単で広く使われるのは固定ウィンドウ(INCR + Lua でのアトミックな EXPIRE ですが、ウィンドウ境界でのバーストを許容できない、より厳密な制御が必要な場合は、スライディングウィンドウ(Sorted Set や Hash + Lua) の実装を検討する必要があります。

ご指摘のおかげで、より正確な説明ができました。ありがとうございます。

riddle_tecriddle_tec

2. 高速なルーティングと転送

  • 課題: Node.js (Next.js Route Handler) はアプリケーションロジックには適していますが、高スループット・低レイテンシが求められるネットワークプロキシ処理には最適ではありません。各リクエストをユーザー空間で処理し、カーネルのネットワークスタックを経由して転送するのはオーバーヘッドが大きくなる可能性があります。
  • アプローチ:
    • 専用プロキシソフトウェアの採用: Nginx, HAProxy, Envoy など、C/C++/Rust で書かれた高性能なリバースプロキシ/ロードバランサーを API Gateway のコアとして採用します。これらはネットワーク I/O を非常に効率的に処理するように設計されています。
    • 効率的なコネクション管理: バックエンドサービスへのコネクションプーリングや keep-alive を活用し、接続確立のオーバーヘッドを削減します。
    • イベント駆動アーキテクチャ: これらのプロキシは、大量の同時接続を少数のスレッド/プロセスで効率的に処理するためのイベント駆動モデル (epoll, kqueue など) を採用しています。
    • (高度) カーネルバイパス: 金融取引システムなど、極端な低レイテンシが要求される場合は、DPDK や XDP/eBPF といった技術を用いてカーネルのネットワークスタックをバイパスし、ユーザー空間で直接ネットワークパケットを処理することもありますが、これは通常、基盤となるプロキシやプラットフォームレベルでの対応となります。API Gateway の実装者が直接触れることは稀です。

はい、専用プロキシソフトウェア (Nginx, Envoy, HAProxy など) とカスタムアプリケーション (今回作成した Next.js アプリのような、認証・レート制限・カスタムロジックなどを担う部分) を連携させる方法はいくつかあります。目的や複雑さに応じて適切なパターンを選択します。

主な連携パターンは以下の通りです。

パターン1: プロキシが単純な転送役 (Proxy Pass / Upstream)

  • 流れ: クライアント → プロキシカスタムアプリ (ゲートウェイロジック) → 最終的なバックエンドサービス
  • 設定:
    • プロキシ (Nginx/Envoy/HAProxy) は、特定のパス (例: /api/) へのリクエストをすべてカスタムアプリ (例: http://localhost:3001/api/gateway/) に転送するように設定します (proxy_pass in Nginx, route to a cluster in Envoy)。
    • TLS 終端や基本的なヘッダーの追加/削除はプロキシが行うことが多いです。
  • 役割:
    • プロキシ: TLS終端、基本的なルーティング(カスタムアプリへの転送)、ロードバランシング(カスタムアプリが複数インスタンスの場合)、静的コンテンツ配信など。
    • カスタムアプリ: プロキシからリクエストを受け取り、認証、レート制限、複雑なルーティング決定、リクエスト/レスポンス変換などのロジックを実行します。その後、カスタムアプリ自身が fetch などを使って最終的なバックエンドサービスにリクエストを送信します。
  • 利点: 設定が比較的シンプル。カスタムアプリ側で複雑なロジックを集中管理しやすい。
  • 欠点: 最終的なバックエンドへのリクエストもカスタムアプリを経由するため、カスタムアプリがボトルネックになる可能性がある。プロキシの高性能な転送能力を最終ホップで活かしきれない。

パターン2: プロキシからの認証/認可チェック (Auth Request / External Authorization)

  • 流れ: クライアント → プロキシ → (認証チェックのため認証サービス (カスタムアプリの一部 or 別サービス) へサブリクエスト) → プロキシ → 最終的なバックエンドサービス
  • 設定:
    • プロキシはリクエストを受け取ると、まず認証/認可専用のエンドポイント (例: http://localhost:3001/api/auth_check) にサブリクエストを送信するよう設定します (Nginx の auth_request ディレクティブ、Envoy の ext_authz フィルターなど)。
    • このサブリクエストには、元のリクエストの Authorization ヘッダーなどが含まれます。
  • 役割:
    • 認証サービス: サブリクエストを受け取り、JWT 検証、API キー検証、レート制限チェック (Redis などへの問い合わせ) を行い、許可なら 200 OK、拒否なら 401/403 をプロキシに返します。許可する場合、ユーザーIDなどの情報をカスタムヘッダーとして返すこともできます。
    • プロキシ: 認証サービスからのレスポンスに基づき、
      • 200 OK ならば、元のリクエストを(認証サービスから返されたヘッダーを追加して)最終的なバックエンドサービスに転送します。
      • 401/403 ならば、そのエラーをクライアントに直接返します。バックエンドへの転送は行いません。
  • 利点: 認証/レート制限ロジックを分離できる。認証成功後のリクエストはプロキシが高性能にバックエンドへ転送できる。認証サービスは独立してスケール可能。
  • 欠点: プロキシの設定がやや複雑になる。認証チェック用のエンドポイントが必要。

パターン3: プロキシの拡張機能/プラグイン利用 (Lua, Wasm など)

  • 流れ: クライアント → プロキシ (内部でカスタムロジック実行) → 最終的なバックエンドサービス
  • 設定:
    • プロキシ自体にカスタムロジックを組み込みます。
      • Nginx/OpenResty: Lua スクリプトを使ってリクエスト処理の各段階 (アクセス時、ヘッダーフィルター時など) にフックし、Redis への接続、外部 HTTP 呼び出し、リクエスト/レスポンスの書き換えなどを実行できます。Kong API Gateway はこの仕組みを多用しています。
      • Envoy: WebAssembly (Wasm) フィルターを使って、様々な言語 (Rust, C++, Go, AssemblyScript など) で書かれたカスタムロジックをプロキシ内で安全に実行できます。
  • 役割:
    • プロキシ + カスタムコード: プロキシのプロセス内で、認証、レート制限、複雑なルーティング、変換などのロジックを実行します。
  • 利点: ネットワークホップが減り、潜在的に最も高性能。柔軟性が高い。
  • 欠点: 特定の言語 (Lua, Wasm対応言語) の知識が必要。プロキシ自体の設定・管理が複雑化する可能性がある。デバッグが難しくなることがある。

選択基準:

  • 単純な認証・レート制限 + 複雑な変換ロジック: パターン1 が手軽。
  • 認証・レート制限が主で、バックエンドへの転送性能を重視: パターン2 が適している。
  • 最大限のパフォーマンスと柔軟性が必要、プロキシ内部での開発に抵抗がない: パターン3 が強力な選択肢。

多くの場合、これらのパターンを組み合わせたり、Apigee, Kong, Tyk のような API Gateway 専用製品 を利用したりします。これらの製品は、内部的に高性能プロキシ (Nginx や Envoy) をベースにしつつ、パターン2や3のような機能を設定やプラグインで容易に利用できるようにしています。

riddle_tecriddle_tec

3. 複雑なルーティングやユースケースへの対応

  • 課題: 単純なプレフィックスベースのルーティングだけでなく、パスの書き換え、ヘッダーの追加/削除/変更、リクエスト/レスポンスボディの変換、gRPC と REST のようなプロトコル変換などが必要になることがあります。
  • アプローチ:
    • 高機能 API Gateway ソリューション: Kong, Tyk, Apigee (Google Cloud), AWS API Gateway といった OSS または商用の API Gateway 製品/サービスを利用します。これらは豊富な機能を設定ベースで提供します。
      • 宣言的なポリシー: ルーティングルール、パス書き換え、ヘッダー操作などを YAML や JSON などの設定ファイルで定義できます。
      • プラグイン/ミドルウェアアーキテクチャ: カスタムロジックを実装するためのプラグイン機構を提供します (例: Kong の Lua プラグイン、Envoy の Wasm フィルター)。
      • リクエスト/レスポンス変換: JSON から XML への変換、フィールドのマスキングなど、ボディの内容を変換する機能を提供します。
    • サービスメッシュの活用: Istio や Linkerd などのサービスメッシュは、トラフィックルーティング、ヘッダー操作、リトライ、サーキットブレーキングなどの機能をサービス間で透過的に提供でき、API Gateway と連携または機能を補完できます。

4. 認証・認可 (AuthN/AuthZ)

  • 課題: 単純な API キーだけでなく、より堅牢で標準化された認証・認可メカニズムが必要です。
  • アプローチ:
    • 認証 (Authentication - Who are you?):
      • OIDC/OAuth2: エンドユーザー認証には、Keycloak, Okta, Auth0 などの Identity Provider (IdP) と OpenID Connect (OIDC) や OAuth2 を連携させるのが標準的です。API Gateway は IdP が発行した JWT (JSON Web Token) を検証します (署名検証、有効期限、発行者、対象者などのクレームチェック)。
      • mTLS (Mutual TLS): サービス間通信の認証には、クライアント(他のサービス)も証明書を提示し、双方向で TLS 認証を行う mTLS が有効です。Gateway はクライアント証明書を検証します。
      • API キー: 外部のサードパーティアプリケーション向けには依然として API キーが使われますが、キーは安全なデータベースや Secrets Manager で管理し、Gateway はそれを検証します。
    • 認可 (Authorization - What can you do?):
      • JWT クレームベース: JWT に含まれるスコープ (scope) やロール (roles) などのクレームに基づいてアクセス制御を行います。
      • 外部化された認可サービス: Open Policy Agent (OPA) のような汎用ポリシーエンジンや、カスタムの認可サービスに問い合わせてアクセス可否を決定します。これにより、認可ロジックを Gateway から分離できます。
      • RBAC/ABAC: ロールベースアクセス制御 (RBAC) や属性ベースアクセス制御 (ABAC) を実装します。

これらの課題に対応するには、多くの場合、私たちが今日 Next.js で実装したような単一アプリケーションではなく、専用のプロキシソフトウェアや API Gateway 製品、認証基盤などを組み合わせてシステムを構築することになります。目的や規模に応じて適切な技術スタックを選択することが重要です。

riddle_tecriddle_tec

気づき

  • 初期の仕様をしっかり決める
  • 作業プロセスを明確化する
  • 各ステップに定期的にテストを仕込む
  • コミットを行う

っていう流れでめちゃくちゃ簡単に作れる。
今回は作ってる間に部屋の片付けをしてた。

このスクラップは5ヶ月前にクローズされました