はじめに:なぜ今、インフラ層でのCORS制御なのか

Kubernetes環境でWeb APIを公開する際、CORS(Cross-Origin Resource Sharing)ポリシーの管理は、フロントエンドとバックエンドをまたぐチーム間の調整コストが高くなる典型例だ。従来のアプローチでは、Ingressリソースの注釈にレスポンスヘッダーを設定するか、アプリケーションコード内でミドルウェアとして実装するかの二択が一般的だった。

これらのアプローチには構造的な問題がある。Ingress注釈は環境ごとに設定が分かれ、ステージングと本番で許可オリジンが乖離するリスクがある。アプリケーションコード内での実装では、CORSポリシーの変更がアプリケーションのデプロイサイクルに依存し、セキュリティチームが独自に許可オリジンを追加できない構造になる。さらに、Ingress注釈は非構造化のキーバリュー形式のため、ポリシーの整合性を静的に検証する手段が限られ、レビュー時の見落としも起こりやすい。

GKEではGateway APIを用いたCORS制御が可能であり、ロードバランサー層でクロスオリジンポリシーを管理する機能を提供している(GKEチームの公式解説記事)。この機能により、インフラエンジニアがアプリケーションのデプロイをトリガーにせずにポリシーを変更できるようになる。

本記事では、このInfrastructure-as-Code的なアプローチの設計意図を整理し、既存のIngressやService Mesh、アプリケーションコードベースのCORS管理からの移行における具体的な判断基準を解説する。特に、GKE Gateway固有の制約を踏まえた実務的な移行パターンに焦点を当てる。

前提知識:Gateway APIとCORSフィルタの仕様

HTTPRouteリソース内での宣言的定義

GKE GatewayにおけるCORS制御は、オープンソースのGateway API仕様に準拠したHTTPRouteリソース内で宣言的に定義される。具体的には、HTTPRouteマニフェストのrulesセクション内でCorsPolicyを指定し、許可するオリジン・メソッド・ヘッダーを明示的に記述する。この構造により、どのルートにどのCORSポリシーが適用されるかがマニフェスト上で一目で確認できる。

バージョン要件と拡張サポート

Gateway APIのHTTPRouteCORS機能はv1.5.0以降、標準チャネルの一部として定義されている(Gateway API CORS User Guide)。ただし、GKE Gatewayでは拡張サポート機能として実装が必要であり、単にマニフェストを適用すれば動作するわけではない。クラスタのGatewayクラスがCORSフィルタに対応していることを確認し、必要に応じて拡張機能を有効にする手順を踏む必要がある。プレビュー段階の機能であるため、本番適用前にステージング環境での動作確認が前提となる。

Ingress注釈との構造的差異

従来のIngressリソースの注釈ベースの設定とは対照的に、Gateway APIのCORSフィルタは構造化されたAPI仕様によりポリシーの可読性と検証性が向上する。注釈が自由記述のキーバリューであるのに対し、CorsPolicyリソースはスキーマが定義されており、kubectl apply時の型検証やCI/CDパイプラインでの静的チェックが容易になる。

