- 1 §1: この記事について
- 2 §2: 前提と全体構成図
- 3 §3. ALB オリジン設計 — VPC Origins(新)× custom header(旧)対比
- 4 4. S3 sorry オリジン設計 — OAC による完全非公開化
- 5 §5. cache behavior 設計 — path pattern マッチング順序
- 6 §6. origin group フェイルオーバー — ALB 障害時の自動 sorry 切替
- 7 §7. Terraform で全体をコード化【クリティカルパス】
- 7.1 7-1. ディレクトリ構成
- 7.2 7-2. バージョン宣言
- 7.3 7-3. 変数定義(variables.tf)
- 7.4 7-4. tfvars 設計(terraform.tfvars)
- 7.5 7-5. ネットワーク基盤(VPC / Subnet / IGW / NAT)
- 7.6 7-6. ALB(internal)
- 7.7 7-7. S3 sorry バケット(OAC 経由のみアクセス可)
- 7.8 7-8. CloudFront VPC Origin(ALB ARN 参照)
- 7.9 7-9. CloudFront Distribution(全体像)
- 7.10 7-10. 出力定義(outputs.tf)
- 7.11 7-11. one-shot apply 手順
- 7.12 7-12. destroy 手順(削除順序が重要)
- 7.13 7-13. リソース一覧まとめ
- 8 §8. 動作確認 — curl / dig / Developer Tools
- 9 §9. コスト見積もり — 月額概算(2026-04 時点 ap-northeast-1 基準)
- 10 §10. まとめと第2弾予告
§1: この記事について
- Terraform 基礎入門 — VPC / EC2 / IAM の基本リソース理解
- GitHub Actions × OIDC × Terraform 複数人開発 CI/CD — 第2弾で使う PR→apply ワークフロー
- Terraform 1.9.x / hashicorp/aws ~> 5.0 の動作環境
- ALB・S3 静的ホスティング・CloudFront の概念
本シリーズの位置づけ:
- 本記事(第1弾): CDN 前段配信の 3 層構成基礎(CloudFront × ALB × S3 sorry)
- 第2弾: CloudFront Functions + IP allowlist によるメンテナンスモード切替運用
本記事では、CloudFront を ALB の前段に配置し、アプリケーションへのリクエストと sorry ページへのリクエストを 2 つのオリジンに振り分ける基礎構成を Terraform 1.9 で実装します。2024年11月に AWS が発表した CloudFront VPC Origins を canonical 採用し、パブリック IP を持たない internal ALB を CloudFront 専用の ingress として保護します。

