
ECS Gateway と CloudFront VPC オリジンの構成を CloudFormation と CLI で組んでみた
はじめに
2026年10月20日に Amazon ECS Gateway が発表されました。
ECS Gateway は、サービスごとのホスト名、証明書、リスナールールを持つ共有 ALB を作る機能です。パブリックとプライベートのゲートウェイをコンソールと CLI で作る手順は、前回の記事で紹介しています。
今回は、ECS Gateway が作る internal ALB を CloudFront の VPC オリジンにつないで Nginx を公開する構成を、CloudFormation で組みます。共有 ALB は最初のサービスを接続した時点で ECS Gateway が作るため、ALB の ARN をテンプレートから参照できません。そこで、ARN は CLI で取得し、CloudFormation のパラメータで渡しました。
この記事では、テンプレートに書けた範囲と CLI で補った箇所を整理します。
構成
東京リージョン(ap-northeast-1)と us-east-1 にまたがる7つのスタックを作成しました。

| スタック(リージョン) | 作った CloudFormation のリソースタイプ | CLI で補った値 |
|---|---|---|
| gwcf-network(ap-northeast-1) | AWS::EC2::VPC、AWS::EC2::InternetGateway、AWS::EC2::VPCGatewayAttachment、AWS::EC2::Subnet、AWS::EC2::NatGateway、AWS::EC2::RouteTable、AWS::EC2::Route、AWS::EC2::SubnetRouteTableAssociation、AWS::EC2::VPCEndpoint、AWS::EC2::SecurityGroup | なし |
| gwcf-gateway(ap-northeast-1) | AWS::IAM::Role、AWS::EC2::SecurityGroup、AWS::ECS::Cluster、AWS::ECS::Gateway、AWS::ECS::GatewayClusterAssociations | CloudFront のプレフィックスリストの ID、VPC オリジンの管理セキュリティグループの ID(VPC オリジンの作成後に更新で渡す) |
| gwcf-nginx(ap-northeast-1) | AWS::Logs::LogGroup、AWS::IAM::Role、AWS::ECS::TaskDefinition、AWS::ECS::Service | なし |
| gwcf-vpc-origin(us-east-1) | AWS::CloudFront::VpcOrigin | ALB の ARN |
| gwcf-cloudfront(us-east-1) | AWS::CloudFront::Distribution | serviceUrl のホスト名 |
| gwcf-alb-logs(ap-northeast-1) | AWS::S3::Bucket、AWS::S3::BucketPolicy、AWS::Logs::DeliverySource、AWS::Logs::DeliveryDestination、AWS::Logs::Delivery | ALB の ARN |
| gwcf-cf-logs(us-east-1) | gwcf-alb-logs と同じ5種類のリソースタイプ | なし |
- ゲートウェイのスタックは2段階で構成しました。ALB の受信元は、VPC オリジンの作成前は CloudFront のマネージドプレフィックスリストにし、作成後は CloudFront が作る管理セキュリティグループ
CloudFront-VPCOrigins-Service-SGに更新します。管理セキュリティグループにすると、自分の CloudFront ディストリビューションからの通信だけに絞れます。 - ALB の ARN が必要な VPC オリジンは、Nginx サービスのスタックの後に作成します。
- アクセスログ配信は、別スタックで追加しました。
CloudFormation で書けなかった範囲
ECS Gateway が作るリソース
ゲートウェイを作ってクラスターに関連付けた時点では、ALB はまだありません。describe-gateway-resources の出力では、hostSharedAlb が空です。
{
"gatewayResources": {
"gatewayArn": "<GATEWAY_ARN>",
"resources": {
"hostSharedAlb": []
}
}
}
最初のサービスを接続すると、ECS Gateway が ALB、リスナー、ターゲットグループ、証明書、リスナールール、サービス用セキュリティグループを作ります。AWS::ECS::Gateway と AWS::ECS::Service の GetAtt では、ALB などの ARN を取得できません。
接続後は、describe-service-revisions の ecsManagedResources が次のキーで ARN を返します。ALB とリスナーの ARN は describe-gateway-resources でも取れます。
"acmCertificateArn": "<ACM_CERTIFICATE_ARN>",
"loadBalancerArn": "<ALB_ARN>",
"listenerArn": "<LISTENER_ARN>",
"listenerRuleArn": "<LISTENER_RULE_ARN>",
"targetGroupArn": "<TARGET_GROUP_ARN>"
CLI で取得した値
CLI で取得してパラメータで渡した値は4つです。
| 値 | 渡す先 | CloudFormation での取得 |
|---|---|---|
| ALB の ARN | gwcf-vpc-origin と gwcf-alb-logs の AlbArn | AWS::ECS::Gateway の GetAtt で取れるのは AccessType、Arn、Configuration.HostSharedAlb.CurrentAlbCount、CreatedAt、Status、UpdatedAt のみ。AWS::ECS::Service の GetAtt で取れるのは Name と ServiceArn のみ |
| serviceUrl | gwcf-cloudfront の OriginDomainName | AWS::ECS::Service の GetAtt で取れるのは Name と ServiceArn のみ |
| VPC オリジンの管理セキュリティグループの ID | gwcf-gateway の VpcOriginSecurityGroupId | AWS::CloudFront::VpcOrigin の GetAtt で取れるのは AccountId、Arn、CreatedTime、Id、LastModifiedTime、Status のみ |
| CloudFront のプレフィックスリストの ID | gwcf-gateway の CloudFrontPrefixListId | AWS が管理するプレフィックスリスト(OwnerId が AWS) |
取得に使ったコマンドです。
# ALB の ARN(出力の loadBalancerArn)
aws ecs describe-gateway-resources --region ap-northeast-1 --gateway gwcf-gateway --output json
# サービスリビジョンの ARN(CLI ヘルプを基に記載。出力は保存していない)
aws ecs list-service-deployments --region ap-northeast-1 --service <SERVICE_ARN>
# serviceUrl
aws ecs describe-service-revisions --region ap-northeast-1 --service-revision-arns <SERVICE_REVISION_ARN> --query 'serviceRevisions[0].ecsManagedResources' --output json
# VPC オリジンの管理セキュリティグループの ID
aws ec2 describe-security-groups --region ap-northeast-1 --filters 'Name=group-name,Values=CloudFront-VPCOrigins-Service-SG*' --query 'SecurityGroups[].{name:GroupName,id:GroupId,desc:Description,vpc:VpcId,tags:Tags}' --output json
# CloudFront のプレフィックスリストの ID
aws ec2 describe-managed-prefix-lists --region ap-northeast-1 --filters 'Name=prefix-list-name,Values=com.amazonaws.global.cloudfront.origin-facing' --output json
各コマンドの出力のうち、使った行です。
"loadBalancerArn": "<ALB_ARN>",
"serviceUrl": "https://gw-<32桁のID>.ecs.ap-northeast-1.on.aws",
"id": "<VPC_ORIGIN_SG_ID>",
"PrefixListId": "<PREFIX_LIST_ID>",
describe-service-revisions に渡すリビジョン ARN は、list-service-deployments が返す targetServiceRevisionArn から取れます。
serviceUrl は https:// 付きで返るので、OriginDomainName には https:// を除いたホスト名を渡します。管理セキュリティグループは、グループ名だけで絞り込むと1件が返りました。VPC オリジンが複数ある環境では、Name=vpc-id,Values=<VPC_ID> も加えて絞ります。
スタック間の値の受け渡し
CLI で取得した4つ以外のスタック間の値は、Fn::GetStackOutput で渡しました。Region を指定すると、東京のスタックから us-east-1 のスタックの Output も参照できます。
| 参照するスタック | Output を持つスタック | Output |
|---|---|---|
| gwcf-gateway | gwcf-network | VpcId、PrivateSubnet1Id、PrivateSubnet2Id、PrivateSubnet3Id |
| gwcf-nginx | gwcf-network | PrivateSubnet1Id、PrivateSubnet2Id、PrivateSubnet3Id、TaskSecurityGroupId |
| gwcf-nginx | gwcf-gateway | ClusterName |
| gwcf-nginx | gwcf-cloudfront(us-east-1) | DistributionDomainName |
| gwcf-cloudfront | gwcf-vpc-origin | VpcOriginId |
| gwcf-cf-logs | gwcf-cloudfront | DistributionArn |
gwcf-cloudfront は gwcf-nginx のサービスの serviceUrl を必要とします。gwcf-nginx は gwcf-cloudfront のドメインを必要とします。2つのスタックは互いに依存しています。そこで gwcf-nginx は、最初は BaseUrl パラメータの既定値(プレースホルダー)で作りました。CloudFront の作成後に UseCloudFrontDomain を true にして更新し、DistributionDomainName を読ませました。
テンプレート
テンプレートは 01-network.yaml から 07-cf-logs.yaml の7ファイルで、次の順にデプロイしました。山括弧の値は、各コマンドの実行前に CLI で取得した値です。
aws cloudformation deploy --region ap-northeast-1 --stack-name gwcf-network --template-file 01-network.yaml --no-fail-on-empty-changeset
aws cloudformation deploy --region ap-northeast-1 --stack-name gwcf-gateway --template-file 02-gateway.yaml --capabilities CAPABILITY_NAMED_IAM --no-fail-on-empty-changeset --parameter-overrides CloudFrontPrefixListId=<PREFIX_LIST_ID>
aws cloudformation deploy --region ap-northeast-1 --stack-name gwcf-nginx --template-file 03-nginx.yaml --capabilities CAPABILITY_NAMED_IAM --no-fail-on-empty-changeset
aws cloudformation deploy --region us-east-1 --stack-name gwcf-vpc-origin --template-file 04-vpc-origin.yaml --no-fail-on-empty-changeset --parameter-overrides AlbArn=<ALB_ARN>
aws cloudformation deploy --region us-east-1 --stack-name gwcf-cloudfront --template-file 05-cloudfront.yaml --no-fail-on-empty-changeset --parameter-overrides OriginDomainName=<SERVICE_HOST>
aws cloudformation deploy --region ap-northeast-1 --stack-name gwcf-alb-logs --template-file 06-alb-logs.yaml --no-fail-on-empty-changeset --parameter-overrides AlbArn=<ALB_ARN>
aws cloudformation deploy --region us-east-1 --stack-name gwcf-cf-logs --template-file 07-cf-logs.yaml --no-fail-on-empty-changeset
ネットワークとゲートウェイ
プライベートサブネット3つの VPC と、internal で HTTPS(443)のゲートウェイを作ります。ゲートウェイの InboundSecurityGroup には、同じスタックで作ったセキュリティグループを指定します。VpcOriginSecurityGroupId が空のときはプレフィックスリストからの443を、値を渡したときは管理セキュリティグループからの443だけを許可する条件にしてあります。
01-network.yaml と 02-gateway.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Sample 1/7 network (ap-northeast-1). Minimal VPC for the ECS Gateway sample:
3 private subnets, IGW, one regional NAT gateway, S3 gateway endpoint, and a security group for the tasks.
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.20.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- { Key: Name, Value: gwcf-vpc }
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- { Key: Name, Value: gwcf-igw }
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref InternetGateway
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
AvailabilityZone: !Select [0, !GetAZs ""]
CidrBlock: 10.20.0.0/24
Tags:
- { Key: Name, Value: gwcf-private-1 }
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
AvailabilityZone: !Select [1, !GetAZs ""]
CidrBlock: 10.20.1.0/24
Tags:
- { Key: Name, Value: gwcf-private-2 }
PrivateSubnet3:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
AvailabilityZone: !Select [2, !GetAZs ""]
CidrBlock: 10.20.2.0/24
Tags:
- { Key: Name, Value: gwcf-private-3 }
# Regional NAT gateway (auto AZ expansion). Only used so that the tasks can pull the public ECR image.
NatGateway:
Type: AWS::EC2::NatGateway
DependsOn: InternetGatewayAttachment
Properties:
AvailabilityMode: regional
ConnectivityType: public
VpcId: !Ref Vpc
Tags:
- { Key: Name, Value: gwcf-natgw }
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- { Key: Name, Value: gwcf-private-rt }
PrivateDefaultRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet3RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet3
RouteTableId: !Ref PrivateRouteTable
S3GatewayEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Gateway
VpcId: !Ref Vpc
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
RouteTableIds:
- !Ref PrivateRouteTable
# Security group of the Nginx tasks. No inbound rule here.
TaskSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: gwcf-task-sg
GroupDescription: gwcf Nginx tasks
VpcId: !Ref Vpc
Tags:
- { Key: Name, Value: gwcf-task-sg }
Outputs:
VpcId:
Value: !Ref Vpc
PrivateSubnet1Id:
Value: !Ref PrivateSubnet1
PrivateSubnet2Id:
Value: !Ref PrivateSubnet2
PrivateSubnet3Id:
Value: !Ref PrivateSubnet3
TaskSecurityGroupId:
Value: !GetAtt TaskSecurityGroup.GroupId
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Sample 2/7 gateway (ap-northeast-1). ECS Gateway (internal ALB, HTTPS 443), the ALB security group, the cluster and
the gateway-cluster association. The ALB itself is created by ECS when the first service connects.
Parameters:
CloudFrontPrefixListId:
Type: String
Description: ID of com.amazonaws.global.cloudfront.origin-facing in this Region (taken with the AWS CLI).
VpcOriginSecurityGroupId:
Type: String
Default: ""
Description: >-
ID of CloudFront-VPCOrigins-Service-SG (taken with the AWS CLI after the VPC origin exists).
Empty = allow the CloudFront managed prefix list. Set = allow only this security group.
DeletionProtection:
Type: String
Default: "true"
AllowedValues: ["true", "false"]
Description: Deletion protection of the gateway. Set to "false" (stack update) before deleting the stack.
Conditions:
UseVpcOriginSecurityGroup: !Not [!Equals [!Ref VpcOriginSecurityGroupId, ""]]
Resources:
InfrastructureRole:
Type: AWS::IAM::Role
Properties:
RoleName: gwcf-ecs-gateway-infra-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { Service: ecs.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonECSInfrastructureRoleForGatewayHostSharedALB
# Inbound security group of the ALB (ours, so the gateway creates no inbound rules on it).
AlbSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: gwcf-gateway-alb-sg
GroupDescription: gwcf ECS Gateway ALB inbound (HTTPS 443 from CloudFront)
VpcId:
Fn::GetStackOutput:
StackName: gwcf-network
OutputName: VpcId
Region: ap-northeast-1
SecurityGroupIngress:
- !If
- UseVpcOriginSecurityGroup
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref VpcOriginSecurityGroupId
Description: HTTPS from the CloudFront VPC origin (managed SG)
- !Ref AWS::NoValue
- !If
- UseVpcOriginSecurityGroup
- !Ref AWS::NoValue
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourcePrefixListId: !Ref CloudFrontPrefixListId
Description: HTTPS from the CloudFront managed prefix list
Tags:
- { Key: Name, Value: gwcf-gateway-alb-sg }
Cluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: gwcf-cluster
Gateway:
Type: AWS::ECS::Gateway
DependsOn: InfrastructureRole
Properties:
GatewayName: gwcf-gateway
InfrastructureRoleArn: !GetAtt InfrastructureRole.Arn
DeletionProtection: !Ref DeletionProtection
PropagateTags: GATEWAY
Configuration:
HostSharedAlb:
Scheme: internal
Protocol: HTTPS
Port: 443
HttpDesync: defensive
MaxAlbCount: 2
NetworkConfiguration:
IpAddressType: ipv4
InboundSecurityGroup: !GetAtt AlbSecurityGroup.GroupId
Subnets:
- Fn::GetStackOutput:
StackName: gwcf-network
OutputName: PrivateSubnet1Id
Region: ap-northeast-1
- Fn::GetStackOutput:
StackName: gwcf-network
OutputName: PrivateSubnet2Id
Region: ap-northeast-1
- Fn::GetStackOutput:
StackName: gwcf-network
OutputName: PrivateSubnet3Id
Region: ap-northeast-1
Tags:
- { Key: Name, Value: gwcf-gateway }
GatewayAssociation:
Type: AWS::ECS::GatewayClusterAssociations
Properties:
Cluster: !Ref Cluster
Gateway: !GetAtt Gateway.Arn
Outputs:
GatewayArn:
Value: !GetAtt Gateway.Arn
ClusterName:
Value: !Ref Cluster
AlbSecurityGroupId:
Value: !GetAtt AlbSecurityGroup.GroupId
Nginx サービス
Nginx は Fargate サービスとして3つのプライベートサブネットに置きました。コンテナは起動時に環境変数を展開して設定ファイルを作ります。応答として、サービス名、Host 系のヘッダー、BASE_URL を返します。
ゲートウェイコネクタの accessTypes には PRIVATE を指定します。指定すると internal のゲートウェイにだけ接続します。省略した場合、後からインターネット向けのゲートウェイをクラスターに関連付けると、次のデプロイでサービスが公開されます。
タスク用セキュリティグループの gwcf-task-sg には、ALB からの受信ルールを書いていません。それでもデプロイは成功し、ターゲットは healthy になりました。タスクの ENI に、gwcf-task-sg に加えて ECS Gateway が作ったサービス用セキュリティグループも付いているためです。このセキュリティグループは、ECS Gateway が ALB に付けたセキュリティグループ(自前の gwcf-gateway-alb-sg とは別)からのポート 80 を許可しています。
03-nginx.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Sample 3/7 nginx (ap-northeast-1). Nginx Fargate service(s) connected to the internal ECS Gateway.
Nginx answers with the host name it sees and the BASE_URL environment variable.
Parameters:
BaseUrl:
Type: String
Default: https://example.invalid
Description: Public URL of the app (the CloudFront domain). A placeholder at first, replaced after CloudFront exists.
UseCloudFrontDomain:
Type: String
Default: "false"
AllowedValues: ["true", "false"]
Description: >-
"true" = BASE_URL is https:// + the CloudFront domain, read from the cloudfront stack (us-east-1) with Fn::GetStackOutput.
"false" = BASE_URL is the BaseUrl parameter.
Conditions:
UseCloudFrontDomainCondition: !Equals [!Ref UseCloudFrontDomain, "true"]
Resources:
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /ecs/gwcf-nginx
RetentionInDays: 3
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: gwcf-task-execution-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { Service: ecs-tasks.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# ---------------------------------------------------------------- service 1
TaskDefinition1:
Type: AWS::ECS::TaskDefinition
Properties:
Family: gwcf-nginx-1
RequiresCompatibilities: [FARGATE]
NetworkMode: awsvpc
Cpu: "256"
Memory: "512"
RuntimePlatform:
CpuArchitecture: X86_64
OperatingSystemFamily: LINUX
ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
ContainerDefinitions:
- Name: nginx
Image: public.ecr.aws/docker/library/nginx:stable
Essential: true
PortMappings:
- { Name: http, ContainerPort: 80, Protocol: tcp, AppProtocol: http }
Environment:
- { Name: SERVICE_NAME, Value: gwcf-nginx-1 }
- Name: BASE_URL
Value: !If
- UseCloudFrontDomainCondition
- !Join
- ""
- - https://
- Fn::GetStackOutput:
StackName: gwcf-cloudfront
OutputName: DistributionDomainName
Region: us-east-1
- !Ref BaseUrl
# The official image expands ${VAR} in /etc/nginx/templates/*.template at start-up.
EntryPoint: [/bin/sh, -c]
Command:
- |
mkdir -p /etc/nginx/templates
cat > /etc/nginx/templates/default.conf.template <<'EOF'
server {
listen 80;
location / {
default_type text/plain;
return 200 "service=${SERVICE_NAME}\nhost=$host\nhttp_host=$http_host\nx_forwarded_host=$http_x_forwarded_host\nx_forwarded_proto=$http_x_forwarded_proto\nx_forwarded_for=$http_x_forwarded_for\nBASE_URL=${BASE_URL}\n";
}
}
EOF
exec /docker-entrypoint.sh nginx -g 'daemon off;'
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: nginx1
Service1:
Type: AWS::ECS::Service
Properties:
ServiceName: gwcf-nginx-1
Cluster:
Fn::GetStackOutput:
StackName: gwcf-gateway
OutputName: ClusterName
Region: ap-northeast-1
TaskDefinition: !Ref TaskDefinition1
LaunchType: FARGATE
DesiredCount: 1
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- Fn::GetStackOutput:
StackName: gwcf-network
OutputName: TaskSecurityGroupId
Region: ap-northeast-1
Subnets:
- Fn::GetStackOutput:
StackName: gwcf-network
OutputName: PrivateSubnet1Id
Region: ap-northeast-1
- Fn::GetStackOutput:
StackName: gwcf-network
OutputName: PrivateSubnet2Id
Region: ap-northeast-1
- Fn::GetStackOutput:
StackName: gwcf-network
OutputName: PrivateSubnet3Id
Region: ap-northeast-1
GatewayConnectors:
- PortName: http
Filters:
AccessTypes: [PRIVATE]
HealthCheck:
HealthCheckPath: /
Outputs:
Service1Name:
Value: !GetAtt Service1.Name
VPC オリジンと CloudFront
04-vpc-origin.yaml は、ALB の ARN をパラメータで受け取ります。
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Sample 4/7 vpc-origin (us-east-1). CloudFront VPC origin that points at the internal ALB of the ECS Gateway.
The ALB ARN is not a CloudFormation attribute of the gateway or the service, so it is passed as a parameter.
Parameters:
AlbArn:
Type: String
Description: ARN of the ALB created by the ECS Gateway (taken with `aws ecs describe-gateway-resources`).
Resources:
VpcOrigin:
Type: AWS::CloudFront::VpcOrigin
Properties:
VpcOriginEndpointConfig:
Name: gwcf-alb
Arn: !Ref AlbArn
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: https-only
Tags:
- { Key: Name, Value: gwcf-vpc-origin }
Outputs:
VpcOriginId:
Value: !Ref VpcOrigin
05-cloudfront.yaml は、オリジンのドメイン名をパラメータで、VPC オリジンの ID を Fn::GetStackOutput で受け取ります。キャッシュポリシーにはキャッシュしない Managed-CachingDisabled を指定しました。オリジンリクエストポリシーには Host を転送しない Managed-AllViewerExceptHostHeader を指定しました。
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Sample 5/7 cloudfront (us-east-1). CloudFront distribution whose origin is the ECS Gateway service host name
(reached through the VPC origin). The Host header is not forwarded; nothing is cached. No log settings here.
Parameters:
OriginDomainName:
Type: String
Description: Host name of serviceUrl without https:// (taken with `aws ecs describe-service-revisions`).
OriginRequestPolicyId:
Type: String
# Managed-AllViewerExceptHostHeader (Host is not forwarded).
Default: b689b0a8-53d0-40ab-baf2-68738e2966ac
Resources:
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Comment: gwcf sample
HttpVersion: http2
IPV6Enabled: false
PriceClass: PriceClass_100
Origins:
- Id: gateway-alb
DomainName: !Ref OriginDomainName
VpcOriginConfig:
VpcOriginId:
Fn::GetStackOutput:
StackName: gwcf-vpc-origin
OutputName: VpcOriginId
Region: us-east-1
DefaultCacheBehavior:
TargetOriginId: gateway-alb
ViewerProtocolPolicy: redirect-to-https
AllowedMethods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
CachedMethods: [GET, HEAD]
Compress: true
# Managed-CachingDisabled
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
OriginRequestPolicyId: !Ref OriginRequestPolicyId
Tags:
- { Key: Name, Value: gwcf-cloudfront }
Outputs:
DistributionId:
Value: !Ref Distribution
DistributionDomainName:
Value: !GetAtt Distribution.DomainName
DistributionArn:
Value: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}
アクセスログ配信
ALB と CloudFront のアクセスログは、それぞれ別のスタックで S3 へ配信します。DeliverySource の ResourceArn には、ALB は CLI で取得した ARN を、CloudFront は Fn::GetStackOutput で取得した gwcf-cloudfront の DistributionArn を渡しています。
06-alb-logs.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Sample 6/7 alb-logs (ap-northeast-1). ALB access logs v2 (CloudWatch Logs vended logs delivery) to S3.
Added after the gateway exists; the ALB attributes (access_logs.s3.*) are not touched.
Parameters:
AlbArn:
Type: String
Description: ARN of the ALB created by the ECS Gateway.
Resources:
LogBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Delete
Properties:
BucketName: !Sub gwcf-alb-logs-${AWS::AccountId}-${AWS::Region}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
LifecycleConfiguration:
Rules:
- Id: expire-logs
Status: Enabled
ExpirationInDays: 7
LogBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref LogBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowLogDeliveryPutObject
Effect: Allow
Principal: { Service: delivery.logs.amazonaws.com }
Action: s3:PutObject
Resource: !Sub ${LogBucket.Arn}/*
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
ArnLike:
aws:SourceArn: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:delivery-source:*
- Sid: AllowLogDeliveryGetBucketAcl
Effect: Allow
Principal: { Service: delivery.logs.amazonaws.com }
Action: s3:GetBucketAcl
Resource: !GetAtt LogBucket.Arn
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
ArnLike:
aws:SourceArn: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:delivery-source:*
DeliverySource:
Type: AWS::Logs::DeliverySource
Properties:
Name: gwcf-alb-access-logs
ResourceArn: !Ref AlbArn
LogType: ALB_ACCESS_LOGS
DeliveryDestination:
Type: AWS::Logs::DeliveryDestination
DependsOn: LogBucketPolicy
Properties:
Name: gwcf-alb-logs-s3-json
DeliveryDestinationType: S3
OutputFormat: json
DestinationResourceArn: !GetAtt LogBucket.Arn
Delivery:
Type: AWS::Logs::Delivery
Properties:
DeliverySourceName: !Ref DeliverySource
DeliveryDestinationArn: !GetAtt DeliveryDestination.Arn
S3SuffixPath: "alb/{yyyy}/{MM}/{dd}"
Outputs:
BucketName:
Value: !Ref LogBucket
07-cf-logs.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: >-
Sample 7/7 cf-logs (us-east-1). CloudFront standard logging v2 to S3, added to an existing distribution
from a separate stack. The distribution ARN comes from the cloudfront stack through Fn::GetStackOutput.
Resources:
LogBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Delete
Properties:
BucketName: !Sub gwcf-cf-logs-${AWS::AccountId}-${AWS::Region}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
LifecycleConfiguration:
Rules:
- Id: expire-logs
Status: Enabled
ExpirationInDays: 7
LogBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref LogBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowLogDeliveryPutObject
Effect: Allow
Principal: { Service: delivery.logs.amazonaws.com }
Action: s3:PutObject
Resource: !Sub ${LogBucket.Arn}/*
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
ArnLike:
aws:SourceArn: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:delivery-source:*
- Sid: AllowLogDeliveryGetBucketAcl
Effect: Allow
Principal: { Service: delivery.logs.amazonaws.com }
Action: s3:GetBucketAcl
Resource: !GetAtt LogBucket.Arn
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
ArnLike:
aws:SourceArn: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:delivery-source:*
DeliverySource:
Type: AWS::Logs::DeliverySource
Properties:
Name: gwcf-cf-access-logs
ResourceArn:
Fn::GetStackOutput:
StackName: gwcf-cloudfront
OutputName: DistributionArn
Region: us-east-1
LogType: ACCESS_LOGS
DeliveryDestination:
Type: AWS::Logs::DeliveryDestination
DependsOn: LogBucketPolicy
Properties:
Name: gwcf-cf-logs-s3-json
DeliveryDestinationType: S3
OutputFormat: json
DestinationResourceArn: !GetAtt LogBucket.Arn
Delivery:
Type: AWS::Logs::Delivery
Properties:
DeliverySourceName: !Ref DeliverySource
DeliveryDestinationArn: !GetAtt DeliveryDestination.Arn
S3SuffixPath: "cf/{yyyy}/{MM}/{dd}"
Outputs:
BucketName:
Value: !Ref LogBucket
動作確認
CloudFront からの到達
プレフィックスリストからの通信を許可した段階で、CloudFront のドメインにリクエストを送ると、プライベートサブネットの Nginx から応答が返りました。
HTTP/2 200
content-type: text/plain
content-length: 279
date: Fri, 13 Nov 2026 16:30:39 GMT
server: nginx/1.30.6
x-cache: Miss from cloudfront
service=gwcf-nginx-1
host=gw-<32桁のID>.ecs.ap-northeast-1.on.aws
http_host=gw-<32桁のID>.ecs.ap-northeast-1.on.aws
x_forwarded_host=
x_forwarded_proto=https
x_forwarded_for=<client-ip>, <cloudfront-ip>
BASE_URL=https://example.invalid
受信元の絞り込み
プレフィックスリスト指定で疎通を確認した後、VpcOriginSecurityGroupId に管理セキュリティグループの ID を渡して gwcf-gateway を更新しました。更新後も CloudFront から疎通できました。
aws cloudformation deploy --region ap-northeast-1 --stack-name gwcf-gateway --template-file 02-gateway.yaml --capabilities CAPABILITY_NAMED_IAM --no-fail-on-empty-changeset --parameter-overrides CloudFrontPrefixListId=<PREFIX_LIST_ID> VpcOriginSecurityGroupId=<VPC_ORIGIN_SG_ID>
Host ヘッダーと BASE_URL
ECS Gateway の ALB はホスト名でサービスを振り分けるため、オリジンリクエストポリシーは Host を転送しない Managed-AllViewerExceptHostHeader にしています。そのため、アプリに届く host は CloudFront のドメインではなく、serviceUrl の on.aws のホスト名です。
画面遷移などで host を利用するアプリは、BASE_URL のような環境変数で公開 URL を渡す必要があります。
gwcf-nginx を UseCloudFrontDomain=true で更新し、https:// を付けた DistributionDomainName を ECS の環境変数 BASE_URL に渡すと、次の応答になりました。
service=gwcf-nginx-1
host=gw-<32桁のID>.ecs.ap-northeast-1.on.aws
http_host=gw-<32桁のID>.ecs.ap-northeast-1.on.aws
x_forwarded_host=
x_forwarded_proto=https
x_forwarded_for=<client-ip>, <cloudfront-ip>
BASE_URL=https://<CLOUDFRONT_DOMAIN>
まとめ
ECS Gateway、ECS サービス、VPC オリジン、CloudFront などを、CloudFormation のテンプレートで作成できました。
ECS Gateway の ALB はホスト名で振り分けるため、アプリ側の環境変数で BASE_URL(NUXT_APP_BASE_URL など)の設定が必要になる場合があります。それでも、共有 ALB をシンプルに実現できます。
マルチテナント環境などで、多数のサービスをなるべく少ない台数の ALB にまとめたい場合や、ALB を公開せず CloudFront 経由のアクセスだけに限定したい場合に、今回の構成をお試しください。
ECS Gateway の作成時点では存在しない ALB の ARN など、スタック間で参照できない値は、今回は CLI で取得して渡しました。Parameter Store やカスタムリソースを使う方法も、今後試してみたいと思います。









