CloudFront のビヘイビアで API-GW のステージを制御しようとしたら失敗した話
失敗した構成
API-GW の前段に CloudFront を配置し、CloudFront のビヘイビアのパスパターンで API-GW のステージを制御しようとした。
-
https://example.com/api/helloが来たら、CloudFrontでいい感じに API-GWのProdステージのGET /helloに投げる

- API クライアント > CloudFront > API-GW > Lambda
- API クライアント(cURL)
-
https://<CloudFrontのドメイン>/prod/helloにアクセスする
-
- CloudFront
- ビヘイビアのパスパターンは
/api/* - オリジンは API-GW の URL を指定する
- オリジンパスは
/prodを指定する - キャッシュポリシーは
CACHING_DISABLEDを指定する - フォワードヘッダーは
AllViewerExceptHostHeaderを指定する
- ビヘイビアのパスパターンは
- API-GW
- ステージは
prodとdevを作成する -
GET /helloリソースを作成する
- ステージは
結論
この枠の中で実現することは不可能
仮に実現したい場合は、下記の選択肢がありそう
事業規模次第だけど、個人的には「CloudFront Function でパスを書き換える」or「Cloudfront を使わずに API-GW のみで完結させる」でいい気がする
CloudFront Function でパスを書き換える

API Gateway に /api リソースor api ステージを追加

API-GW カスタムドメイン + ベースパスマッピングを使用

Cloudfront を使わずに API-GW のみで完結させる

実現不可の理由(公式ドキュメント)
ちゃんと「追加します」って書いてあった(not 変換)
オリジン(API Gateway など)内の特定のディレクトリから CloudFront にコンテンツをリクエストさせたい場合は、スラッシュ(/)で始まるディレクトリパスを入力してください。
CloudFront は、このディレクトリパスをオリジンドメインの値に追加します
(例:cf-origin.example.com/production/images)。
パスの末尾にスラッシュ(/)は追加しないでください。
たとえば、ディストリビューションに以下の値を指定したとします。
- オリジンドメイン – amzn-s3-demo-bucket という名前の Amazon S3 バケット
- オリジンパス – /production
- 代替ドメイン名(CNAME) – example.com
ユーザーがブラウザで example.com/index.html と入力すると、CloudFront は Amazon S3 に対して amzn-s3-demo-bucket/production/index.html のリクエストを送信します。
ユーザーがブラウザで example.com/acme/index.html と入力すると、CloudFront は Amazon S3 に対して amzn-s3-demo-bucket/production/acme/index.html のリクエストを送信します。
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesOrigin.html
原文
Origin path
If you want CloudFront to request your content from a directory in your origin, enter the directory path, beginning with a slash (/).
CloudFront appends the directory path to the value of Origin domain, for example, cf-origin.example.com/production/images.
Do not add a slash (/) at the end of the path.
For example, suppose you’ve specified the following values for your distribution:
- Origin domain – An Amazon S3 bucket named amzn-s3-demo-bucket
- Origin path – /production
- Alternate domain names (CNAME) – example.com
When a user enters example.com/index.html in a browser, CloudFront sends a request to Amazon S3 for amzn-s3-demo-bucket/production/index.html.
When a user enters example.com/acme/index.html in a browser, CloudFront sends a request to Amazon S3 for amzn-s3-demo-bucket/production/acme/index.html.
検証時のログとか
実行 Shellと実行結果
- API-GW の dev ステージへは正常にアクセス可能
- API-GW の prod ステージへは正常にアクセス可能
- CloudFront 経由でアクセスすると 403 Forbidden エラーとなる
Shellファイル
API_GW_HOST="https://<api-gwドメイン>.execute-api.ap-northeast-1.amazonaws.com"
API_GW_DEV="$API_GW_HOST/dev/hello"
API_GW_PROD="$API_GW_HOST/prod/hello"
CLOUDFRONT_DOMAIN="https://<cloudfrontドメイン>.cloudfront.net"
CLOUDFRONT_API="$CLOUDFRONT_DOMAIN/api/hello"
echo "=== APIGW Dev"
curl -i $API_GW_DEV
echo ""
echo "=== APIGW Prod"
curl -i $API_GW_PROD
echo ""
echo "=== CloudFront"
curl -i $CLOUDFRONT_API
echo ""
=== APIGW Dev
HTTP/2 200
content-type: application/json
content-length: 94
date: Thu, 20 Nov 2025 13:25:03 GMT
x-amzn-trace-id: Root=1-691f16af-1c2cb66e199c3a7201f228a5;Parent=3f4e0d2bd1589363;Sampled=0;Lineage=1:ead6d511:0
x-amzn-requestid: b0fb7fd4-3f8d-4d9d-874e-606da9dfb580
x-amz-apigw-id: UWB7bEd7tjMEASw=
x-cache: Miss from cloudfront
via: 1.1 f485912663487526227b85e90a0da778.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P8
x-amz-cf-id: WM_TYKjW_PUGCaihw4gmQtlZiIiyyu-6EulE-DPdob_APP7IlIk_FA==
{"message":"Hello World from stage: dev","stage":"dev","timestamp":"2025-11-20T13:25:03.520Z"}
=== APIGW Prod
HTTP/2 200
content-type: application/json
content-length: 96
date: Thu, 20 Nov 2025 13:25:03 GMT
x-amzn-trace-id: Root=1-691f16af-779b85f24d645fe1431be29e;Parent=26711222eba96d30;Sampled=0;Lineage=1:ead6d511:0
x-amzn-requestid: 024f994d-c2dd-4869-b1aa-e4a226df2be8
x-amz-apigw-id: UWB7gHzttjMEhGA=
x-cache: Miss from cloudfront
via: 1.1 eb025597eaaccb791918dc400048d224.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-P8
x-amz-cf-id: ywKWU3YNS1n2nvySeY2FiMcIRslB-yRo2uLxin1SZO9ZzMbn3wphmQ==
{"message":"Hello World from stage: prod","stage":"prod","timestamp":"2025-11-20T13:25:03.693Z"}
=== CloudFront
HTTP/2 403
content-type: application/json
content-length: 23
x-amz-cf-pop: NRT12-P8
date: Thu, 20 Nov 2025 13:25:03 GMT
x-amz-apigw-id: UWB7jE4stjMEWeA=
x-amzn-requestid: c542973f-4d6e-4e29-bf15-dee501013801
x-amzn-errortype: ForbiddenException
via: 1.1 b6a7097997e2c9a80454aa70047f9342.cloudfront.net (CloudFront), 1.1 84116bff0a26d7866b2386043fce704c.cloudfront.net (CloudFront)
x-cache: Error from cloudfront
x-amz-cf-pop: NRT20-P3
x-amz-cf-id: hnTZm8BRPo2kttrhpFj6jnwfEuABzaJNo2mLPIujcVLOw-hIYsqkQQ==
{"message":"Forbidden"}
Terraform
クライアントリクエストを /prodにするパターン
CloudFront
# CloudFront Distribution
# -----------------------------
resource "aws_cloudfront_distribution" "main" {
enabled = true
comment = "test"
origin {
domain_name = replace(aws_api_gateway_stage.prod.invoke_url, "/^https?://([^/]*).*/", "$1")
origin_id = "api-gateway-origin"
origin_path = ""
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "api-gateway-origin"
viewer_protocol_policy = "redirect-to-https"
# CachingDisabled マネージドキャッシュポリシー
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"
# AllViewerExceptHostHeader マネージドオリジンリクエストポリシー
origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac"
}
ordered_cache_behavior {
path_pattern = "/prod/*"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "api-gateway-origin"
viewer_protocol_policy = "redirect-to-https"
# CachingDisabled マネージドキャッシュポリシー
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"
# AllViewerExceptHostHeader マネージドオリジンリクエストポリシー
origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
tags = {
Name = "${local.namespace}-cloudfront"
Namespace = local.namespace
}
}
API-GW
# API Gateway REST API
# -----------------------------
resource "aws_api_gateway_rest_api" "main" {
name = "${local.namespace}-api"
description = "test"
tags = {
Name = "${local.namespace}-api"
Namespace = local.namespace
}
}
# API Gateway Resources and Methods
# -----------------------------
resource "aws_api_gateway_resource" "hello" {
rest_api_id = aws_api_gateway_rest_api.main.id
parent_id = aws_api_gateway_rest_api.main.root_resource_id
path_part = "hello"
}
resource "aws_api_gateway_method" "hello_get" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.hello.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "hello_lambda" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.hello.id
http_method = aws_api_gateway_method.hello_get.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.hello.invoke_arn
}
# API Gateway Deployments and Stages
# 視認性悪いからモジュールにしたい
# -----------------------------
resource "aws_api_gateway_deployment" "main" {
rest_api_id = aws_api_gateway_rest_api.main.id
triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.hello.id,
aws_api_gateway_method.hello_get.id,
aws_api_gateway_integration.hello_lambda.id,
]))
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_api_gateway_method.hello_get,
aws_api_gateway_integration.hello_lambda,
]
}
resource "aws_api_gateway_stage" "prod" {
deployment_id = aws_api_gateway_deployment.main.id
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = "prod"
tags = {
Name = "${local.namespace}-api-prod"
Namespace = local.namespace
}
}
resource "aws_api_gateway_stage" "dev" {
deployment_id = aws_api_gateway_deployment.main.id
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = "dev"
tags = {
Name = "${local.namespace}-api-dev"
Namespace = local.namespace
}
}
# Lambda Permissions
# source arn ハードコードで良いのか微妙だが、一旦これで。
# -----------------------------
resource "aws_lambda_permission" "apigw_lambda_prod" {
statement_id = "AllowExecutionFromAPIGatewayProd"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.main.execution_arn}/prod/GET/hello"
}
resource "aws_lambda_permission" "apigw_lambda_dev" {
statement_id = "AllowExecutionFromAPIGatewayDev"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.main.execution_arn}/dev/GET/hello"
}
よく使うTerraformが散らかりまくっているので整理したいなぁ…(しない)
Discussion