本記事で実現すること
本ハンズオンを完了すると、次のゴール状態に到達します。
- viewer → CloudFront distribution → VPC Origins → private ALB → アプリ の経路が動作している
/sorry/*パスのリクエストが CloudFront → S3(OAC) 経由で sorry ページを返す- ALB はパブリック IP を持たず、VPC 外からは CloudFront 経由以外で到達不能
- S3 バケットは完全非公開で、CloudFront の OAC 署名を持つリクエストのみが許可される
- ALB 障害時は origin group が自動で sorry へフォールバックする
- 上記すべてが
terraform apply1 回で完成する Terraform コードとして IaC 化されている
この構成が動く状態からが、第2弾(CloudFront Functions × KeyValueStore によるメンテナンスモード切替)のスタートラインです。
CloudFront を初めて使う方へ
CloudFront は AWS のコンテンツデリバリーネットワーク(CDN)サービスです。distribution(ディストリビューション)と呼ばれる設定単位を作成し、そこにオリジン(コンテンツの源泉となるサーバ)を登録します。クライアントのリクエストは一番近いエッジロケーションで受け付けられ、キャッシュがあればそこから返答、なければオリジンに転送します。
本記事では ALB(Application Load Balancer)と S3 の 2 つをオリジンとして登録します。どちらのオリジンにリクエストを転送するかは cache behavior のパスパターンで決定します。/sorry/* → S3、それ以外 → ALB、という振り分けです。
CloudFront の設定変更は反映に数分〜10 分程度かかります。terraform apply 完了後、distribution のステータスが Deployed になるまで動作確認を待ってください。
AWS コンソールの CloudFront > Distributions でステータスをリアルタイムに確認できます。CLI では aws cloudfront get-distribution --id <distribution_id> の Status フィールドを参照してください。
なぜ ALB の前に CloudFront を置くのか
「ALB だけでいいのでは?」という疑問は自然です。CloudFront を前段に置く理由は主に 3 つあります。
理由1: エッジキャッシュによるレイテンシ削減とオリジン負荷軽減
CloudFront は世界 600 以上のエッジロケーションを持ちます。静的アセット(JS / CSS / 画像)を CloudFront キャッシュから返すことで、東京リージョンの ALB まで往復せずに済みます。ALB や EC2 / Fargate の処理負荷を下げながら、エンドユーザーの体感レイテンシも改善します。
キャッシュ TTL は cache behavior ごとに個別に設定できるため、API レスポンス(TTL = 0)と画像(TTL = 86400)を同一 distribution で柔軟に扱えます。
理由2: DDoS 対策と AWS WAF の連携拠点
CloudFront は AWS Shield Standard(無料)と統合されており、L3 / L4 の DDoS 攻撃を自動緩和します。さらに AWS WAF を distribution に紐付けると、IP レート制限・地理制限・SQLi / XSS フィルタリングを CDN 層で実施できます。
ALB に直接 WAF を適用するより CloudFront 層で先に遮断するほうがコスト効率が高く、悪意あるリクエストを ap-northeast-1 のオリジンまで到達させません。本シリーズではインフラ基礎として WAF 設定は省略しますが、設計上の接続点として理解しておくことが重要です。
理由3: sorry ページへの即時切替(第2弾への伏線)
CloudFront の cache behavior は path pattern ごとにオリジンを切り替えられます。/sorry/* を S3 に、それ以外を ALB に振り分けるだけで、「sorry ページを S3 から静的配信しながら、アプリケーション層には一切手を加えない」構成が成立します。
さらに第2弾では CloudFront Functions(viewer-request フック) を使い、メンテナンスモード中は allowlist 外の IP からのリクエスト URI を /sorry/index.html に書き換えます。この動的切替も ALB・アプリコードへの変更ゼロで実現できます。「アプリケーション層に触れずに CDN 前段だけで運用を制御できる」——それが CloudFront を前段に置く最大の価値です。
所要時間とコスト目安
| 区分 | 時間 |
|---|---|
| 初回(コード理解 + apply + 動作確認) | 60〜90 分 |
| 2 回目以降(手順確認済みの再構築) | 約 20 分 |
| 区分 | 費用目安 |
|---|---|
| 1 時間のハンズオン検証 | $0.05 未満 |
| 放置した場合の月額(常時稼働) | $17〜22(ALB 代が大半) |
ALB は起動しているだけで $0.0225/時 × 730 時間 ≒ $16.4/月 が発生します。ハンズオン後は必ず terraform destroy を実行してください。月額 $30 の AWS Budget アラートを設定しておくと、うっかり放置による予期せぬ課金を防げます。
コスト試算の詳細は §9 で AWS 公式 pricing page を参照しながら解説します。
ECS Blue/Green デプロイとの棲み分け
本シリーズと ECS Blue/Green デプロイ編はいずれも ALB を使いますが、切替の対象層が異なります。
| 項目 | ECS Blue/Green デプロイ | 本シリーズ(CloudFront 前段) |
|---|---|---|
| 切替対象 | ALB の Target Group(Green ↔ Blue のアプリコンテナ) | CloudFront の cache behavior(ALB オリジン ↔ S3 sorry オリジン) |
| 目的 | アプリケーションのバージョン切替(新旧コンテナの無停止入替) | CDN 前段でのオリジン振分・メンテナンスモード切替 |
| 実行層 | ALB / ECS レイヤ | CloudFront / CDN レイヤ |
| 反映速度 | 秒〜数十秒(TG ドレイン次第) | 数分〜10 分(CF distribution 反映) |
| Terraform リソース主役 | aws_codedeploy_deployment_group / aws_lb_listener_rule | aws_cloudfront_distribution / aws_cloudfront_vpc_origin |
両者は補完関係です。ECS Blue/Green でアプリバージョンを管理しつつ、CloudFront 前段でキャッシュ・DDoS 対策・メンテ切替を担う構成が実運用に近い形です。本記事は「CDN 前段層の設計」に集中し、ECS コンテナのデプロイ詳細は ECS 編に委ねます。
第2弾予告
本記事(第1弾)で構築する基礎インフラの上に、第2弾では CloudFront Functions × KeyValueStore による IP allowlist ベースのメンテナンスモード切替を実装します。
第2弾の主要トピック:
- CloudFront Functions(viewer-request)での IP 判定ロジック実装
- KeyValueStore を使った allowlist の動的管理(関数コード再デプロイ不要)
- Terraform tfvars の
maintenance_mode = true / false1 変数で切替 - GitHub Actions PR → plan → approve → apply の運用ワークフロー
git revertによるロールバック手順
第1弾の cache behavior 設計(§5)で「CloudFront Functions をどこに挿入するか」を意識しながら読み進めると、第2弾の理解がスムーズになります。
§2: 前提と全体構成図
前提環境
本ハンズオンを実施するには、以下の環境が整っていることを確認してください。
| ツール | 推奨バージョン | 備考 |
|---|---|---|
| Terraform | 1.9.x | hashicorp/aws ~> 5.60 以上を使用(VPC Origins 対応) |
| AWS CLI | v2 | aws configure 済みの IAM / SSO 設定 |
| Git | 2.x | 第2弾の PR 駆動ワークフローで使用 |
AWS プロバイダのバージョンに注意: aws_cloudfront_vpc_origin リソースは hashicorp/aws 5.60 以前では未サポートです。~> 5.60 を providers.tf に明記してください。
IAM 権限: CloudFront・ALB・EC2(VPC / Subnet / SG)・S3・IAM(OAC 用 policy)の Create / Describe / Delete が必要です。本ハンズオンでは AdministratorAccess を持つ IAM ロールまたはユーザーを推奨します(本番環境では最小権限に絞ること)。
GitHub リポジトリ(第2弾で使用): 第2弾では GitHub Actions × OIDC による terraform apply を実装します。CodeStar Connections による GitHub 連携を事前に設定しておくとスムーズです(第2弾開始時に案内します)。
全体アーキテクチャ概要
本ハンズオンで構築する構成は以下のとおりです。
┌────────────────────────────────────────────────────────┐
│ CloudFront Distribution │
viewer │ │
──── HTTPS ──────────>│ cache behavior 評価 │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ /sorry/*│ │ default (その他) │ │
│ │ → S3 Origin (OAC) │ │ → ALB Origin │ │
│ └──────────┬──────────┘ └──────────┬───────────┘ │
│ │ │ │
└────────────-│--------------------------│----------------┘
│ │
┌───────────▼──────┐ ┌────────────▼─────────────────┐
│ S3 バケット│ │ CloudFront VPC Origins │
│ (sorry HTML) │ │ (2024-11 新機能)│
│ OAC SigV4 検証 │ └────────────┬─────────────────┘
└──────────────────┘│ VPC 内部経由
┌──────────▼───────────┐
│ internal ALB│
│ (private subnet) │
└──────────┬────────────┘
│
┌──────────▼───────────┐
│ Fargate / EC2 アプリ │
└──────────────────────┘
ポイント:
– ALB は internal = true(パブリック IP なし・private subnet 配置)
– CloudFront VPC Origins が VPC 内の ALB と直接通信(ALB への公開インターネットアクセス不要)
– S3 バケットは public_access_block 全 true + OAC による SigV4 署名検証で保護

本記事で新規追加するリソース一覧
以下のリソースを Terraform で定義し、terraform apply 一度で完成させます。
| Terraform リソース | 役割 |
|---|---|
aws_vpc / aws_subnet / aws_internet_gateway / aws_nat_gateway | 基盤 VPC(public × 2 AZ / private × 2 AZ) |
aws_lb(internal = true) | private subnet に配置する内部 ALB |
aws_lb_target_group / aws_lb_listener | ALB のターゲットグループとリスナー(HTTP 80) |
aws_cloudfront_vpc_origin | CloudFront から VPC 内 ALB へのプライベート接続(2024-11 新機能) |
aws_s3_bucket / aws_s3_bucket_public_access_block | sorry ページ格納バケット(完全非公開) |
aws_s3_bucket_versioning | sorry コンテンツのバージョン管理(ロールバック用) |
aws_cloudfront_origin_access_control | S3 への SigV4 署名(OAC) |
aws_s3_bucket_policy | CloudFront サービスプリンシパルのみ許可する bucket policy |
aws_cloudfront_distribution | 2 オリジン(ALB / S3)+ 2 cache behavior の distribution |
aws_cloudfront_origin_group(オプション) | ALB 障害時の自動 S3 フォールバック |
第2弾で追加予定のリソース
第2弾では、以下のリソースを本記事の Terraform コードに追加します。
| Terraform リソース | 役割 |
|---|---|
aws_cloudfront_function | viewer-request で IP 判定・URI 書換を行う CloudFront Functions |
aws_cloudfront_key_value_store | allowlist と maintenance_mode を格納する KeyValueStore |
aws_cloudfrontkeyvaluestore_key | KVS の config キーに JSON 形式でデータを書き込む |
これらは本記事の aws_cloudfront_distribution に function_association として後から追加するため、第1弾のコードを大きく書き直す必要はありません。§5(cache behavior 設計)で「どこに CloudFront Functions を挿入するか」を意識して設計します。
§3. ALB オリジン設計 — VPC Origins(新)× custom header(旧)対比
本章では CloudFront のオリジンとして ALB を接続する 2 通りの方式を解説する。
2024 年 11 月に GA となった VPC Origins を canonical 実装として採用し、
従来の custom header + prefix list 方式との差異を明確化する。

§3-1. CloudFront VPC Origins — 採用理由
CloudFront VPC Origins は 2024 年 11 月に GA となった機能で、
VPC 内のプライベートサブネットに配置した ALB・NLB・EC2 インスタンスを
パブリック IP アドレスなしで CloudFront のオリジンとして使用できる。
公式情報
– What’s New: Amazon CloudFront VPC Origins (2024-11)
– 開発者ガイド: VPC Origins
– AWS Blog: Introducing Amazon CloudFront VPC Origins
本構成で VPC Origins を canonical 採用する 3 つの理由
| 理由 | 詳細 |
|---|---|
| パブリック IP 不要 | ALB を internal スキームにしてプライベートサブネット配置できる。EIP 費用・攻撃面がゼロ |
| 追加料金なし | VPC Origins は CloudFront の既存データ転送料金に含まれる。CF → ALB 間に追加コストは発生しない |
| SG ルール簡素化 | CloudFront managed prefix list の変更追随が不要。VPC CIDR からの inbound 1 ルールで完結 |
VPC Origins を使うと CloudFront が ALB の VPC に直接アクセスする専用 ingress を確立する。
ALB はインターネットに露出しないため「CloudFront 専用 ingress」として機能する。
§3-2. VPC Origins 実装(canonical)
全体リソース構成
aws_lb → internal ALB(private subnet)
aws_cloudfront_vpc_origin → VPC Origin(ALB ARN を参照)
aws_cloudfront_distribution → origin block で vpc_origin_config を指定
aws_security_group → ALB SG: inbound を VPC CIDR のみ許可
Terraform コード
ALB — internal スキーム + private subnet
resource "aws_lb" "main" {
name= "${var.prefix}-alb"
internal = true # パブリックIP不要
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets= var.private_subnet_ids
tags = {
Name = "${var.prefix}-alb"
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn= var.acm_certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
VPC Origin リソース
resource "aws_cloudfront_vpc_origin" "alb" {
vpc_origin_endpoint_config {
name = "${var.prefix}-vpc-origin-alb"
arn = aws_lb.main.arn
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols {
items = ["TLSv1.2"]
quantity = 1
}
}
tags = {
Name = "${var.prefix}-vpc-origin-alb"
}
}
CloudFront distribution — origin に vpc_origin_config を指定
resource "aws_cloudfront_distribution" "main" {
# ... 省略(§5 で cache behavior を詳述)...
origin {
domain_name = aws_lb.main.dns_name
origin_id= "alb-main"
vpc_origin_config {
vpc_origin_id= aws_cloudfront_vpc_origin.alb.id
origin_keepalive_timeout = 5
origin_read_timeout= 30
}
}
# S3 sorry オリジン・cache behavior は §4/§5 で詳述
enabled= true
is_ipv6_enabled = true
}
ALB セキュリティグループ — VPC 内からのみ inbound 許可
resource "aws_security_group" "alb" {
name = "${var.prefix}-alb-sg"
description = "ALB inbound from VPC only (CloudFront VPC Origins)"
vpc_id= var.vpc_id
ingress {
description = "HTTPS from VPC (CloudFront VPC Origins)"
from_port= 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
egress {
from_port= 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.prefix}-alb-sg"
}
}
ポイント: SG は VPC CIDR 1 ルールで完結
VPC Origins では CloudFront が VPC 内部 IP から ALB に接続するため、
SG の inbound は var.vpc_cidr からの許可 1 本で十分。
従来の custom header 方式で必要だった CloudFront managed prefix list の登録・更新が不要になる。
§3-3. legacy 方式(custom header + CF managed prefix list)— 比較節
概要と仕組み
VPC Origins 以前の標準的な実装では、以下の 2 層防御で ALB をインターネット直打ちから守っていた。
- CloudFront managed prefix list で ALB の SG inbound を CF の IP レンジのみに制限
- X-Origin-Verify custom header で CF 経由リクエストを証明し、ALB listener rule でヘッダー一致のみ転送
Terraform コード(legacy 参考実装・要点のみ)
# ALB は internet-facing(パブリック IP 必須)
resource "aws_lb" "main_legacy" {
internal = false
subnets= var.public_subnet_ids
# ...
}
# SG: CF managed prefix list からの inbound のみ許可
data "aws_ec2_managed_prefix_list" "cloudfront" {
name = "com.amazonaws.global.cloudfront.origin-facing"
}
resource "aws_security_group" "alb_legacy" {
ingress {
from_port = 443
to_port= 443
protocol = "tcp"
prefix_list_ids = [data.aws_ec2_managed_prefix_list.cloudfront.id]
}
}
# CloudFront: X-Origin-Verify custom header を付与
resource "aws_cloudfront_distribution" "main_legacy" {
origin {
custom_header {
name = "X-Origin-Verify"
value = var.origin_verify_secret
}
custom_origin_config {
origin_protocol_policy = "https-only"
origin_ssl_protocols= ["TLSv1.2"]
}
}
}
# ALB listener rule: ヘッダー一致のみ転送・不一致は 403
resource "aws_lb_listener_rule" "verify_header" {
condition {
http_header {
http_header_name = "X-Origin-Verify"
values = [var.origin_verify_secret]
}
}
}
VPC Origins が推奨される理由
| 比較軸 | VPC Origins(推奨) | custom header(legacy) |
|---|---|---|
| ALB スキーム | internal(private subnet) | internet-facing(public subnet) |
| パブリック IP | 不要 | 必要(EIP or ALB public DNS) |
| SG ルール | VPC CIDR 1 本 | CF managed prefix list(定期的に更新あり) |
| シークレット管理 | 不要 | X-Origin-Verify の値をローテーション必要 |
| 追加料金 | なし | なし(ただし EIP 費用発生) |
| 可用性リスク | 低い | prefix list の IP レンジ拡張時に SG 更新漏れリスク |
注意: legacy 方式は新規構築には使わない
custom header 方式は VPC Origins GA(2024-11)以前の標準実装。
新規構築では必ず VPC Origins を選択すること。
既存構成の移行手順は §7 で解説する。
§3-4. VPC Origins の運用制約
VPC Origins は便利だが、削除・変更時にいくつかの制約がある。
制約 1: distribution 関連付け中は削除不可
aws_cloudfront_vpc_origin は、CloudFront distribution に関連付けられている間は削除できない。terraform destroy 実行時に以下のエラーが発生する。
Error: deleting CloudFront VPC Origin: operation error CloudFront:
DeleteVpcOrigin, https response error StatusCode: 409,
VpcOriginInUse: The VPC origin is currently in use by a distribution.
正しい disassociate 手順:
# 1. distribution の origin block から vpc_origin_config を削除し apply
terraform apply -target=aws_cloudfront_distribution.main
# 2. VPC Origin が未参照になってから削除
terraform destroy -target=aws_cloudfront_vpc_origin.alb
制約 2: Terraform drift への対処
CloudFront distribution と VPC Origin は別リソースとして管理されるため、
コンソール側で直接変更すると Terraform の state と乖離(drift)が発生する。
# drift 検出・state 同期
terraform plan -target=aws_cloudfront_vpc_origin.alb
terraform refresh
運用 Tips: VPC Origin の名前変更は再作成を伴う
vpc_origin_endpoint_config.name を変更すると Terraform は VPC Origin を
destroy → create する。distribution 関連付け中に destroy が走るため、
名前変更時は -target で distribution を先に更新すること。
4. S3 sorry オリジン設計 — OAC による完全非公開化
4-1. なぜ OAI ではなく OAC か
CloudFront から S3 バケットへアクセスを制限する仕組みとして、旧来の OAI(Origin Access Identity) と現行の OAC(Origin Access Control) が存在します。2022年8月に OAC が正式リリースされて以降、AWS は OAC への移行を推奨しており、新規構成では OAC 一択です。
| 比較軸 | OAI(旧) | OAC(現行推奨) |
|---|---|---|
| 署名方式 | カスタム署名(S3 専用) | SigV4(AWS 標準署名) |
| SSE-KMS 対応 | ❌ 非対応 | ✅ 対応 |
| 全リージョンの S3 対応 | 一部リージョン制限あり | ✅ 全リージョン対応 |
| S3 Access Points 対応 | ❌ 非対応 | ✅ 対応 |
| IAM ポリシー記述 | CanonicalUser Principal | Service: cloudfront.amazonaws.com Principal(標準 IAM) |
OAC が有利な3点を端的にまとめます。
- SigV4 署名: AWS の標準署名プロセスを使うため、IAM ポリシーの
Conditionでaws:SourceArnによる配信ディストリビューション単位の絞り込みが可能。複数の CloudFront ディストリビューションが同一バケットにアクセスする環境でも、どのディストリビューションからのアクセスか識別できます。 - SSE-KMS 対応: S3 側の暗号化を KMS キーで管理している場合、OAI では CloudFront からの復号が不可でした。OAC は SigV4 を使うため KMS キーポリシーに CloudFront サービスを追加するだけで復号可能になります。
- 全リージョン対応: OAI は一部リージョン(ap-southeast-3 等)で制限がありましたが、OAC は全リージョンの S3 を対象にできます。
4-2. S3 バケットの完全非公開化
sorry ページを配信する S3 バケットは、CloudFront 経由のアクセス のみ を許可し、インターネットからの直接アクセスは完全遮断します。S3 の「Block Public Access」設定を4項目すべて true にすることが出発点です。
# sorryページ配信用 S3 バケット
resource "aws_s3_bucket" "sorry" {
bucket = "${var.project}-sorry-${var.env}"
tags = {
Name = "${var.project}-sorry-${var.env}"
Env = var.env
Purpose = "sorry-page"
}
}
# バケット公開アクセスの完全ブロック
resource "aws_s3_bucket_public_access_block" "sorry" {
bucket = aws_s3_bucket.sorry.id
block_public_acls = true
block_public_policy = true
ignore_public_acls= true
restrict_public_buckets = true
}
block_public_acls と ignore_public_acls は ACL ベースの公開設定を無効化し、block_public_policy と restrict_public_buckets はバケットポリシーによる公開を禁止します。4項目すべて true にしないとバケットポリシーで意図せず公開になるケースがあるため、必ず全項目を明示します。
4-3. S3 静的ホスティングを使わない理由
重要な落とし穴: S3 コンソールで「静的ウェブサイトホスティング」を有効化(Terraform では website {} ブロック)すると、エンドポイントが http://bucket.s3-website.region.amazonaws.com 形式になります。このエンドポイントは OAC に対応していません。
OAC を使うためには S3 の REST エンドポイント(bucket.s3.region.amazonaws.com)を使う必要があり、website {} ブロックは設定しません。その代わり、CloudFront の default_root_object または cache behavior の設定でルートパスへのリクエストを sorry/index.html へ解決します。
❌ 静的ホスティング有効 + OAC → 動作しない(website エンドポイントは OAC 非対応)
✅ 静的ホスティング無効(REST エンドポイント)+ OAC → 正常動作
4-4. OAC リソースの定義
OAC は CloudFront のリソースとして定義し、S3 バケットに紐付けます。
# Origin Access Control(OAC)
resource "aws_cloudfront_origin_access_control" "s3_sorry" {
name= "${var.project}-s3-sorry-oac-${var.env}"
description = "OAC for S3 sorry bucket"
origin_access_control_origin_type = "s3"
signing_behavior= "always"
signing_protocol= "sigv4"
}
signing_behavior の選択肢は always / no-override / never の3つです。always は CloudFront が常に SigV4 署名を付与するため、オリジン側に未署名リクエストが届きません。sorry ページは外部からの直接アクセスを遮断する用途なので always を推奨します。
4-5. S3 バケットポリシー — CloudFront サービスプリンシパルで絞り込む
OAC を使う場合のバケットポリシーは、Service: cloudfront.amazonaws.com を Principal に指定し、Condition で特定の CloudFront ディストリビューションからのリクエストのみ許可します。
# S3 バケットポリシー(OAC 経由のみ許可)
resource "aws_s3_bucket_policy" "sorry" {
bucket = aws_s3_bucket.sorry.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowCloudFrontServicePrincipal"
Effect = "Allow"
Principal = {
Service = "cloudfront.amazonaws.com"
}
Action= "s3:GetObject"
Resource = "${aws_s3_bucket.sorry.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.main.arn
}
}
}
]
})
depends_on = [aws_s3_bucket_public_access_block.sorry]
}
depends_on で aws_s3_bucket_public_access_block を指定する点が重要です。Block Public Access が有効になる前にポリシーを適用しようとすると、Public access block との競合でエラーになる場合があります。
AWS:SourceArn に CloudFront ディストリビューションの ARN を指定することで、同一アカウント内の 別のディストリビューションからのアクセスも拒否 できます。OAI の CanonicalUser では実現できなかった、ディストリビューション単位の細粒度な制御です。
4-6. sorry コンテンツの配置構成
sorry ページのファイル構成例を示します。CloudFront の cache behavior で /sorry/* に対するリクエストをこの S3 バケットにルーティングします(§5 で詳述)。
s3://your-project-sorry-prod/
└── sorry/
├── index.html← メンテナンス中ページ(HTML)
├── logo.png ← サービスロゴ画像
└── style.css ← スタイルシート(CDN 依存を排除)
Terraform での初期コンテンツアップロード(aws_s3_object リソース):
resource "aws_s3_object" "sorry_index" {
bucket = aws_s3_bucket.sorry.id
key = "sorry/index.html"
source = "${path.module}/sorry-contents/index.html"
content_type = "text/html; charset=utf-8"
etag= filemd5("${path.module}/sorry-contents/index.html")
}
resource "aws_s3_object" "sorry_logo" {
bucket = aws_s3_bucket.sorry.id
key = "sorry/logo.png"
source = "${path.module}/sorry-contents/logo.png"
content_type = "image/png"
etag= filemd5("${path.module}/sorry-contents/logo.png")
}
etag に filemd5() を指定すると、ファイル内容が変わった場合のみ S3 へアップロードされます。
4-7. S3 バージョニングとアクセスログの設定
バージョニング(sorry コンテンツのロールバック対応)
sorry ページを更新した後、意図しないデザイン崩れや誤字を発見した場合のロールバックに備えてバージョニングを有効化します。
resource "aws_s3_bucket_versioning" "sorry" {
bucket = aws_s3_bucket.sorry.id
versioning_configuration {
status = "Enabled"
}
}
バージョニング有効時は aws s3api list-object-versions で過去バージョンを確認し、aws s3api copy-object でロールバックできます。
サーバーアクセスログ(監査用)
sorry ページへのアクセス履歴を監査目的で保持します。ログは 別バケット に書き出す必要があります(ログバケット自身にログを書き込むと無限ループになるため)。
# ログ保存専用バケット
resource "aws_s3_bucket" "sorry_access_log" {
bucket = "${var.project}-sorry-access-log-${var.env}"
tags = {
Name = "${var.project}-sorry-access-log-${var.env}"
Purpose = "s3-access-log"
}
}
resource "aws_s3_bucket_public_access_block" "sorry_access_log" {
bucket= aws_s3_bucket.sorry_access_log.id
block_public_acls = true
block_public_policy = true
ignore_public_acls= true
restrict_public_buckets = true
}
# S3 サーバーアクセスログ有効化
resource "aws_s3_bucket_logging" "sorry" {
bucket = aws_s3_bucket.sorry.id
target_bucket = aws_s3_bucket.sorry_access_log.id
target_prefix = "sorry-access-logs/"
}
サーバーアクセスログは CloudFront のアクセスログ(§5 で設定)とは別物です。S3 サーバーアクセスログは「CloudFront がオリジン S3 へ行った GET リクエスト」、CloudFront アクセスログは「ビューワーが CloudFront へ行ったリクエスト」を記録します。両方を保持することで、エンドツーエンドのアクセス追跡が可能になります。

4-8. ここまでの構成まとめ
§4 で構築した S3 sorry オリジンの構成要素を整理します。
| リソース | 役割 |
|---|---|
aws_s3_bucket.sorry | sorry コンテンツ格納バケット(パブリックアクセス完全遮断) |
aws_s3_bucket_public_access_block.sorry | インターネット直接アクセス禁止(4項目 true) |
aws_cloudfront_origin_access_control.s3_sorry | OAC(SigV4 署名・always) |
aws_s3_bucket_policy.sorry | CloudFront サービスプリンシパル + SourceArn 条件での絞り込み |
aws_s3_bucket_versioning.sorry | コンテンツロールバック用バージョニング |
aws_s3_bucket_logging.sorry | 監査用サーバーアクセスログ(別バケットへ) |
次の §5 では、CloudFront の cache behavior を設定し、/sorry/* パターンのリクエストをこの S3 バケットへルーティングする構成を実装します。
§5. cache behavior 設計 — path pattern マッチング順序
CloudFront の cache behavior とは
CloudFront ディストリビューションには cache behavior という概念があり、リクエストの URL パスパターンに応じてどのオリジンへ転送するか、何をキャッシュするかを細かく制御できます。本記事では次の 2 つの behavior を設定します。
| behavior の種別 | path pattern | 転送先オリジン |
|---|---|---|
| ordered_cache_behavior #1 | /sorry/* | S3(sorry バケット) |
| default_cache_behavior | *(残り全て) | ALB(アプリケーション本体) |
この 2 behavior 構成が本記事のコアです。CloudFront は リクエスト URL を上から順に評価し、最初にマッチした behavior を採用します。default_cache_behavior は最後のフォールバックとして必ず 1 つ定義します。

5-1. path pattern マッチングの仕組み
CloudFront の path pattern マッチングには次のルールがあります。
- 評価順序:
ordered_cache_behaviorは配列の先頭から順に評価される - 最初のマッチ優先: 複数のパターンが当たる場合、インデックスが小さい方が採用される
- default は最後:
default_cache_behaviorは path pattern を持たず、どの ordered にもマッチしなかったリクエストを受け取る - 大文字小文字: デフォルトでは case-sensitive(
/Sorry/*≠/sorry/*)
本記事の場合、/sorry/index.html へのリクエストは ordered #1 でマッチして S3 へ転送され、/api/users や / はいずれも ordered にマッチしないため default(ALB)へ転送されます。
5-2. S3 sorry 向け ordered_cache_behavior
/sorry/* は静的な sorry ページ群を S3 から配信します。コンテンツが変わらない限りキャッシュし続けてよいため、AWS マネージドの CachingOptimized ポリシーを使います。
# modules/cloudfront-alb-s3-stack/main.tf(抜粋)
resource "aws_cloudfront_distribution" "main" {
# ... origins は §3/§4 で定義済み ...
# ─── ordered behavior #1: /sorry/* → S3 ───────────────────────────
ordered_cache_behavior {
path_pattern = "/sorry/*"
target_origin_id = "s3-sorry"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
viewer_protocol_policy = "redirect-to-https"
# CachingOptimized マネージドポリシー
# TTL: default 86400s / max 31536000s
# Gzip/Brotli 圧縮を自動有効化
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
# S3 向けは origin request policy 不要(OAC が署名を付与)
}
allowed_methods を ["GET", "HEAD"] のみに絞ることで、POST/PUT などの書き込みリクエストを S3 に転送しないようにします。S3 の sorry バケットは読み取り専用なので、これが正しい設定です。
CachingOptimized ポリシー(ID: 658327ea-f89d-4fab-a63d-7e88639e58f6)の特徴:
| 設定項目 | 値 |
|---|---|
| Default TTL | 86,400 秒(24 時間) |
| Max TTL | 31,536,000 秒(365 日) |
| Min TTL | 0 秒 |
| Gzip 圧縮 | 有効 |
| Brotli 圧縮 | 有効 |
| Cache key | URL のみ(Header/Cookie を含まない) |
sorry ページのような静的コンテンツでは、Header や Cookie をキャッシュキーに含める必要がないため CachingOptimized が最適です。
5-3. ALB 向け default_cache_behavior
アプリケーション本体は ALB 経由で動的コンテンツを返すため、キャッシュを無効化し、かつクライアントのリクエストをそのまま ALB へ転送する設定が必要です。
# ─── default behavior: 残り全て → ALB ────────────────────────────
default_cache_behavior {
target_origin_id = "alb-main"
# PUT/POST/DELETE を含む全 HTTP メソッドを許可
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
# キャッシュ対象は GET/HEAD/OPTIONS のみ(ただし CachingDisabled により実質キャッシュなし)
cached_methods = ["GET", "HEAD", "OPTIONS"]
viewer_protocol_policy = "redirect-to-https"
# CachingDisabled マネージドポリシー: TTL を 0 に強制・キャッシュを完全無効化
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"
# AllViewer オリジンリクエストポリシー:
# クライアントの全 Header / Cookie / QueryString を ALB へ転送
origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3"
# セキュリティヘッダーポリシー(次節で定義)
response_headers_policy_id = aws_cloudfront_response_headers_policy.security_headers.id
}
CachingDisabled ポリシー(ID: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad)の特徴:
| 設定項目 | 値 |
|---|---|
| Default TTL | 0 秒 |
| Max TTL | 0 秒 |
| Min TTL | 0 秒 |
| 実質動作 | 全リクエストをオリジンへ転送(キャッシュなし) |
AllViewer ポリシー(ID: 216adef6-5c7f-47e4-b989-5492eafa07d3)の特徴:
| 転送対象 | 動作 |
|---|---|
| HTTP メソッド | 全て転送 |
| Header | クライアントの全 Header を転送 |
| Cookie | 全 Cookie を転送 |
| QueryString | 全クエリストリングを転送 |
AllViewer を使うことで、Authorization ヘッダーや Session Cookie が ALB へそのまま届き、認証・セッション管理が正常に機能します。
5-4. セキュリティレスポンスヘッダーポリシー
ブラウザのセキュリティ強化のため、レスポンスに Security Header を付与します。CloudFront のレスポンスヘッダーポリシー機能を使い、アプリケーション側を変更せずに一括適用できます。
resource "aws_cloudfront_response_headers_policy" "security_headers" {
name = "${var.project_name}-${var.environment}-security-headers"
comment = "Security headers for ${var.project_name}"
security_headers_config {
# HTTPS 強制(1年間・サブドメイン含む)
strict_transport_security {
access_control_max_age_sec = 31536000
include_subdomains= true
preload = false
override = true
}
# MIME タイプスニッフィング防止
content_type_options {
override = true
}
# クリックジャッキング防止
frame_options {
frame_option = "DENY"
override = true
}
# XSS フィルター(レガシーブラウザ向け)
xss_protection {
mode_block = true
protection = true
override = true
}
# Referrer ポリシー
referrer_policy {
referrer_policy = "strict-origin-when-cross-origin"
override = true
}
}
# 基本的な CSP(本番環境では適宜厳格化)
custom_headers_config {
items {
header= "Content-Security-Policy"
value = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
override = false
}
}
}
override = true を設定すると、オリジン(ALB)が同名ヘッダーを返していた場合でも CloudFront 側の値で上書きします。false の場合はオリジンのヘッダーを優先します。CSP は override = false にすることで、アプリ側で独自 CSP を設定していれば ALB の値が優先される設計にしています。
5-5. §5 まとめと第2弾への伏線
本節で設定した 2 behavior 構成は「/sorry/* → S3(キャッシュあり・GET のみ)」「それ以外 → ALB(キャッシュなし・全メソッド・AllViewer 転送)」という 2 層構成です。
第2弾への伏線: この default_cache_behavior の target_origin_id を動的に切り替えることで、CF Functions を使ったメンテナンスモード切替を実現します。具体的には、CF Functions がリクエストを検査し「メンテ時間内」と判定した場合に target_origin_id を "s3-sorry" へ書き換えることで、ALB を無停止のまま sorry ページへ誘導します。この仕組みは第2弾で詳しく解説します。
§6. origin group フェイルオーバー — ALB 障害時の自動 sorry 切替
origin group とは
CloudFront の origin group は、プライマリオリジンとセカンダリオリジン(フェイルオーバー先)をペアにした仕組みです。プライマリが特定のステータスコードを返したとき、自動的にセカンダリへリトライします。
本記事での使い方は次のとおりです。
| 役割 | オリジン |
|---|---|
| プライマリ | ALB(アプリケーション本体) |
| セカンダリ(フェイルオーバー先) | S3 sorry バケット |
ALB が 5xx エラーを返した瞬間(サーバーダウン・デプロイ失敗・過負荷など)、CloudFront が S3 の sorry ページへ自動的に切り替えます。DNS 変更や手動操作は不要で、CloudFront 層だけで完結します。

6-1. フェイルオーバーの流れ
リクエストは通常どおり ALB(プライマリ)へ転送されます。ALB が failover_criteria.status_codes に一致するステータスコードを返すと、CloudFront は自動的にセカンダリ(S3 sorry)へリトライします。フェイルオーバーはクライアントには透過的で、クライアントは 503 を受け取らず、sorry ページを 200 で受け取ります。
6-2. aws_cloudfront_origin_group の定義
# modules/cloudfront-alb-s3-stack/main.tf(抜粋)
resource "aws_cloudfront_distribution" "main" {
# ... origins(alb-main / s3-sorry)は §3/§4 で定義済み ...
# ─── origin group: ALB 障害時に S3 sorry へ自動フォールバック ────
origin_group {
origin_id = "alb-with-sorry-failover"
# この status_codes を受け取ったとき、セカンダリへリトライ
failover_criteria {
status_codes = [500, 502, 503, 504]
}
# プライマリ: ALB(通常トラフィック)
member {
origin_id = "alb-main"
}
# セカンダリ: S3 sorry(フォールバック先)
member {
origin_id = "s3-sorry"
}
}
failover_criteria.status_codes に指定できるのは 500/502/503/504/403/404 です。本記事では ALB 障害を想定した 5xx 系 4 種を指定します。
| ステータスコード | 想定される原因 |
|---|---|
| 500 | ALB の背後のコンテナ・EC2 が内部エラーを返す |
| 502 | ALB が不正なレスポンスを受け取る(コンテナクラッシュ等) |
| 503 | ALB にヘルスチェックを通過したターゲットがない |
| 504 | ALB のタイムアウト(コンテナ応答遅延) |
6-3. default_cache_behavior に origin group を紐付ける
origin group を使うには、default_cache_behavior の target_origin_id を個別オリジンの ID ではなく origin group の ID に変更します。
# ─── default behavior: origin group 経由で ALB へ転送 ────────────
default_cache_behavior {
# 個別オリジン "alb-main" ではなく origin group を指定
target_origin_id = "alb-with-sorry-failover"
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD", "OPTIONS"]
viewer_protocol_policy = "redirect-to-https"
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # CachingDisabled
origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3" # AllViewer
response_headers_policy_id = aws_cloudfront_response_headers_policy.security_headers.id
}
target_origin_id = "alb-with-sorry-failover" に変えるだけで origin group が有効になります。ordered_cache_behavior(/sorry/* → S3)は origin group を経由せず、引き続き S3 へ直接転送されます。
6-4. 制約と注意事項
origin group が有効な behavior の制約:
origin group は default_cache_behavior のみ で有効です。ordered_cache_behavior に target_origin_id として origin group を指定することはできません(Terraform apply でエラーになります)。
本記事の設計では /sorry/* の ordered behavior は S3 に直接転送しているため、この制約に引っかかる場面はありません。ただし、複数のパスパターンでフェイルオーバーを実現したい場合は、Lambda@Edge または CloudFront Functions による制御が必要になります。
フェイルオーバーの遅延:
CloudFront がプライマリのエラーを検知してからセカンダリへリトライするまで、通常 1〜2 秒の追加レイテンシ が発生します。瞬断は避けられませんが、ALB から 503 がそのままクライアントに届くよりもはるかに良好なユーザー体験を提供できます。
オリジンのタイムアウト設定:
custom_origin_config の origin_read_timeout(デフォルト 30 秒)はフェイルオーバーの速度に直結します。本番環境では 10 秒程度に短縮し、障害検知を早めることを推奨します。
6-5. origin group vs. CF Functions — 使い分け
本記事で登場する 2 種類の sorry 切替手段の違いを整理します。
| 比較項目 | origin group(本節) | CF Functions(第2弾) |
|---|---|---|
| 切替トリガー | ALB の 5xx 応答を検知(自動) | Functions 内のロジックが判定(計画的) |
| 想定シナリオ | 障害・予期せぬダウン | 計画メンテナンス・デプロイウィンドウ |
| 切替速度 | 1〜2 秒(オリジンエラー検知後) | リクエスト処理時(ほぼ即時) |
| 追加コスト | なし | CF Functions の実行回数に応じた微額 |
| 切替操作 | 不要(自動) | KVS のフラグを更新(手動 or スクリプト) |
| 切戻し | 自動(ALB が回復すれば自動でプライマリへ) | KVS フラグをリセット |
実運用では両方を併用するのが一般的です。
- origin group: 常時有効にしてバックストップとして機能させる(ALB が突然落ちても sorry が出る)
- CF Functions: 計画メンテ時に KVS フラグを立てて全トラフィックを sorry へ誘導する
この 2 層構成により、障害時・計画時のどちらのシナリオでも sorry ページが確実に表示されます。
§6 まとめ
# ここまでの設定を整理すると:
# 1. origin group を定義
origin_group {
origin_id = "alb-with-sorry-failover"
failover_criteria { status_codes = [500, 502, 503, 504] }
member { origin_id = "alb-main" } # プライマリ
member { origin_id = "s3-sorry" } # フェイルオーバー先
}
# 2. default behavior の転送先を origin group に変更
default_cache_behavior {
target_origin_id = "alb-with-sorry-failover" # ←ここだけ変更
# ... 他の設定は §5 と同じ ...
}
# 3. /sorry/* は引き続き S3 直結(origin group を経由しない)
ordered_cache_behavior {
path_pattern = "/sorry/*"
target_origin_id = "s3-sorry"
# ...
}
次の §7 では、§3〜§6 で定義した全リソースを 1 つの Terraform モジュールに統合し、terraform apply 1 コマンドで環境全体を構築できるコードに仕上げます。
§7. Terraform で全体をコード化【クリティカルパス】
CloudFront × ALB(internal)× S3 sorry の全構成を Terraform でコード化し、terraform apply 一発で環境を再現できる状態にする。

7-1. ディレクトリ構成
modules/
└── cloudfront-alb-s3-stack/
├── main.tf # VPC / ALB / CloudFront / S3 全リソース
├── variables.tf # 入力変数定義
├── outputs.tf # distribution_domain_name 等
└── terraform.tfvars # 環境固有値
7-2. バージョン宣言
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
hashicorp/aws ~> 5.0(v5.31 以降)で aws_cloudfront_vpc_origin が GA となるため必須。
7-3. 変数定義(variables.tf)
variable "project_name" {
type = string
description = "全リソースの Name タグ prefix"
}
variable "environment" {
type = string
description = "dev / stg / prod"
validation {
condition = contains(["dev", "stg", "prod"], var.environment)
error_message = "dev / stg / prod のいずれかを指定"
}
}
variable "alb_container_port" {
type = number
default = 8080
}
variable "sorry_bucket_name_suffix" {
type = string
description = "S3 バケット名の suffix(AWS アカウント ID 推奨)"
}
variable "cloudfront_price_class" {
type = string
default = "PriceClass_200"
}
7-4. tfvars 設計(terraform.tfvars)
project_name = "cf-alb-sorry"
environment = "dev"
alb_container_port = 8080
sorry_bucket_name_suffix = "123456789012"# AWS アカウント ID
cloudfront_price_class= "PriceClass_200"
7-5. ネットワーク基盤(VPC / Subnet / IGW / NAT)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support= true
enable_dns_hostnames = true
tags = { Name = "${var.project_name}-vpc-${var.environment}" }
}
resource "aws_subnet" "public" {
count = 2
vpc_id= aws_vpc.main.id
cidr_block = "10.0.${count.index}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = { Name = "${var.project_name}-public-${count.index}" }
}
resource "aws_subnet" "private" {
count = 2
vpc_id= aws_vpc.main.id
cidr_block = "10.0.${count.index + 10}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = { Name = "${var.project_name}-private-${count.index}" }
}
data "aws_availability_zones" "available" { state = "available" }
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags= { Name = "${var.project_name}-igw" }
}
resource "aws_eip" "nat" { domain = "vpc" }
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = { Name = "${var.project_name}-nat" }
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route { cidr_block = "0.0.0.0/0"; gateway_id = aws_internet_gateway.main.id }
tags = { Name = "${var.project_name}-rtb-public" }
}
resource "aws_route_table_association" "public" {
count = 2
subnet_id= aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route { cidr_block = "0.0.0.0/0"; nat_gateway_id = aws_nat_gateway.main.id }
tags = { Name = "${var.project_name}-rtb-private" }
}
resource "aws_route_table_association" "private" {
count = 2
subnet_id= aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
7-6. ALB(internal)
ALB を internal = true にすることで VPC 外から直接到達できなくなる。CloudFront VPC Origin 経由のみが唯一の入口となる。
resource "aws_security_group" "alb" {
name= "${var.project_name}-alb-sg-${var.environment}"
vpc_id = aws_vpc.main.id
ingress {
from_port= 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
egress {
from_port= 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb" "main" {
name= "${var.project_name}-alb-${var.environment}"
internal = true
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets= aws_subnet.private[*].id
}
resource "aws_lb_target_group" "app" {
name = "${var.project_name}-tg-${var.environment}"
port = var.alb_container_port
protocol = "HTTP"
vpc_id= aws_vpc.main.id
target_type = "ip"
health_check {
path = "/health"
healthy_threshold= 2
unhealthy_threshold = 3
interval= 30
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn= aws_acm_certificate.alb.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
7-7. S3 sorry バケット(OAC 経由のみアクセス可)
resource "aws_s3_bucket" "sorry" {
bucket = "${var.project_name}-sorry-${var.sorry_bucket_name_suffix}-${var.environment}"
}
resource "aws_s3_bucket_public_access_block" "sorry" {
bucket= aws_s3_bucket.sorry.id
block_public_acls = true
block_public_policy = true
ignore_public_acls= true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "sorry" {
bucket = aws_s3_bucket.sorry.id
versioning_configuration { status = "Enabled" }
}
resource "aws_cloudfront_origin_access_control" "sorry" {
name= "${var.project_name}-oac-${var.environment}"
origin_access_control_origin_type = "s3"
signing_behavior= "always"
signing_protocol= "sigv4"
}
data "aws_iam_policy_document" "sorry_bucket" {
statement {
sid = "AllowCloudFrontOAC"
effect = "Allow"
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
actions= ["s3:GetObject"]
resources = ["${aws_s3_bucket.sorry.arn}/*"]
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values= [aws_cloudfront_distribution.main.arn]
}
}
}
resource "aws_s3_bucket_policy" "sorry" {
bucket = aws_s3_bucket.sorry.id
policy = data.aws_iam_policy_document.sorry_bucket.json
}
7-8. CloudFront VPC Origin(ALB ARN 参照)
resource "aws_cloudfront_vpc_origin" "alb" {
vpc_origin_endpoint_config {
name = "${var.project_name}-vpc-origin-${var.environment}"
arn = aws_lb.main.arn
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols {
items = ["TLSv1.2"]
quantity = 1
}
}
tags = { Name = "${var.project_name}-vpc-origin-${var.environment}" }
}
origin_protocol_policy = "https-only" で CF → ALB 間の通信を TLS で保護する。
7-9. CloudFront Distribution(全体像)
resource "aws_cloudfront_distribution" "main" {
enabled= true
is_ipv6_enabled = true
price_class = var.cloudfront_price_class
comment= "${var.project_name}-${var.environment}"
origin {
domain_name = aws_lb.main.dns_name
origin_id= "alb-main"
vpc_origin_config {
vpc_origin_id= aws_cloudfront_vpc_origin.alb.id
origin_keepalive_timeout = 5
origin_read_timeout= 30
}
}
origin {
domain_name = aws_s3_bucket.sorry.bucket_regional_domain_name
origin_id = "s3-sorry"
origin_access_control_id = aws_cloudfront_origin_access_control.sorry.id
}
origin_group {
origin_id = "alb-with-sorry-failover"
failover_criteria { status_codes = [500, 502, 503, 504] }
member { origin_id = "alb-main" }
member { origin_id = "s3-sorry" }
}
default_cache_behavior {
target_origin_id = "alb-with-sorry-failover"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods= ["GET", "HEAD"]
forwarded_values {
query_string = true
headers= ["Host", "Authorization"]
cookies { forward = "all" }
}
min_ttl = 0; default_ttl = 0; max_ttl = 0
}
ordered_cache_behavior {
path_pattern = "/sorry/*"
target_origin_id = "s3-sorry"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods= ["GET", "HEAD"]
forwarded_values {
query_string = false
cookies { forward = "none" }
}
min_ttl = 0; default_ttl = 86400; max_ttl = 31536000
}
viewer_certificate {
cloudfront_default_certificate = true
}
restrictions {
geo_restriction { restriction_type = "none" }
}
tags = { Name = "${var.project_name}-cf-${var.environment}" }
depends_on = [aws_cloudfront_vpc_origin.alb]
}
7-10. 出力定義(outputs.tf)
output "cloudfront_distribution_domain" {
value = aws_cloudfront_distribution.main.domain_name
description = "§8 動作確認で使用"
}
output "alb_dns_name" {
value = aws_lb.main.dns_name
}
output "sorry_bucket_name" {
value = aws_s3_bucket.sorry.bucket
}
output "cloudfront_vpc_origin_id" {
value = aws_cloudfront_vpc_origin.alb.id
description = "destroy 時の順序制御で参照"
}
7-11. one-shot apply 手順
cd modules/cloudfront-alb-s3-stack
terraform init
terraform plan -var-file="terraform.tfvars"
terraform apply -var-file="terraform.tfvars"
apply 完了後、cloudfront_distribution_domain 出力の URL で §8 の動作確認を行う。
7-12. destroy 手順(削除順序が重要)
CloudFront Distribution と VPC Origin に依存関係があるため、Distribution を先に無効化してから destroy する。
# ステップ1: Distribution の enabled を false に変更して apply
terraform apply -var-file="terraform.tfvars" -target=aws_cloudfront_distribution.main
# ステップ2: Status が Disabled になったことを確認
aws cloudfront get-distribution \
--id $(terraform output -raw cloudfront_distribution_id) \
--query "Distribution.Status"
# ステップ3: 全リソース削除
terraform destroy -var-file="terraform.tfvars"
destroy 前の確認コマンド:
aws cloudfront list-distributions \
--query "DistributionList.Items[*].{ID:Id,Status:Status,Enabled:Enabled}" \
--output table
注意:
terraform destroyで NAT Gateway・EIP も削除される。environment変数を必ず確認してから実行すること。
7-13. リソース一覧まとめ
| リソース | Terraform 型 | 役割 |
|---|---|---|
| VPC / Subnet × 4 | aws_vpc / aws_subnet | ネットワーク基盤 |
| IGW / NAT GW | aws_internet_gateway / aws_nat_gateway | パブリック・プライベート通信 |
| ALB | aws_lb | internal ロードバランサ |
| Target Group / Listener | aws_lb_target_group / aws_lb_listener | コンテナへのルーティング |
| VPC Origin | aws_cloudfront_vpc_origin | CF → ALB 内部通信 |
| S3 バケット + ブロック + バージョニング | aws_s3_bucket 系 | sorry ページ格納 |
| OAC + Bucket Policy | aws_cloudfront_origin_access_control | CF service principal のみ許可 |
| CF Distribution | aws_cloudfront_distribution | エッジ配信・フェイルオーバー制御 |
terraform apply 一発で全構成が完成する。次の §8 では CloudFront ドメイン経由の動作確認を行う。
§8. 動作確認 — curl / dig / Developer Tools
Terraform で構築した CloudFront × ALB × S3 sorry 構成が正しく動いているか、5つのシナリオで検証する。
8-1. 確認用の変数を設定する
CF_DOMAIN=$(terraform output -raw cloudfront_domain_name)
BUCKET_NAME=$(terraform output -raw sorry_bucket_name)
ALB_DNS=$(terraform output -raw alb_dns_name)
echo "CF_DOMAIN : ${CF_DOMAIN}"
echo "BUCKET : ${BUCKET_NAME}"
8-2. シナリオ1 — CloudFront 経由の ALB コンテンツ確認
Via と x-cache ヘッダーで CloudFront 経由かどうかを確認する。
curl -v "https://${CF_DOMAIN}/" 2>&1 | grep -E "^< (HTTP|Via|x-cache|x-amz-cf)"
期待するレスポンス:
< HTTP/2 200
< via: 1.1 abcdef1234567890.cloudfront.net (CloudFront)
< x-cache: Miss from cloudfront
< x-amz-cf-id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==
| ヘッダー | 確認ポイント |
|---|---|
via | cloudfront.net が含まれていれば CF 経由 |
x-cache | 初回は Miss from cloudfront(正常) |
x-amz-cf-id | CF が付与するリクエスト追跡 ID |
8-3. シナリオ2 — sorry ページ確認(S3 オリジン)
/sorry/ パスへのリクエストが S3 オリジンから返ることを確認する。
curl -v "https://${CF_DOMAIN}/sorry/" 2>&1 | grep -E "^< (HTTP|content-type|via|x-cache)"
期待するレスポンス:
< HTTP/2 200
< content-type: text/html
< via: 1.1 abcdef1234567890.cloudfront.net (CloudFront)
< x-cache: Miss from cloudfront
content-type: text/html と via: cloudfront.net の両方を確認すること。
8-4. シナリオ3 — ALB への直接 HTTPS 接続(VPC 外から到達不能)
ALB は internal ロードバランサーのため、VPC 外からは到達できないことを確認する。
curl -v --connect-timeout 10 "https://${ALB_DNS}/" 2>&1 | grep -E "connect|timed out|refused"
期待するレスポンス(いずれかが返れば OK):
* connect to xxx.xxx.xxx.xxx port 443 failed: Connection refused
# または
* Connection timed out after 10001 milliseconds
Connection refused または timed out が返れば、ALB が VPC 内にのみ公開されていることが確認できる。
8-5. シナリオ4 — S3 バケットへの anonymous HTTPS 接続(403 確認)
S3 バケットは OAC 経由でのみアクセス可能なため、匿名アクセスは 403 が返ることを確認する。
curl -s -o /dev/null -w "%{http_code}" \
"https://${BUCKET_NAME}.s3.ap-northeast-1.amazonaws.com/sorry/index.html"
期待するレスポンス: 403
# 応答本文も確認する場合
curl -s "https://${BUCKET_NAME}.s3.ap-northeast-1.amazonaws.com/sorry/index.html"
<Error><Code>AccessDenied</Code><Message>Access Denied</Message></Error>
AccessDenied が返れば OAC が正しく機能している証拠だ。
8-6. シナリオ5 — origin group フェイルオーバー発火確認
ALB security group の CloudFront 由来 inbound を一時的に deny し、S3 sorry ページへフェイルオーバーすることを確認する。
手順 1: ALB security group で CF インバウンドを一時的に削除
# modules/cloudfront-alb-s3-stack/main.tf
# aws_security_group_rule.alb_from_cloudfront の ingress 定義を一時コメントアウト
terraform apply -auto-approve
手順 2: フェイルオーバー確認
curl -v "https://${CF_DOMAIN}/" 2>&1 | grep -E "^< (HTTP|content-type|x-cache)"
# 期待: HTTP/2 200 + sorry コンテンツ(ALB 不応答 → S3 フェイルオーバー)
手順 3: security group を元に戻して正常復帰確認
terraform apply -auto-approve
curl -v "https://${CF_DOMAIN}/" 2>&1 | grep -E "^< (HTTP|via|x-cache)"
# 期待: HTTP/2 200 + via: cloudfront.net(ALB コンテンツに戻る)
8-7. ログ確認
CloudFront アクセスログ(S3 出力)
LOG_BUCKET=$(terraform output -raw cloudfront_log_bucket_name)
aws s3 ls "s3://${LOG_BUCKET}/" --recursive | sort | tail -5
aws s3 cp "s3://${LOG_BUCKET}/<ログファイル名>" /tmp/cf-access.log
head -3 /tmp/cf-access.log
主要フィールド: cs-uri-stem(リクエストパス)、sc-status(ステータスコード)、x-edge-result-type(Hit / Miss / Error)
ALB アクセスログ
ALB_LOG_BUCKET=$(terraform output -raw alb_log_bucket_name)
aws s3 ls "s3://${ALB_LOG_BUCKET}/" --recursive | sort | tail -5
8-8. よくあるエラー3種と切り分け
| エラー | 主な原因 | 確認 / 対処 |
|---|---|---|
403 AccessDenied | OAC の bucket policy 誤設定 / WAF ブロック | S3 バケットポリシーの Principal: cloudfront.amazonaws.com を確認。WAF / Geo restriction も確認 |
502 Bad Gateway | ALB target unhealthy / VPC Origin 接続失敗 | ECS タスクが RUNNING か確認。ALB security group で CloudFront マネージドプレフィックスリスト(pl-xxxxxxxxx)からの 443 inbound を許可しているか確認 |
504 Gateway Timeout | ALB origin response timeout / ECS 起動待ち | origin_response_timeout(デフォルト 30 秒)を延長。ECS タスクが RUNNING になるまで待つ |
§9. コスト見積もり — 月額概算(2026-04 時点 ap-northeast-1 基準)
本記事で構築した CloudFront × ALB × S3 構成の常時稼働月額と1 時間ハンズオンコストを試算する。
注: 以下の料金は AWS 公式料金ページ をもとに 2026-04 時点で算出。最新料金は必ず公式ページで確認すること。
9-1. サービス別料金内訳
CloudFront — CloudFront 料金
| 項目 | 単価(ap-northeast-1) | 試算 |
|---|---|---|
| データ転送(アウト) | 1TB/月まで 無料(Free Tier) | $0 |
| HTTPS リクエスト | $0.0100 / 10,000 req | ~$0(数千 req) |
| CloudFront VPC Origins | 追加料金なし(AWS 公式発表) | $0 |
ALB — ELB 料金
| 項目 | 単価 | 常時稼働 730h/月 |
|---|---|---|
| ALB 時間料金 | $0.0225 / 時 | $0.0225 × 730 ≈ $16.4 |
| LCU | $0.008 / LCU 時 | 低負荷: ~$1 |
S3 — S3 料金
| 項目 | 単価 | 試算 |
|---|---|---|
| ストレージ | $0.025 / GB / 月 | ~$0.001(HTML 数 KB) |
| GET リクエスト | $0.0004 / 1,000 req | ~$0 |
9-2. 合計試算
┌────────────────────────────────────────────────────────────┐
│ CloudFront × ALB × S3 — コスト試算 │
│ (2026-04 / ap-northeast-1 / AWS 公式料金ページ参照)│
├──────────────────────┬──────────────────┬──────────────────┤
│ サービス │ 常時稼働 / 月 │ 1時間ハンズオン│
├──────────────────────┼──────────────────┼──────────────────┤
│ CloudFront │ $0 〜 $1 │ $0.00 │
│ ALB│ $17 〜 $18│ $0.02 │
│ S3 sorry bucket│ < $0.01│ $0.00 │
│ VPC Origins 追加料金 │ $0 │ $0.00 │
├──────────────────────┼──────────────────┼──────────────────┤
│ 合 計 │ 約 $18 〜 $22│ 約 $0.04 │
└──────────────────────┴──────────────────┴──────────────────┘
月額コストの大半は ALB の固定費($16.4/月) だ。CloudFront VPC Origins は 2024 年の AWS 発表により追加料金なし。
9-3. 放置事故対策
terraform destroy 手順
ハンズオン終了後は必ず実行する。ALB を放置すると月 $17+ が継続課金される。
cd terraform/cloudfront-alb-s3-stack
# 削除前に S3 ログバケットを空にする(バケットが空でないと destroy が失敗する場合がある)
LOG_BUCKET=$(terraform output -raw cloudfront_log_bucket_name)
aws s3 rm "s3://${LOG_BUCKET}" --recursive
# 全リソースを削除
terraform destroy
# 削除完了を確認
terraform show# リソースが空になっていること
AWS Budgets アラート(月額 $30 閾値)
aws budgets create-budget \
--account-id "$(aws sts get-caller-identity --query Account --output text)" \
--budget '{
"BudgetName": "cloudfront-alb-s3-handson",
"BudgetLimit": {"Amount": "30", "Unit": "USD"},
"TimeUnit": "MONTHLY",
"BudgetType": "COST"
}' \
--notifications-with-subscribers '[{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [{"SubscriptionType": "EMAIL", "Address": "your-email@example.com"}]
}]'
月額 $30 の 80%($24)を超えたらメール通知。Address を自分のメールアドレスに変更して実行する。
コンソールでも設定可能: Billing → Budgets → Create budget → Cost budget
§10. まとめと第2弾予告
10-1. 本記事で構築した構成の全体像
本記事では、CloudFront を前段に配置した 3 層配信基盤を Terraform 1.9 で一から実装しました。
構築した主要コンポーネントをあらためて整理します。
| レイヤー | リソース | 役割 |
|---|---|---|
| CDN 前段 | aws_cloudfront_distribution | エッジキャッシュ・リクエストルーティング |
| ALB 接続 | aws_cloudfront_vpc_origin | VPC 内 private ALB への安全な経路 |
| アプリオリジン | aws_lb(internal = true)+ ターゲット | 実アプリへのリバースプロキシ |
| sorry オリジン | aws_s3_bucket + OAC | 静的エラーページの安全な配信 |
| 自動切替 | origin group + failover criteria | ALB 5xx 時の S3 sorry への自動 failover |
| IaC 管理 | Terraform 1.9 / hashicorp/aws ~> 5.0 | 全リソースのバージョン管理・再現性 |
この構成により、アプリ障害時に自動で sorry ページへ切替が完了する基盤が整いました。また、CloudFront 前段を置くことで、ALB への直アクセスを VPC Origin 経由に限定し、パブリック IP なしでの安全な配信を実現しています。
10-2. 本記事で身につくスキルの棚卸
第1弾で習得できる技術スキルを 8 要素に整理します。
1. CloudFront 前段配信アーキテクチャの設計判断
「なぜ ALB の前に CloudFront を置くのか」という設計判断の根拠(エッジキャッシュ性能・DDoS/WAF 連携・sorry 切替の一元管理)を実装を通して体得できます。単なる「CF を足せば速くなる」という理解を超え、3 層構成それぞれの責務分離が明確になります。
2. CloudFront VPC Origins(2024-11 新機能)による private ALB 保護
aws_cloudfront_vpc_origin を用いた VPC Origins は、パブリック IP なしで CloudFront と private ALB を直結する 2024 年 11 月の新機能です。従来の custom header + CF managed prefix list による実装と比較し、なぜ VPC Origins が canonical 構成として採用されるのかを理解できます。
3. custom header + CF managed prefix list による legacy 互換構成の理解
VPC Origins 対応前のレガシー構成の仕組みと、現在も legacy 環境で使われる理由を把握します。新旧を比較することで、CloudFront と ALB の接続パターンの全体観が身につきます。
4. S3 + Origin Access Control(OAC)による sorry ページ配信
OAC を使った S3 バケットポリシー設計を習得します。OAI(旧方式)との違い、HTTPS 強制、バケットポリシーの最小権限設計を通して、静的コンテンツの安全な配信パターンを理解できます。
5. cache behavior の path pattern 設計とマッチング順序
/error/* パスを S3 オリジンへルーティングし、/* でアプリへ振り分ける cache behavior の設計を習得します。優先度(precedence)による評価順序の理解は、複数オリジンを持つ CloudFront 構成の基礎です。
6. origin group による自動 failover
ALB が 5xx を返した際に S3 sorry へ自動で failover する origin group の仕組みを実装します。failover criteria(HTTP ステータスコード)の設計と、failover 時のリクエスト経路を Terraform コードと照合しながら理解できます。
7. Terraform 1.9 で全体を IaC 化する実装パターン
aws_cloudfront_distribution の全設定(origins / cache behaviors / viewer certificate / geo_restriction / logging)を Terraform で管理する実践パターンを習得します。plan → apply → destroy の一連のサイクルを通して、複数サービスが絡む IaC 設計の勘所が身につきます。
8. destroy 手順と料金管理
ハンズオン後の確実な後片付け手順(S3 バケット内オブジェクト削除 → terraform destroy)と、CloudFront の料金体系(リクエスト数・転送量課金・放置リスク)を理解します。「構築できるが壊し方がわからない」という状態を防ぎます。
10-3. 第1弾のカバー範囲と第2弾への橋渡し
第1弾でカバーした範囲を一言でまとめると、「CloudFront × ALB × S3 の 3 層構成基礎 + 自動 failover まで」です。
インフラを構築して動作確認できる状態にしましたが、メンテナンス時の切替運用はまだ手動です。たとえば「計画メンテ中はすべてのアクセスを sorry ページへ誘導したい」という要件に対して、現時点では origin group の failover criteria を手動で変更し terraform apply を打つしかありません。
次の第2弾では、CloudFront Functions と KeyValueStore を使い、IP アドレスに基づいて sorry ページと通常コンテンツを動的に振り分けるメンテナンス切替運用を実装します。tfvars の maintenance_mode フラグをひとつ変えて terraform apply を実行するだけで切替が完了する仕組みを構築し、さらに GitHub Actions の PR ワークフローと OIDC を組み合わせた PR 駆動 apply による承認フロー付きのメンテ切替まで実装します。
| 項目 | 第1弾(本記事) | 第2弾(次回) |
|---|---|---|
| メイン技術 | CF × ALB × S3 OAC | CF Functions × KeyValueStore |
| 切替方式 | origin group failover(自動・条件ベース) | IP allowlist + maintenance_mode フラグ(運用者制御) |
| IaC 管理 | terraform plan / apply / destroy 基礎 | PR 駆動 apply + git revert ロールバック |
| 対象者 | CDN 前段配信を初めて構築する方 | メンテ運用の自動化・安全な切替を実装したい方 |
10-4. 関連シリーズ
本記事の理解をさらに深めるために、以下のシリーズも参照してください。
基礎固め
– Terraform 基礎入門 — VPC / EC2 / IAM の基本リソース理解。CloudFront 構成を理解するための土台です。
CI/CD 連携
– GitHub Actions × OIDC × Terraform 複数人開発 CI/CD — 第2弾で使う PR→plan→apply ワークフローの基礎。OIDC 認証と branch 保護ルールの組み合わせを先に理解しておくと第2弾がスムーズです。
ECS + Blue/Green デプロイ
– ECS Blue/Green × CodeDeploy シリーズ — アプリ層のデプロイ戦略。本記事の CDN 前段層とは責務が異なりますが、「アプリ更新時に sorry ページを出さないための設計」として組み合わせると実践的な構成になります。
10-5. おわりに
CloudFront を前段に置くことの本質は、「エッジでの意思決定」を手に入れることです。
オリジンが何サーバあろうと、どんなパス設計であろうと、すべてのリクエストは最初に CloudFront のエッジを通過します。そのエッジで「どこへ届けるか」「何を返すか」「誰を通すか」を制御できれば、アプリ層の変更なしにメンテ切替・障害隔離・アクセス制御が実現できます。
第1弾では静的なルールに基づく振り分けと自動 failover を実装しました。第2弾では、そこに「動的な意思決定」を追加します。IP allowlist を KVS から読み取り、リクエストごとにメンテ中かどうかをリアルタイムで判断する CloudFront Functions の実装を通して、エッジコンピューティングの真価を体感してください。
10-6. 後片付けリマインダー
ハンズオン環境を放置すると CloudFront のリクエスト料金・ALB の時間課金・S3 のストレージ料金が継続的に発生します。検証が完了したら必ず以下の順序でリソースを削除してください。
# S3 バケット内のオブジェクトを先に削除(terraform destroy は空バケットのみ削除可能)
aws s3 rm s3://<your-sorry-bucket-name> --recursive
# 全リソースの削除
terraform destroy -var-file="terraform.tfvars"
terraform destroy 後、AWS コンソールで CloudFront distribution・ALB・S3 バケットが削除されていることを目視確認する習慣をつけると安心です。
- CloudFront 前段配信アーキテクチャの設計判断(なぜ CF を ALB 前に置くのか)
- CloudFront VPC Origins(2024-11 新機能)による private ALB 保護
- custom header + CF managed prefix list による legacy 互換構成の理解
- S3 + Origin Access Control(OAC)による sorry ページ配信
- cache behavior の path pattern 設計とマッチング順序
- origin group による自動 failover
- Terraform 1.9 で全体を IaC 化する実装パターン
- destroy 手順と料金管理
次の発展:
- 第2弾予告: CloudFront Functions × KeyValueStore で IP allowlist ベースのメンテナンスモード切替を実装