CloudFront /api/* で 1 ドメイン化 — dynamic block と Origin Request Policy
API Gateway は https://xxx.execute-api.region.amazonaws.com という ugly な URL を払い出します。本記事ではこれを https://lab.iigtn.com/api/contact という同一ドメインに統合する設定を解説します。フロント呼び出しの CORS も自動で解決される副次効果があります。
なぜ 1 ドメインに統合するか
| 観点 | 別ドメイン (api.lab.iigtn.com) | 同一ドメイン (lab.iigtn.com/api/*) |
|---|---|---|
| URL の見た目 | 整理されている | シンプル |
| CORS | 必要 | 不要(同一オリジン) |
| cookie 共有 | 困難 | 容易 |
| 追加 DNS / 証明書 | 必要 | 不要 |
| 運用 | 2 系統 | 1 系統 |
本シリーズは個人サイト規模なので 1 ドメイン統合の方がシンプル。CORS preflight も同一オリジンなので発生しません。
CloudFront に「2 つ目の origin」を追加
CloudFront Distribution は複数の origin を持てます。本シリーズでは:
- origin 1: S3 バケット(既存・default behavior)
- origin 2: API Gateway(新規・
/api/*behavior)
これを Terraform の dynamic block で条件付きで定義します(frontend_cdn モジュールに後付けする形で実装)。
frontend_cdn モジュールに API origin 引数を追加
# modules/frontend_cdn/variables.tf に追加
variable "api_origin_domain_name" {
description = "/api/* を委譲する API Gateway のホスト名 (例: xxx.execute-api.ap-northeast-1.amazonaws.com)。空文字なら API behavior を作らない"
type = string
default = ""
}
variable "api_path_pattern" {
description = "API behavior にマッチさせるパスパターン"
type = string
default = "/api/*"
}
デフォルト値を空にしておくことで、API を持たない静的サイトでもこのモジュールを使い回せます。
dynamic block で条件付き origin 追加
resource "aws_cloudfront_distribution" "this" {
# ... 既存の S3 origin はそのまま
# API GW origin: api_origin_domain_name が空でない時だけ追加
dynamic "origin" {
for_each = var.api_origin_domain_name == "" ? [] : [1]
content {
domain_name = var.api_origin_domain_name
origin_id = "apigw-origin"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only" # API GW は HTTPS のみ
origin_ssl_protocols = ["TLSv1.2"]
}
}
}
}
dynamic は Terraform の便利構文。for_each に空配列を渡せばブロック生成 0、要素 1 個渡せば 1 個生成。条件付きでブロックを足す王道。
/api/* behavior を dynamic block で追加
dynamic "ordered_cache_behavior" {
for_each = var.api_origin_domain_name == "" ? [] : [1]
content {
path_pattern = var.api_path_pattern # /api/*
target_origin_id = "apigw-origin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["DELETE","GET","HEAD","OPTIONS","PATCH","POST","PUT"]
cached_methods = ["GET","HEAD"]
compress = true
# API レスポンスはキャッシュしない
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # CachingDisabled
# Host 以外のヘッダ・クエリ・cookie をオリジンに転送
origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac" # AllViewerExceptHostHeader
}
}
AllViewerExceptHostHeader が必要な理由
これがハマりポイント。普通に AllViewer を使うと、リクエストの Host ヘッダが lab.iigtn.com のまま API Gateway に届きます。すると API GW は「自分宛じゃないリクエスト」と判断してエラー。
正解は 「Host ヘッダだけは転送せず、API GW 側で xxx.execute-api... を Host とする」。これを実現するのが AllViewerExceptHostHeader マネージドポリシー。
{"message":"Forbidden"} が返る場合、ほぼ確実に Host ヘッダ問題です。AllViewerExceptHostHeader を使うか、Host ヘッダを除外するカスタムポリシーを書く。
envs/prod/main.tf でモジュール呼出
module "frontend_cdn" {
source = "../../modules/frontend_cdn"
domain_name = var.domain_name
certificate_arn = module.network_dns.certificate_arn
bucket_name = local.web_bucket_name
# /api/* を API Gateway に向ける
api_origin_domain_name = module.backend_api.api_endpoint_host
providers = {
aws = aws
aws.us_east_1 = aws.us_east_1
}
}
module.backend_api.api_endpoint_host は API Gateway モジュールが output しているホスト名(xxx.execute-api...)。Terraform が apply 順を自動解決してくれる。
backend_api モジュール側の output
# modules/backend_api/outputs.tf
output "api_endpoint" {
value = aws_apigatewayv2_api.this.api_endpoint
# https://xxx.execute-api.ap-northeast-1.amazonaws.com
}
output "api_endpoint_host" {
value = replace(aws_apigatewayv2_api.this.api_endpoint, "https://", "")
# xxx.execute-api.ap-northeast-1.amazonaws.com (CloudFront origin に直で使える)
}
CloudFront の origin は ホスト名のみ(https:// 不要)。replace() で先頭の https:// を削るのが一手間。
apply の順序
Terraform は依存関係を自動解決するので、特に意識せず terraform apply で OK:
- backend_api モジュールが先に apply される(API GW 等が作られて api_endpoint_host が確定)
- その値を frontend_cdn モジュールが受け取って Distribution を更新
- CloudFront Distribution の更新は 5〜15 分かかる
動作確認
# 直接 API GW を叩く
curl -X POST "https://xxx.execute-api.ap-northeast-1.amazonaws.com/api/contact" \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"a@b.com","message":"From direct"}'
# {"ok":true}
# CloudFront 経由 (本番経路)
curl -X POST "https://lab.iigtn.com/api/contact" \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"a@b.com","message":"From CDN"}'
# {"ok":true}
両方 200 が返れば成功。ブラウザの DevTools で確認すると、レスポンスヘッダに Via: cloudfront.net (CloudFront) と X-Amz-Cf-Pop: NRT12-P4(東京 Edge)が見えます。
フロント側の fetch コード
同一ドメインなので相対パスで叩けます:
// frontend/index.html の script タグ内
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message }),
});
ハードコードされた API URL が無いので、テスト・デプロイ環境に依存しないクリーンなコード。
Behavior の優先順位
CloudFront は behavior を「より具体的なものから順に」マッチさせます:
/api/*← まずこれをチェック → API GW へdefault_cache_behavior← マッチしなければここ → S3 へ
つまり /api/contact は API GW、/index.html や /static/main.js は S3。明示的に書かなくても自動でこの順序になる。
次の記事
API + フロント が同一ドメインで動くようになったら、次は 運用フェーズの観測 に入ります。次の記事では CloudWatch Alarms + SNS で「壊れたら気付ける」仕組みを作ります。
📚 用語集
- dynamic block (Terraform)
- Terraform の構文で、ネストされたブロック(
origin {}等)をfor_eachで動的に生成する。条件付きブロック追加にも使える(空配列 vs 1 要素配列で 0 or 1 ブロック生成)。 - ordered_cache_behavior
- CloudFront の追加 behavior。default_cache_behavior 以外のパスパターン用。
path_patternでパスを指定する。 - path_pattern
- CloudFront behavior でマッチさせる URL パスのパターン。wildcard(
*/?)が使える。 - custom_origin_config
- S3 以外の origin(API Gateway、外部サーバ等)の追加設定。HTTP/HTTPS ポート、TLS バージョン等を指定。
- origin_protocol_policy
- CloudFront → origin への通信プロトコル指定。
https-only/http-only/match-viewer。API GW は HTTPS のみなのでhttps-only。 - AllViewerExceptHostHeader
- AWS マネージドの Origin Request Policy。Host 以外のすべてをオリジンに転送。API Gateway を origin にする時の定番。
- Host ヘッダ
- HTTP リクエストの Host を示すヘッダ。複数のドメインが同じサーバを使う時の振り分けに使われる。CloudFront → API GW では Host を上書き必須。
- CachingDisabled
- AWS マネージドの Cache Policy。「キャッシュしない」設定。動的 API 用。
- allowed_methods
- CloudFront behavior で受け付ける HTTP メソッドの集合。POST を許可する場合は
["DELETE","GET","HEAD","OPTIONS","PATCH","POST","PUT"]一択。 - cached_methods
- CloudFront でキャッシュ対象とするメソッド。通常は
["GET","HEAD"]のみ(POST 等の結果はキャッシュしない)。 - 同一オリジン (Same-Origin)
- scheme + host + port が一致するリソース。同一オリジンなら CORS preflight 不要・cookie 共有可能。
- CORS preflight
- クロスオリジン POST 等の前にブラウザが自動で送る OPTIONS リクエスト。同一オリジン化すれば不要に。
- Behavior の優先順位
- CloudFront はより具体的な path_pattern を優先する。
/api/*が/*(default)より先にマッチする。 - fetch (相対パス)
- 同一ドメイン内なら
fetch('/api/contact')のようにhttps://...を省略できる。環境ごとの URL 切替が不要になる利点。