APIGatewayとFastAPIのCORS設定のお話

# はじめに
今回はAWS環境においてAPIGatetayとFastAPIにて、CORS設定でこんなお話がありましたという内容です。

結論としては、API GatewayがHTTP統合でバックエンドと連携をするのであれば、バックバックエンド側（FastAPI）でCORS設定した方が単純で良さそうでしたという話です。

ただ、ここまで至る紆余曲折があったので情報の整理をかねて記事にします。

# CORSとは
CORSについては、先人達が分かりやすい記事をたくさん投稿してくださっているので参考記事を引用しながら簡単に説明します。詳細は参考記事を見てください。

## CORS（Cross-Origin Resource Sharing）について
CORSの説明の前にオリジン(Origin)について簡単に説明しておくと、URL内の「スキーム+ホスト+ドメイン+ポート番号」までのことをオリジンと言います[[1]](#参考記事)（例:`https://www.n-hanshin.com:334` <sub>[※1](#注釈)</sub>）。このオリジンが異なる場合にアクセス制御が行われ、そのメカニズムがCORSと呼ばれるものになります。

今回の構成のようにフロントエンドとバックエンドのサーバーが異なる場合、オリジンが異なるためCORS対応を行う必要があります。
もし、どのフロントエンド（クライアント）側からでもwebサーバーのリソースが操作可能な場合は不正なデータ操作や漏洩につながることは容易に想像できると思います。なので、異なるオリジンからのアクセスは制限する必要あり、そのための設定がCORSです。
なぜCORSが必要かは参考記事[[2]](#参考記事)、[[3]](#参考記事)を見て頂くと分かりやすいと思います。

## プリフライトリクエストとヘッダーについて
CORSによる通信が発生し、アクセスの承認をする必要のあるAPIリクエストの場合、プリフライトリクエストと呼ばれるお伺いが行われた後に、本来のリクエストが行われます。
このプリフライトリクエストは`OPTIONS`メソッドで行われ、APIに許可されている通信内容をヘッダー情報として取得します。
プリフライトリクエスト以降のやり取りに取得したヘッダーがなければフロントエンド（クライアント）側はレスポンス内容などリソースにアクセスができない状態になります。

ヘッダーの内容としては以下の項目が使用されます。

>**CORSに関連するHTTPヘッダの例**
![cors_headers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682846/63f3d932-20f0-4a33-b9b3-6db1effaa159.png)
（プリフライトリクエスト（単純ではないリクエスト）より引用[[4]](#参考記事)）


また、通信フローは以下の手順になります。
>**プリフライトリクエストの場合の通信フロー**
① クライアントは `https://aaa.com` へアクセス。
② `aaa.com` のサーバはレスポンスを返す。
③ ブラウザは `https://bbb.com` へリクエスト送信して問題ないかを確認。（呼び出し元ドメインや送信予定のリクエストのメソッド・ヘッダ情報を送信）
④ サーバ `bbb.com` は③の条件のリクエストを受け入れOKの場合、受け入れ可能条件を送信。
⑤ ブラウザは追加コンテンツ取得のため、`https://bbb.com` へアクセス。
⑥ サーバ `bbb.com` は`Access-Control-Allow-Origin`ヘッダに表示を許可する呼び出し元として `https://aaa.com/` を指定し、コンテンツを送信。
![preflight_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682846/e039a6db-18a5-4fe9-af00-78ad5f38f820.png)
（プリフライトリクエスト（単純ではないリクエスト）より引用[[4]](#参考記事)）


# 今回の状況について
## おおまかな構成について
フロントエンドとバックエンドの大まかな構成は以下の図のようになっていました。

- **フロントエンド**
  - S3にHTTPなどのコンテンツを配置し、CloudFrontで配信する
- **バックエンド**
  - API Gatewayを用いてAPIのエンドポイントを公開し、その裏側はECSでAPI毎の機能を提供する
  - ECSはVPC内に配置されており、API GatewayをVPC統合で連携させている
    - プライベート統合なので、厳密にはNLBが間に設定されている
  - ECSのバックエンドの機能はFastAPIで実装されている

![archi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682846/3d428208-bac1-4304-9db3-417dbd7a5efd.png)

## API Gatewayの設定内容について

### APIの設定の状況
APIGatewayの設定はリソースパスを定義して、バックエンドのAPI毎にルーティングを行っていました。
また、API毎の設定としてVPCプロキシ統合にチェックを入れており、統合リクエストと統合レスポンスをカスタマイズしない状態にしていました。

コンソール画面の状態としては、以下の画像のような状態でした（設定内容は例です）。

![APIGateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682846/71c90141-bb91-4c6c-9f58-da0b69a9e135.png)
![APIGateway_VPCプロキシ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682846/f54a9055-511f-4aa5-b42b-f1e51fe5205b.png)

> note:
この設定を見て、「あれ？」と思われる方もいると思います。
後の解決策にても述べますが、リソースパスの設定は不要でした。

### CORS設定の状況
CORSの設定もAPI Gatewayのコンソール画面で行っていました。
リソースの詳細から「CORSを有効にする」から、許可するメソッドやOriginの設定を行いました。

![APIGateway_CORS1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682846/32d8b687-c621-4521-a049-0954109f70f6.png)

![APIGateway_CORS2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682846/bd0a8426-1042-4bfa-91e7-130bfa096ab9.png)


## バックエンド（FastAPI）の状況など
**バックエンド（FastAPI）**
API Gatewayで設定できているという認識のため、こちらは何も設定は施していない状況でした。

**フロントエンド**
フロントエンド側で必要なCORSによるAPI通信の設定は有効にしていました。

# 発生した問題
## 問題発覚時のエラー
さて、ここまで紹介した設定状況でフロントエンド（クライアント）側からAPIを打鍵すると、以下のようなエラーが発生しました。

```
Access to fetch at 'http://apigateway.AAAA:3000/' from origin 'http://cloudfront.BBBB:8080'has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```
`http://apigateway.AAAA:3000/`はAPIのアドレス、`http://cloudfront.BBBB:8080`はフロント（クライアント）のアドレスだと解釈してください。

つまり「`http://cloudfront.BBBB:8080/`から`http://apigateway.AAAA:3000`へのアクセスはCORSポリシーによってブロックされています。」と注意されてしまいました。

## 状況を整理して分かったこと
このエラーが発生した通信の内容を整理した結果、以下の状態になっていることがわかりました。

- **異常だと思われること**
  - プリフライトリクエスト後のリクエストに対して、レスポンスヘッダー中に`Access-Control-Allow-Origin`が存在しないこと
- **正常だと思われること**
  - プリフライトリクエストは成功していること（200番台のレスポンス）
  - プリフライトリクエストには`Access-Control-Allow-Origin`などのCORSに必要なヘッダーが付与されていること
  - CORSに必要なヘッダーの設定値（許可されている項目）は想定しているあたいであること

[プリフライトリクエストとヘッダーについて](#プリフライトリクエストとヘッダーについて)で引用した通信フローに当てはめると、⑥の通信だけが異常な状態でした。

# 解決策
対応策としては単純で、バックエンド側でAPI通信を制御する機能を持っているのでそちらに一任してしまうことでした。

FastAPIでCORS設定を行う方法としては、CORSMiddlewareを用いてCORSで許可する通信を設定すれば良いだけでした。このミドルウェアはOPTIONSメソッドのリクエストを横取りし、適切なヘッダー情報を付与しレスポンスを返してくれます[[5]](#参考記事)。

```python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
    "http://localhost.tiangolo.com",
    "https://localhost.tiangolo.com",
    "http://localhost",
    "http://localhost:8080",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/")
async def main():
    return {"message": "Hello World"}
```

なので、API Gateway側でリソースパスなどをマメに設定し、CORSを有効にすることが不要になりました。
具体的なAPI Gatewayの設定は「HTTPプロキシ統合」となるように設定すれば問題ありませんでした[[6]](#参考記事)。


# 問題だったこと
では、なぜAPI GatewayでのCORS設定が上手くいかなかったのか考えてみます。
awsの資料などを読み自ら解釈した点などがあるので、「ココ違うよ」などがあれば教えてください。

結論として「VPCプロキシ統合を有効にしていた状態でCORS設定をしたこと」が諸悪の根源（問題）だと考えています。

理由は以下の2つです。

**1. VPCプロキシ統合を有効にすることで、統合レスポンスが編集できなかったため**
**2. API GatewayでCORS設定を有効にすると、統合レスポンスでヘッダー情報を付与するため** 

1.については、コンソール画面でも説明されている通りにクライアントのリクエスト、APIサーバーからのレスポンスを編集せずに返すことが説明されています。

また、2.についてはawsのディベロッパーガイドの「API Gateway での REST API の CORS」に以下のような記載[[7]](#参考記事)があります。

>「AWS Management Consoleを使用して非プロキシ統合の CORS を有効にする」
AWS Management Consoleを使用して CORS を有効にすることができます。API Gateway は、OPTIONS メソッドを作成し、**Access-Control-Allow-Origin ヘッダーを既存のメソッド統合レスポンスに追加します。**
これは常に機能するとは限りません。場合によっては、少なくとも 200 個すべてのレスポンスに対して、すべての CORS 対応メソッドの Access-Control-Allow-Origin ヘッダーを返すように統合レスポンスを手動で変更する必要があります。

加えて、「プロキシ統合の CORS サポートを有効にする[[8]](#参考記事)」では以下のようにも記載されています。

>バックエンドが `Access-Control-Allow-Origin` ヘッダー、`Access-Control-Allow-Methods` ヘッダー、`Access-Control-Allow-Headers` ヘッダーを返す必要があります

以上のことから、プロキシ統合にてCORS設定をする際にはバックエンドでCORSのヘッダーを付与する必要があったが、見落としていたことが問題でした。

# おわりに
CORSポリシーによってブロックされたエラーを発端に、CORSについてやAPIGateway、FastAPIの設定方法を調べた内容をまとめてみました。

問題だったことでも言及しましたが、プロキシ統合ではバックエンドでのCORSのためのヘッダー付与が必要でした。しかし、API Gatewayに中途半端にCORSの責務を負わせるなら、バックエンド側でCORS設定した方が単純で良さそうでしたという話でした。

---
# 参考記事
- [1] [オリジン間リソース共有 (CORS)とは - オリジンとは](https://qiita.com/Hirohana/items/9b5501c561954ad32be7#%E3%82%AA%E3%83%AA%E3%82%B8%E3%83%B3%E3%81%A8%E3%81%AF)
- [2] [CORS #Security - CORSはなぜ必要？](https://qiita.com/mrshouuge/items/c4d5d06d1cf0b73548e1#cors%E3%81%AF%E3%81%AA%E3%81%9C%E5%BF%85%E8%A6%81)
- [3] [初心者が知るべき CORS の基本 - SOPが必要な理由](https://developers.play.jp/entry/2024/05/31/174429#%E3%81%AA%E3%81%9C%E8%A8%B1%E5%8F%AF%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%8B:~:text=Same%2DOrigin%E3%81%A7%E3%81%99%E3%80%82-,SOP%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E7%90%86%E7%94%B1,-%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82Same%2DOrigin)
- [4] [CORS #Security - プリフライトリクエスト（単純ではないリクエスト）](https://qiita.com/mrshouuge/items/c4d5d06d1cf0b73548e1#%E3%83%97%E3%83%AA%E3%83%95%E3%83%A9%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E5%8D%98%E7%B4%94%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%84%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88)
- [5] [FastAPI - CORS (オリジン間リソース共有)](https://fastapi.tiangolo.com/ja/tutorial/cors/#cors)
- [6] [API Gateway での REST API の HTTP 統合 - API Gateway の HTTP プロキシ統合を設定する](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/setup-http-integrations.html#api-gateway-set-up-http-proxy-integration-on-proxy-resource)
- [7] [API Gateway での REST API の CORS - シンプルではないリクエストの CORS を有効にする](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html#apigateway-enable-cors-non-simple)
- [8] [API Gateway での REST API の CORS - プロキシ統合の CORS サポートを有効にする](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html#apigateway-enable-cors-proxy)

# 注釈
※1: なんでや阪神関係ないやろ