allowOriginsフィールドには、特定のオリジン(https://app.example.com)、ワイルドカード(*)、またはスキームとホスト名の間にワイルドカードを含む半指定のオリジン(https://*.bar.com)を指定できる。allowCredentialsフィールドはブラウザがCORSリクエストに資格情報を含めることを許可するかどうかを指定し、trueの場合にはワイルドカードではなく特定のオリジンを明示的に定義する必要がある。この制約は、資格情報を扱うAPIでセキュリティを担保するための仕様の意図であり、設計時に意識すべきポイントだ。

設計判断:アプリ層 vs インフラ層でのCORS管理のトレードオフ

インフラ層でCORSを制御する最大の利点は、アプリケーションコードの変更なしにポリシー変更が可能となり、デプロイサイクルが独立することだ。許可オリジンの追加やメソッドの制限変更が、アプリケーションのビルド・テスト・デプロイの手順を踏まずに行える。これは、セキュリティインシデント発生時に即座に許可オリジンを制限したい場面や、新規フロントエンド環境の接続を承認したい場面での対応速度に直結する。

一方で、動的なオリジン判定が必要なケースや、リクエストごとに異なるカスタムヘッダーを許可する複雑な処理では、アプリ層での実装の方が柔軟性が高い場合がある。Gateway APIのCORSフィルタは静的なルールの宣言には適しているが、リクエストのコンテキストに応じて動的にポリシーを変えるロジックは、現状の仕様では表現しにくい。例えば、APIキーの種別によって許可するオリジンを切り替えるような要件は、アプリ層のミドルウェアで実装する方が自然だ。

静的なCORSルールで収まるWeb APIやフロントエンド連携においては、インフラ層オフロードが保守コスト削減に直結する。特に、フロントエンドのデプロイ頻度が高い環境では、CORSポリシーの変更がバックエンドのデプロイに依存しないことが、リリースサイクルのボトルネック解消につながる。以下に、両アプローチの比較をまとめる。

観点アプリ層でのCORS管理インフラ層(GKE Gateway)でのCORS管理
ポリシー変更の反映速度アプリ再デプロイが必要マニフェスト適用で即時反映
動的オリジン判定リクエスト内容に応じた分岐が可能静的ルールの宣言のみ
デプロイ依存性アプリのリリースサイクルに依存インフラ変更と独立
静的検証・レビュー性コードレビューに依存スキーマ検証・GitOpsで整合性確保
カスタムヘッダーの動的制御条件分岐で柔軟に対応可能固定ヘッダーリストの指定が基本

判断の基準として、許可オリジンが数件〜十数件程度で、変更頻度が月数回以下、かつ資格情報の扱いが明確なAPIであれば、インフラ層へのオフロードが合理的だ。逆に、オリジンがリクエストのコンテキストに依存して動的に変化する、あるいはヘッダーの許可範囲がエンドポイント単位で大きく異なる場合は、アプリ層での実装を維持し、インフラ層では基本的なヘッダーの透過処理に留める設計が現実的である。

実装パターン:HTTPRouteマニフェストでの宣言的定義

GKE GatewayにおけるCORS制御は、Gateway API仕様に準拠したHTTPRouteリソースのrulesセクション内で宣言的に定義される(GKEチームの解説記事)。Ingressのannotationsのようにキーと文字列値の組み合わせで暗黙的に設定するのではなく、構造化されたスキーマでポリシーを記述するため、kubectl apply時のスキーマ検証やGitOpsツールでのdiff確認が自然に機能する。

以下は、特定のオリジンからのアクセスを許可する最小構成の例である。

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-cors
  namespace: production
spec:
  parentRefs:
    - name: prod-gateway
      kind: Gateway
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: "/api"
      cors:
        allowOrigins:
          - type: Exact
            value: "https://app.example.com"
        allowMethods:
          - GET
          - POST
          - OPTIONS
        allowHeaders:
          - Content-Type
          - Authorization
        allowCredentials: false
        maxAge: 3600
      backendRefs:
        - name: api-service
          port: 80

allowOriginsには3つの形式を指定できる。第一に、スキームとホスト名を完全一致で指定する形式(https://app.example.com)。第二に、ワイルドカード(*)で全オリジンを許可する形式。第三に、スキームとホスト名の間にワイルドカードを含む半指定パターン(例: https://*.bar.com)である(Gateway API CORS User Guide)。半指定パターンはサブドメインが動的に増えるSaaS環境で有用だが、*.example.comのようにトップレベルのワイルドカードにすると、同ドメイン配下の全サブドメインが許可対象になる点に注意が必要だ。

最も重要な制約はallowCredentialsの扱いである。このフィールドをtrueに設定すると、ブラウザはCORSリクエストにCookieやHTTP認証情報を含めるようになるが、その代わりとしてワイルドカードや半指定パターンは使用できず、特定のオリジンを明示的に列挙する必要がある。これはブラウザ仕様のセキュリティ要件であり、GKE Gatewayが独自に追加した制限ではない。資格情報を扱うAPIでは、この制約を前提にオリジンリストを設計する必要がある。

なお、このCORS機能はGateway API v1.5.0以降の標準チャネルに組み込まれているが、GKE Gatewayでは拡張サポート機能として実装が必要となる(Gateway API CORS User Guide)。つまり、クラスタのGateway APIバージョンがv1.5.0未満の場合はこの機能を使用できない。導入前にkubectl get crd httproutes.gateway.networking.k8s.io -o yamlで現在のCRDバージョンを確認し、必要に応じてGKEクラスタのアップグレード計画に組み込むべきである。

Gatewayクラスの選択とマルチクラスターの制約

GKE Gatewayは3つのGatewayクラスをサポートしている。それぞれがロードバランサーの構成とネットワークの範囲を決定するため、CORSポリシーの適用範囲にも直接影響する。

Gatewayクラスネットワーク範囲用途の目安
gke-l7-rilbリージョン内(Internal LB)VPC内サービス間通信、プライベートAPI
gke-l7-regional-external-managedリージョン(External LB)リージョン単位の公開API
gke-l7-global-external-managedグローバル(External LB)グローバル公開サービス、CDN連携

選択の判断基準は、CORSの観点からは「どのネットワーク境界でオリジンチェックを行うか」に集約される。gke-l7-rilbは内部ロードバランサーを使用するため、外部からの直接アクセスが存在しない。この場合、CORSフィルタが意味を持つのは、VPC内から異なるオリジン(別VPCやオンプレミス)へアクセスするケースに限られる。一方、gke-l7-regional-external-managedとgke-l7-global-external-managedは外部トラフィックを直接受けるため、CORSフィルタが実際のブラウザリクエストに対して機能する。

グローバル公開サービスではgke-l7-global-external-managedが自然な選択だが、ここで注意すべきは後述する正規表現の制約が最も厳しくなる点である。リージョン内で完結するSaaSや、特定リージョンのユーザーのみを対象とするサービスではgke-l7-regional-external-managedがコストと制約のバランスが良い。

もう一つの重要な制約は、GKE Gatewayがマルチクラスターゲートウェイに対応していない点である(GKEチームの解説記事)。つまり、複数のGKEクラスタを単一のGatewayリソースで横断してルーティングする構成では、このCORS機能を使用できない。マルチクラスター環境では、各クラスタに個別のGatewayリソースを作成し、DNSレベルでトラフィックを分散する設計になる。この場合、CORSポリシーもクラスタ単位で定義・管理する必要があり、ポリシーの整合性を保つための運用プロセス(GitOpsでの単一ソース管理など)を別途設計する必要がある。

実務上の判断としては、CORSポリシーを「1つのHTTPRouteで完結できるか」を確認することから始めるべきだ。オリジンリストが全クラスタで共通であれば問題ないが、クラスタごとに異なるオリジンを許可する必要がある場合は、マルチクラスター非対応の制約が設計を大きく左右する。そのようなケースでは、アプリ層でのCORS処理を維持し、インフラ層ではヘッダーの透過処理に留める方が運用負荷が低くなる場合がある。

落とし穴:RequestRedirectとの併用制限と正規表現の罠

GKE GatewayのCORS導入で最も頻繁に遭遇する設計上の壁が、CORSフィルタとRequestRedirectフィルタの併用制限である。これらは同じルートルール内で同時に指定できないため、HTTPS強制(HTTP→HTTPSリダイレクト)とCORS制御を別々のルートルールに分割する必要がある(GKEチームの解説記事)。

具体的には、HTTPリスナー(ポート80)のルートルールにRequestRedirectを配置してHTTPSへ転送し、HTTPSリスナー(ポート443)のルートルールにCORSフィルタを配置する、という2段構成になる。この分割により、リダイレクト後にCORSチェックが実行される順序が保証される。逆に、同じルールに両方を記述しようとしても、Gateway APIの検証でエラーとなりマニフェストの適用が失敗する。

もう一つの落とし穴はURLマッチングの正規表現制限である。CORSフィルタのオリジン判定とPathPrefixマッチの組み合わせにおいて、URLマップの正規表現制限が意図しないアクセス拒否を引き起こし得る。特に、半指定オリジン(https://*.example.com)を使用する場合、ホスト名のワイルドカード部分とパスのマッチングが独立して評価されるため、期待通りに通らないケースがある。

正規表現の上限はGatewayクラスによって異なる。グローバル外部ロードバランサーではリスナーあたり1つの正規表現のみが許可される一方、リージョン外部および内部ゲートウェイではホスト名あたり最大5つの正規表現が使用可能である(GKEチームの解説記事)。グローバルクラスで複数の異なる正規表現パターンが必要な場合は、リスナーを分割するか、PathPrefixやExactマッチに代替する設計判断が必要になる。

flowchart TD
    A["リクエスト受信"] --> B{"リスナー/パスマッチ"}
    B -->|"ルール1 (ポート80)"| C["RequestRedirect"]
    B -->|"ルール2 (ポート443)"| D["CORSフィルタ"]
    C --> E["301/308 リダイレクト"]
    D --> F{"allowOrigins判定"}
    F -->|"一致"| G["バックエンド転送"]
    F -->|"不一致"| H["プリフライト失敗"]

図に示すように、リダイレクトとCORSチェックは別々のルートルールで独立して処理される。この設計上の分離を前提に、HTTPRouteマニフェストのrules配列の順序(先頭が優先)も意識する必要がある。PathPrefixの/で全パスをキャッチするルールを先頭に置くと、より具体的なパス向けのCORSルールが到達不能になる。マッチの優先順を意図通りに制御するため、より具体的なパターンを先頭に配置し、最後のルールをフォールバックとするのが安全な設計である。

運用設計:ポリシー変更時の検証とデプロイ戦略

CORSポリシーはHTTPRouteリソースの一部として宣言的に定義されるため、kubectl apply やGitOpsツール(Argo CD、Fluxなど)で管理できる。ただし、この機能はプレビュー段階であり(GKEチームの発表)、本番環境に適用する前にステージング環境での検証を必須とするプロセスを確立する必要がある。

具体的には、以下のような検証手順を標準化することを推奨する。

  1. ステージング環境にマニフェストを適用する
  2. 許可するオリジンからのプリフライトリクエスト(OPTIONS)を発行し、レスポンスヘッダー(Access-Control-Allow-Origin、Access-Control-Allow-Methods 等)を確認する
  3. 許可外のオリジンからのリクエストが正しく拒否されることを確認する
  4. allowCredentials が true の場合、ワイルドカードオリジンが使用されていないことを確認する

CORSポリシーの変更が即座に反映される場合と、ロードバランサー側のキャッシュにより遅延が生じる場合がある。プレビュー段階ではこの挙動が安定していない可能性があるため、変更後一定時間待機してから再検証するのが安全である。特に、allowOrigins のリストにオリジンを追加した直後に「まだ反映されていない」と誤判断して重複エントリを追加する失敗はよく見られる。変更の反映に時間がかかる場合は、まず既存エントリの更新(in-place update)を優先し、追加は次回のバッチ処理に回す運用ルールを設けることで、リストの肥大化を防げる。

監査証跡の観点では、どのHTTPRouteにどのようなCORSルールが適用されているかを可視化しておくことが重要だ。kubectl get httproute -o yaml で全リソースをダンプし、CORS関連のフィールドを抽出してレビューに回すスクリプトを用意しておくと、変更の追跡が容易になる。Gitのコミット履歴と対応づけることで、「いつ、誰が、どのオリジンを追加したか」を事後に確認できる状態を維持しよう。特に、業務部門から「このドメインを許可してほしい」という依頼が頻発する環境では、承認フローとマニフェスト変更の対応関係を明確にしておくことが、障害時の原因切り分けを大幅に短縮する。

発展:セキュリティとパフォーマンスへの影響

インフラ層でCORS制御を行うことで、アプリケーションが毎リクエストでオリジン判定やヘッダー付与を行うオーバーヘッドが削減される。スループットへの影響は負荷量やバックエンドの処理内容に依存するため、定量評価は実際の環境でのベンチマークに委ねるべきだが、アプリケーションコードからCORS処理を除去できること自体が、デプロイの複雑さやバグ発生の要因を減らす効果がある。特に、マイクロサービス化が進んでいる環境では、CORS設定が各サービスに分散している状態からインフラ層に集約することで、設定の不一致による「ある環境では動作するが別環境では失敗する」といった問題の発生頻度が低下する。

セキュリティ面では、allowOrigins の指定方法がリスクの大きさを直接決める。Gateway APIの仕様では、特定のオリジン、ワイルドカード(*)、スキームとホスト名の間にワイルドカードを含む半指定パターン(例: https://*.bar.com)の3つの形式が許可されている(Gateway API CORS User Guide)。ワイルドカードや半指定パターンは管理対象のオリジンが増える環境で便利だが、DNSの乗っ取りやサブドメインの権限移譲により意図しないオリジンが許可対象に含まれるリスクがある。最小権限の原則に基づき、可能な限り特定のオリジンを明示する設計を優先すべきである。

allowCredentials を true に設定する場合、仕様上ワイルドカードオリジンは使用できず、特定のオリジンを明示的に定義する必要がある(Gateway API CORS User Guide)。これはブラウザのセキュリティモデル上、資格情報付きリクエストの許可範囲を曖昧にできないためであり、設計時にこの制約を前提にオリジンリストを管理する必要がある。Credentials付きリクエストが発生するエンドポイントと発生しないエンドポイントでHTTPRouteを分離し、前者のみで明示的なオリジンリストを維持する設計が、管理コストとセキュリティのバランスを取る有効な手段となる。

将来的には、Gateway APIの仕様拡張によりCORSフィルタの機能も拡張される見込みがある。HTTPRouteCORS機能はv1.5.0以降標準チャネルの一部だが、拡張サポート機能として実装が必要となる(Gateway API CORS User Guide)。GKE Gatewayがどのバージョンで安定版を提供するかは未定であるため、アップグレード計画にこの依存関係を組み込み、安定版リリース時の機能差分を確認するプロセスを事前に決めておくとよい。

まとめ:インフラ主導のCORS管理への移行ロードマップ

GKE GatewayのネイティブCORSサポートは、Ingressの注釈やアプリケーションコードに散在していたCORS設定を、インフラ層の宣言的リソースに集約する方向性を示している。プレビュー段階ではあるが(GKEチームの発表)、ロードバランサー層でポリシーを一元管理する設計思想自体は、Gateway APIエコシステムの標準的なアプローチと整合しており、安定版リリース後も継続的な投資が期待できる。

移行を検討する際の判断基準を整理する。まず、GKE Gatewayは gke-l7-rilb、gke-l7-regional-external-managed、gke-l7-global-external-managed の3つのGatewayクラスをサポートし、マルチクラスターゲートウェイには未対応である(GKEチームの発表)。単一クラスターで完結するサービスであれば問題ないが、マルチリージョン・マルチクラスター構成を前提とするアーキテクチャでは、現時点ではアプリ層でのCORS管理を継続する選択肢も視野に入れる必要がある。

実務的な移行ロードマップとしては、以下のような段階を踏むことを推奨する。

  1. 既存CORS設定の棚卸し:アプリ層やIngress注釈で定義されているオリジンリスト・メソッド・ヘッダーを洗い出す
  2. Gatewayクラスの選定:トラフィックの地理的範囲とレイテンシ要件に基づきクラスを決定する
  3. ステージング環境でのHTTPRoute構築と検証:CORSフィルタの動作確認とRequestRedirectとの併用制約の検証を行う
  4. 本番環境への段階的適用:トラフィックの一部をGKE Gateway経由に切り替えて段階的に移行する
  5. 運用プロセスの整備:GitOpsによる変更管理、監査レビュー、障害時のロールバック手順を確立する

プレビュー段階の制約(正規表現の制限、マルチクラスター非対応、RequestRedirectとの併用不可など)を理解した上で、自社のアーキテクチャに適合する範囲で導入を進めることが、安定した運用への近道となる。CORSポリシーは一度設計すると頻繁に変更されないことが多いが、フロントエンドのドメイン構成が変わるタイミング(リブランディング、CDNの切り替え、マルチテナント化など)でまとめて見直す運用サイクルを設けておくと、設定の陳腐化を防止できる。

関連記事

参考

本記事は海外の技術トレンド「Native CORS support on GKE Gateway: Offloading cross-origin policy management to infrastructure」(DEV)で話題のテーマをもとに、両儀システムソリューションズが独自に解説したものです。