0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【CloudFront】S3 と API Gateway を1つのドメインに束ねたら CORS が消えた話

0
Posted at

はじめに

静的サイト(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 と格闘している人の、別の選択肢になれば嬉しいです🙌

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?