はじめに
静的サイト(S3)からバックエンドの API(API Gateway)を fetch したら、ブラウザに CORS で弾かれました。
Access to fetch at 'https://xxxx.execute-api.../api/hello'
from origin 'https://yyyy.cloudfront.net' has been blocked by CORS policy
以前にも CORS で詰まったことがあり、そのときは「サーバー側で許可ヘッダを返す」で対処しました。今回も同じ手を考えたのですが、構成を見直して そもそも CORS が起きない形にしたら、許可ヘッダなしで通りました。
先に結論:S3 と API Gateway を同じ CloudFront ドメインから配信すると、ブラウザから見て同一オリジンになり、CORS が発生しません。「許可ヘッダで対処」ではなく「構成で起こさない」という別解です。
環境
- Amazon CloudFront
- Amazon S3(静的サイト)
- Amazon API Gateway(HTTP API)+ Lambda
- Terraform で構築
- リージョン: ap-northeast-1(東京)
起きたこと
最初は、フロント(S3 を CloudFront で配信)と API(API Gateway のドメイン)を別々のドメインで持っていました。
- 画面:
https://yyyy.cloudfront.net - API:
https://xxxx.execute-api.ap-northeast-1.amazonaws.com
この状態でフロントから API を叩くと、ホストが違うのでブラウザが「別オリジンへのリクエスト」と判断し、CORS のチェックが入ります。プリフライト(OPTIONS)含め、許可設定をしていないと弾かれます。
原因:オリジンが違うとブラウザが止める
ブラウザの「オリジン」は スキーム + ホスト + ポート の組で決まります。
https://yyyy.cloudfront.net ← 画面のオリジン
https://xxxx.execute-api... ← API のオリジン(ホストが違う)
ホストが違えば別オリジン。だから API 側で「このオリジンからのアクセスを許可する」と明示しない限り、ブラウザがレスポンスをブロックします。これは仕様どおりの動きで、CORS 自体が悪いわけではありません。ただ、許可ヘッダの設定は地味に間違えやすく、できればそもそも別オリジンにしない方が楽だな、と思いました。
対処:CloudFront で1ドメインに束ねる
CloudFront に2つのオリジンを登録して、パスで振り分けるようにしました。
-
/(デフォルト) → S3(静的サイト) -
/api/*→ API Gateway
# デフォルトは S3 へ
default_cache_behavior {
target_origin_id = "s3"
# ...
}
# /api/* だけ API Gateway へ
ordered_cache_behavior {
path_pattern = "/api/*"
target_origin_id = "apigw"
# API が使うメソッドを許可・転送する(使う分だけに絞るのが安全)
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer_except_host.id
# ...
}
こうすると、ブラウザから見える URL は全部 https://yyyy.cloudfront.net の同一ドメインになります。
curl -s https://yyyy.cloudfront.net/ # → 画面(S3)200
curl -s https://yyyy.cloudfront.net/api/hello # → API(API Gateway)200
画面も API も同じドメインなので、ブラウザにとっては同一オリジン。CORS のチェックがそもそも発生しません。
ひとつ補足すると、許可ヘッダが要らなかったのは「ブラウザが CloudFront の同じドメイン経由で /api/* を呼ぶ構成」だからです。API Gateway の実 URL を直接叩く別クライアントや、別ドメインの画面から使う場合は、従来どおり API 側に CORS 設定が必要になります。「常に CORS 設定が不要」という話ではありません。
注意1:API Gateway をオリジンにするときは Host ヘッダを転送しない
ここで1つハマりどころがあります。CloudFront から API Gateway へ転送するとき、閲覧者の Host ヘッダ(=CloudFront のドメイン)をそのまま転送すると、API Gateway 側でホスト不一致になり弾かれることがあります。
対策として、/api/* のビヘイビアには Managed-AllViewerExceptHostHeader(閲覧者の Host を除外し、CloudFront がオリジンのドメインを Host として付け直す、というマネージドの Origin request policy)を当てています。これで Host だけは API Gateway 用に差し替えてくれます。
注意2:API のキャッシュ方針は明示する
CloudFront は静的配信でキャッシュが効くのが利点ですが、API のレスポンスをキャッシュするかは設計判断です。認証済みユーザーごとに内容が変わる API では、意図せず共有キャッシュしないよう、キャッシュを無効化するか、キャッシュキー(含めるヘッダ・Cookie・クエリ)を慎重に設計する必要があります。今回の /api/hello は誰が見ても同じ内容なので深く悩みませんでしたが、ユーザー依存の API を足すときは要注意ポイントです。
学び
- オリジンは スキーム + ホスト + ポート。1つでも違えば別オリジンで、CORS の対象になる。
- CORS は「許可ヘッダで通す」だけでなく、「同一ドメインに束ねて起こさない」という構成の選択肢がある。
- ただし許可ヘッダが不要になるのは CloudFront 同一ドメイン経由で呼ぶ場合。API を直接叩く経路には別途 CORS が要る。
- API 用ビヘイビアは 使う HTTP メソッドを
allowed_methodsで許可する。 - API Gateway をオリジンにするときは Host ヘッダを転送しない(
AllViewerExceptHostHeader)。 - API のキャッシュは設計判断。ユーザーごとに変わる API は共有キャッシュに注意。
おわりに
過去に CORS で詰まったときは「許可ヘッダをどう返すか」ばかり考えていましたが、今回「そもそも別オリジンにしない」という手があると分かって、視界が広がりました😊 同じく CORS と格闘している人の、別の選択肢になれば嬉しいです🙌
参考
- AWS公式: CloudFront で複数オリジンとキャッシュビヘイビアを使う
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html - AWS公式: キャッシュビヘイビアで HTTP メソッドを設定する
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html - AWS公式: マネージドオリジンリクエストポリシー(AllViewerExceptHostHeader)
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html - MDN: オリジン / 同一オリジンポリシー(CORS が起きる条件)
https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy