必需品
2 つのポート上の 2 つの Router 軸 (8080 データ プレーン、 8090 管理プレーン)。同じ共有 AppStateから構築されます。データ プレーンにはどのルートでも API キーが必要です。プレーン管理には専用の JWT オーディエンス コンソールが必要です。2 つのメカニズムが重複することはありません。レート制限は 2 回適用されます。つまり、認証前に IP によって、次に認証されたアクターによって認証されます。サーキット ブレーカーはグローバル ミドルウェアではありません。これはサービスごと (および PostgREST の専用ターゲットごと) のオブジェクトであり、プロキシ コードで直接呼び出されます。そして、最終的なプロキシは、ルートに応じて、場合によっては HTTP メソッドまたはデータベース ルックアップに応じてトランスポートを変更します。つまり、ほとんどのトラフィックに対する NATS 要求/応答、ストレージに対する直接 HTTP フロー、3 つのリアルタイム バリアント、および専用の Postgres CRUD です。
単一のゲートウェイ、まったく異なる 2 つの視聴者
データ プレーン トラフィックは、SDK またはクライアント アプリから送信されます。つまり、大量の匿名リクエスト、または API キーによって認証されたリクエストであり、パブリック API に近い悪用プロファイルが含まれます。トラフィック管理プレーンは Studio (プロジェクトの管理インターフェイス) から提供され、プロジェクトの作成、キーのローテーション、テナントのログの読み取りなどの機密性の高い操作を実行します。この 2 つは共通のターゲット (aura-auth、aura-db、aura-storage などの同じ内部サービスにプロキシ) を共有していますが、同じリスク面ではありません。
両方を同じルーターを通過させるには、2 つの不適切なオプションから選択する必要があります。Studio CORS がパブリック SDK (Access-Control-Allow-Origin: *) に必要なワイルドカードを継承するか、SDK が内部ダッシュボード用に設計されたオリジンの制限されたリストを継承するかのいずれかです。 aura-gateway コードは、このテンションを main.rsから整理します。2 つの別個の Routerは、それぞれ独自の CorsLayer を持ちます。ワイルドカードはデータ プレーン側で承認され、拒否され、管理プレーン側でエラーとして記録されます。
2 つの axum Router、1 つの共有 AppState
この分離は個別のデプロイメントではありません。2 つのプランは、同じ AppState (Postgres プール、NATS クライアント、Moka キャッシュ、サーキット ブレーカー) 上の同じプロセスで実行されます。 Router の構造のみが異なり、それぞれ起動時に 1 回呼び出され、2 つの別個の TcpListenerによって処理される 2 つの専用関数を介します。
2 つのルート グラフは同じベース service_routes()から始まります。同じプロキシ ハンドラー (db_proxy、 storage_proxy、 functions_proxy…) が両方のプランにマウントされ、それぞれに固有の追加ルートが付いています。同じハンドラーを再利用すると、プロキシの二重実装が回避されます。ミドルウェアのみを分岐させることで、セキュリティ境界を確保するためにビジネス ロジックが重複することを回避します。バックエンド自体がマルチサービス Cargo ワークスペースとして構造化されている場合は、Cargo ワークスペース アーキテクチャ ガイド を参照してください。ゲートウェイは、この部門の他のクレートの 1 つにすぎません。
入場時に認証が分岐する
データ プレーンでは、少数の真のパブリック パス (/health、JWKS、登録エンドポイント) を除き、すべてのルートで API キーが必須です。これは、apikey または X-API-Key ヘッダーとして送信されます。または、WebSocket および SSE ストリーミング ルートの場合のみ、?apikey=パラメーターとして送信されます。コードは、service_role キーに対するこの最後のモード、つまり URL キーがアクセス ログ、OTel トレース、および Referer ヘッダーに漏洩することを明示的に禁止しています。 JWT はデータ プレーン側でオプションのままです。JWT がないと、呼び出し元は anonのままになります。これを使用すると、 authenticatedになります。
管理プレーンでは、API キーは存在しません。JWT コンソールのみが受け入れられます。その対象ユーザーは正確に aurabase-controlである必要があります。ロールはトークン自体によって保持されるのではなく、プロジェクトを所有する組織のユーザーのメンバーシップからリクエストごとに再計算され、プロジェクト → 組織の関係を通じて継承されます。
| トークンが必要です | API キー (apikey / X-API-Key)、常に | JWT コンソール (権限: Bearer)、常に |
|---|---|---|
| 役割の昇格 | オプションの JWT: anon → 認証済み | 組織から継承された RBAC (所有者/管理者/開発者/閲覧者) |
| クエリ文字列を入力します | WS/SSE でのみ許容され、service_role では許容されません | 該当なし |
| 想定される聴衆 | 対象のプロジェクト(パスのUUID) | 「aurabase-control」を修正しました |
| コルス | ワイルドカード * 使用可能 | ワイルドカードは拒否されました、Studio のオリジンのみ |
ミドルウェアの実際の順序 (そしてそれが重要な理由)
axum は、連続した .layer() 呼び出しでミドルウェアをスタックします。そして、実行順序を管理するルールは実際には驚くべきものです。最後に配置された .layer() レイヤーが最も外側のレイヤーになるため、受信リクエストが最初に通過するレイヤーとなり、最後に応答が返されるレイヤーになります。したがって、ファイルを線形に読み取ると、実際の実行順序とは逆の順序になります。
request_id ミドルウェアは、RESPONSE に X-Request-Id ヘッダーを設定するだけであり、受信リクエストには設定しません。 AccessLogLayer はファイル内でその後に配置されるため、より外部的であり、したがって前に走査されるため、request_id フィールドのキャプチャでは、チェーンのさらに先で生成された識別子ではなく、クライアントが送信したヘッダーが読み取られます。呼び出し元が X-Request-Idを提供しなかった場合、アクセス ログ行には空のフィールドが残りますが、返される応答には新しく生成された UUID が含まれます。隠れた欠陥ではありません。.layer() 文字列が書き込まれる順序は、それに帰する論理的順序について何も保証しないことを思い出させてください。
レート制限: 最初に IP、次にアクター
レート制限は、チェーン内の 2 つの異なる時点で、2 つの異なるパスで適用されます。 1 つ目は、認証前に実行され、IP アドレスによって制限されます。一般的なフラッド対策フィルターで、公道でもアクティブです。これがないと、認証されていないフローが、JWT チェックをトリガーすることなく、ログ集約などの高価なエンドポイントを攻撃する可能性があります。 2 つ目は認証後に実行され、認証によって挿入されたクレームを使用してアクター (API キーまたはユーザー) ごとに制限します。これが実際の製品のクォータであり、請求とプランにカウントされるものです。
この実装は、ローカル計算には governor クレート (トークン バケット) を使用し、ゲートウェイ インスタンス間の分散にはスライディング ウィンドウの Lua Redis スクリプトを使用し、Redis が使用できない場合はローカル フォールバック (Moka キャッシュ) に依存します。リポジトリのデフォルト: 100 リクエスト/秒、バースト 1000。
サーキット ブレーカーはレイヤーではなく、ターゲットごとのオブジェクトです
チェーンの残りの部分とは異なり、サーキット ブレーカーはどの .layer()にも表示されません。 AppState は、サービス (認証、データベース、リアルタイム、ストレージ、関数、通知、AI、プロビジョナー、コントロール) ごとに 1 つの CircuitBreaker インスタンスを運びます。リクエストを試行する前に try_acquire_probe() を呼び出し、結果に応じて record_success() または record_failure() を呼び出すのは、ルーターではなくプロキシ コード自体です。
PostgREST の場合は独特です。専用トポロジ (プロジェクト固有の Postgres および PostgREST) 内のプロジェクトには共有フォールト ドメインがありません。各 PostgREST プロセスは独自のターゲットです。したがって、ゲートウェイは、解決されたターゲットによってインデックス付けされたサーキット ブレーカーのテーブルを維持し、オンザフライで設定され、非アクティブなエントリを削除するスイープによって 60 秒ごとにパージされます。このパージがなければ、新しい専用プロジェクトごとに、決して消えることのないエントリが追加されることになります。
プローブ トークンが明示的に消費されない場合、プローブ トークンは Drop として返されます。これは、解放されるブランチに到達せずにリクエストのすべての試行がタイムアウトになった場合に役立ちます。また、自動再実行は、NATS 側での不達が厳密に証明された場合にのみトリガーされます (NoResponders)。単純なゲートウェイ タイムアウトでは、リクエストの実際の配信については何も証明されず、再実行するとリクエストが 2 回実行される可能性があります。
最後のリンク: NATS または直接 HTTP、決してランダムではありません
最終的なプロキシは、単一のプロトコルを逆方向に話すことはなく、選択はルートによって固定されません。HTTP メソッド、さらにはデータベース検索に依存する可能性があります。ほとんどのトラフィック (認証、機能、通知、制御、データベースの大部分) について、ゲートウェイは HTTP リクエストを NATS エンベロープでシリアル化し、それをサービス専用のサブジェクトへのリクエスト/応答として送信します。これは、TCP ハンドシェイクなしのラウンド トリップであり、この RPC タイプのトラフィックでは従来の HTTP プロキシよりも大幅に高速であることがコードに記載されています。
ストレージ、リアルタイムの 3 つのバリアント (WebSocket、SSE、およびブロードキャスト/チャネル/プレゼンスの REST)、および条件付きでの Postgres CRUD リクエストは、このパスを出て、ライブのプールされた HTTP クライアントを通過します。ストレージはこの選択を明示的に行いました。NATS エンベロープでバイナリ本文をエンコードするには、シリアル化して両端のメモリに完全にロードし、NATS メッセージ サイズの上限を超えないようにする必要があります。これは、大きなオブジェクトにとっては実際のコストとなります。 WebSocket と SSE は、単純に要求/応答セマンティクスを許容しません。プロトコルのアップグレードとオープンのままのフローには、同等の NATS がありません。
最も興味深いケースは /v1/db/*で、そのハンドラーはリクエストごとに自ら決定します。管理ルート (スキーマ、ポリシー、生の SQL) は常に NATS で aura-db に入り、 PUT は常に NATS に入ります (PostgREST は完全な置換で 405 を返します)。MongoDB プロジェクトは常に NATS に入ります。そして、専用の PostgREST インスタンスが解決された Postgres プロジェクトの CRUD のみが Direct HTTP に入ります。この専用インスタンスが解決されない場合、ゲートウェイは共有 PostgREST にフォールバックせずに 503 に応答します。機能低下したサイレント フォールバックではなく、フェイルクローズが想定されます。 セキュリティ ヘッダー (厳密な CSP、CORS 資格情報なし) は、応答がゲートウェイを出る前にチェーンの最後に配置され、これらすべてのパスに均一に適用されます。
グローバル タイムアウトではなく、ルートごとのタイムアウト バジェット
ゲートウェイは、グローバル タイムアウトではなく、ルートのグループごとに TimeoutLayer を適用します。これは、ミドルウェアの順序と同じスタッキング メカニズムにリンクされた選択です。エッジ関数のルートには、残りのルートよりもはるかに長いバジェットが必要です (関数は正当に数分間実行できます)。リポジトリのデフォルトは、ほとんどのルートで 30 秒ですが、 /v1/functions/*の場合は 380 秒です。
単一のグローバル TimeoutLayer をすべての上に積み重ねると、両方のグループが同じ制限でカットされます。内側に配置されたより長いタイムアウトに関係なく、最も外側の位置に配置された最も短いタイムアウトが常に勝ちとなります。したがって、関数に別個のバジェットを付与する唯一の方法は、関数が決して共通ラップに入らないことです。ルートの各ブランチは独自の TimeoutLayerを持ち、2 つのルーターが結合される前に配置されます。その後、グローバル タイムアウトは適用されません。
このパターンを別の場所 (チェックリスト) で再現します。
- サービスごとではなく、PLAN (公開サーフェス) ごとに分離します。侵害されたパブリック SDK が管理ダッシュボードの CORS オリジン リストに到達することはありません。
- 2 つの個別の展開ではなく、単一の共有状態を維持します。ビジネス ロジックの複製には、ルーターの複製よりもコストがかかります。
- 実際のミドルウェアの順序を確認するには、ファイルの線形読み取りからではなく、最後の
.layer()から追跡します。 - IP ごとのレート制限 (認証前) とアクターごとのクォータ (後) を分離します。そうしないと、認証されていないフローによりコストのかかる検証が無制限に強制されます。
- プロキシ内の実際のネットワーク呼び出しのできるだけ近くにブレーカーを配置し、フォールト ドメインが共有されていない場合はターゲットごとにブレーカーのサイズを決定します。
- 単純なタイムアウトではなく、不達が証明された場合にのみリクエストを再実行してください。
- ルーターを結合する前に、各ルート グループに独自のタイムアウト バジェット セットを与えます。最長のバジェットを上書きするようなグローバル
TimeoutLayerを設定しないでください。