NO IMAGE

CloudFront WAF Bot Control Rate Limit Lambda@Edge Terraform 実装

NO IMAGE
目次

1. Vol3 位置付けとアーキテクチャ全体像

Vol1+Vol2+Vol3 積層アーキテクチャ全体像

TL;DR: Vol1 で構築した CloudFront × ALB × S3 配信基盤と Vol2 の CFF メンテ切替運用に、AWS WAF v2 マネージドルール・Bot Control・Rate Limit v2・Lambda@Edge の防御 4 本柱を Terraform locals + for_each + dynamic で宣言的に追加する。初回 90〜120 分・追加月額 +$5〜10 でプロダクション最小防御構成が完成する。

Vol1 / Vol2 で達成したこと

Vol3 の防御層を追加する前に、Vol1/Vol2 で構築済みの基盤を振り返る。

Vol1 — CloudFront × ALB × S3 配信基盤(公開済)

  • CloudFront VPC Origins × ALB × S3 Sorry の 3 層構成を Terraform 1.9 で one-shot apply
  • ap-northeast-1 / us-east-1 の dual region 構成・Origin Group によるオリジンフェイルオーバー
  • hashicorp/aws ~> 5.60 ベースの provider 設計(Vol2/Vol3 と完全互換)

Vol2 — CloudFront Functions × KeyValueStore × IP allowlist × メンテ切替(公開済)

  • CloudFront Functions(CFF)+ KeyValueStore で IP allowlist によるメンテナンスモード切替を実装
  • PR → terraform plan → apply の CI/CD フローで tfvars を安全に更新
  • CFF viewer-request での軽量フィルタリング(処理時間 < 1ms・無料枠内)

Vol3 が解く課題

Vol1/Vol2 で完成した配信基盤は高可用・低コスト・運用自動化の観点では十分だが、攻撃防御の観点では素通し状態のままだ。プロダクション前夜で必ず直面する脅威を整理しよう。

脅威概要放置した場合の影響
DDoS / HTTP Flood大量リクエストによるサービス妨害オリジン負荷急増・コスト爆発
SQLi / XSS悪意あるペイロードによるインジェクション攻撃DB 破壊・セッションハイジャック
悪意ある Botスクレイパー・クレデンシャルスタッフィング・脆弱性スキャンコンテンツ盗用・不正ログイン
大量スクレイピング短時間集中アクセスによるコンテンツ根こそぎ収集帯域逼迫・著作権問題
不正地理アクセスコンプライアンス上許可できない地域からの接続規制違反・ライセンス違反

これら 5 脅威を 1 記事・1 terraform apply で一括対処するのが Vol3 の目的だ。

防御 4 本柱の役割分担

Vol3 では 4 層の防御を CloudFront の前段・内部に重ねる。各層は独立して機能し、組み合わせることで多層防御を実現する。

サービス役割主な対象脅威
1AWS WAF v2マネージドルール + カスタムルールでリクエスト内容を検査・遮断SQLi / XSS / 既知の不正ペイロード
2Bot ControlAWS 管理のシグネチャで既知 Bot を分類・遮断スクレイパー / クローラー / 不正自動ツール
3Rate Limit v2IP・ヘッダー・Cookie の複合キーで閾値超過リクエストを遮断HTTP Flood / DDoS / スクレイピング
4Lambda@Edgeviewer-request / origin-request で複雑な判定ロジックを実行JWT 検証 / A/B テスト / 動的画像処理

リクエストの評価順序は WAF → Bot Control → Rate Limit → Lambda@Edge の順だ。前段のルールがヒットした時点で後続評価をスキップするため、処理コストの高いロジックは後段に置くのが設計原則となる。1 リクエストの最悪レイテンシは CFF 1ms + WAF 10ms + Lambda@Edge 50ms = 計 61ms が目安だ。

【前提知識チェックリスト】

  • Vol1(CloudFront × ALB × S3 配信基盤編)読了・apply 経験あり
  • Vol2(CloudFront Functions × IP allowlist × メンテ切替編)読了・apply 経験あり
  • Terraform 1.9.x + hashicorp/aws ~> 5.60(Vol1/Vol2 と同一バージョン)
  • GitHub Actions OIDC による自動 plan/apply フロー経験あり
  • ap-northeast-1 リージョン既定(WAF WebACL は us-east-1 に別途作成)
  • AWS WAF / Bot Control / Lambda@Edge は名前を知るが未体験でも可

所要時間とハンズオンコスト

シナリオ所要時間追加コスト目安
初回ハンズオン(コンソール確認 → Terraform apply → destroy)90〜120 分$0.30
2 回目以降(ルール追加・閾値調整)30 分$0.05
常時稼働(月額・Vol1+Vol2+Vol3 合算追加分)+$5〜10/月

ハンズオン後は terraform destroy でリソースを撤去すること。Bot Control Common の $10/月は有効化時間に応じた日割り請求のため、検証後は即時削除を推奨する。

【コスト概算(2026-04 時点・AWS 公式 Pricing 参照)】

  • AWS WAF WebACL: $5.00/月(1 WebACL)
  • WAF マネージドルール: $1.00/ルール × 7 本 = $7.00/月
  • Bot Control Common: $10.00/月 + $1.00/100 万リクエスト
  • Lambda@Edge viewer-request: $0.60/100 万リクエスト + GB-sec 課金
  • Kinesis Firehose(WAF full log 転送): $0.029/GB
  • ハンズオン 1h の Vol1+Vol2+Vol3 合計: 約 $0.30
  • 月次 Budget アラート: $50 推奨(Bot Control Targeted 有効化時は要注意)

本記事の範囲と対象外

本記事のスコープを明確にする。Vol3 はプロダクション最小防御構成の追加に集中し、高度なオプション機能は意図的に対象外とした。

対象(本記事で実装する):
– AWS WAF v2 CLOUDFRONT scope(マネージドルール 5 群 + カスタムルール:geo / IP-set / size constraint)
– Bot Control Common モード(Targeted は対象外)
– Rate Limit v2(IP / ヘッダー / クエリ 複合キー・評価窓 60s〜600s 選択)
– Lambda@Edge(viewer-request: JWT 検証・A/B テスト振り分け / origin-response: 画像リサイズ)
– WAF full log → CloudWatch Logs Insights + Kinesis Firehose → S3 → Athena による可視化

対象外(本記事では扱わない):
– AWS Shield Advanced(DDoS 専用・月 $3,000〜の有料サービス)
– ACM 証明書の自動回転・HTTPS 詳細設定(Vol1 で完了済)
– Amazon Cognito 連携・OIDC IdP 統合
– Bot Control Targeted モード(月 $10/100 万リクエスト 課金リスクにより対象外)
– WAF Fraud Control(アカウント乗っ取り・詐欺検知の専用アドオン)

次節(§2)からは WAF v2 の実装を、コンソール操作と Terraform の二段構えで説明する。まずはコンソールで WebACL とルールの関係を視覚的に把握し、その後 locals + for_each + dynamic による宣言的実装へ移行する流れだ。

1-4. 推奨バージョンとプロバイダ構成

本記事のハンズオンは terraform 1.9.x / hashicorp/aws ~> 5.60 を前提とする。WAF v2 Rate-based rule v2 の複合キー(custom_keys)は provider 5.37.0 以降で GA 済、本記事サンプルが動作する最小版数だ。Vol1/Vol2 と同じ provider バージョンで統一しているため、required_providers を再宣言する必要はなく、Vol1 の main.tf に WAF/L@E 関連リソースを追記するだけで apply が通る。

CLOUDFRONT スコープの WAF WebACL と Lambda@Edge はいずれも us-east-1 固定のため、本記事では aws.us_east_1 という provider alias を追加し、Vol1 の ap-northeast-1 とは別インスタンスで宣言する。§2 冒頭で alias の定義例を示す。

【本記事の範囲】

  • WAF v2 マネージドルール 5 群(CommonRuleSet / KnownBadInputs / IpReputationList / AnonymousIpList / SQLiRuleSet)
  • Bot Control Common(Targeted は対象外・コスト理由)
  • Rate Limit v2・Lambda@Edge viewer-request / origin-response
  • WAF ログ → CloudWatch Logs Insights / Firehose → S3 → Athena の 2 経路
  • Shield Advanced・ACM 証明書回転・Cognito 連携は対象外

2. WAF v2 マネージドルール + カスタムルール(コンソール + Terraform)

WAF評価順序フロー図 マネージド5群+カスタム2群

AWS WAF v2 は CloudFront に直接アタッチできる Web Application Firewall です。scope=CLOUDFRONT を指定した WebACL は us-east-1 固定で作成する必要があります。本章では、マネージドルール 5 群とカスタムルール 3 本を Terraform の locals + for_each + dynamic で宣言的に管理する手順を解説します。

WAF v2 CLOUDFRONT scope は us-east-1 固定

  • ap-northeast-1 の provider では apply が失敗する(InvalidParameterException: scope
  • Terraform で provider = aws.us_east_1 alias を必ず設定すること
  • Vol1 main.tf に provider "aws" { alias = "us_east_1" region = "us-east-1" } を追記してから apply する

2-1. コンソール操作:WebACL 作成と CloudFront への紐付け

手順(所要 10〜15 分)

  1. WAF & Shield コンソール → Create web ACL → Resource type: Amazon CloudFront distributions(自動で us-east-1 に切替)
  2. Name: your-project-cf-waf / Default action: Allow
  3. Add managed rule groups で以下 5 群を追加し、各ルールの Override を Count に設定(初期観察用):
  4. Core rule set(AWSManagedRulesCommonRuleSet)
  5. Known bad inputs(AWSManagedRulesKnownBadInputsRuleSet)
  6. Amazon IP reputation list(AWSManagedRulesAmazonIpReputationList)
  7. Anonymous IP list(AWSManagedRulesAnonymousIpList)
  8. SQL database(AWSManagedRulesSQLiRuleSet)
  9. カスタムルール 2 本追加(2-3 参照)
  10. Add associated AWS resources → 対象の CloudFront distribution を選択 → Create web ACL
Count モード 1 週間運用を必ず行うこと

  • 初回は全マネージドルールを Count にして誤検知ログを 7 日分採取する
  • CloudWatch メトリクス CountedRequests を日次レビューし、誤検知ゼロを確認してから Block 昇格
  • AmazonIpReputationList と AnonymousIpList は即 Block でも誤検知リスクが低い

2-2. Terraform locals/for_each 設計

マネージドルール 5 群を locals の map で定義し、aws_wafv2_web_acl 内で dynamic "rule" ブロックとして展開します。ルール追加・削除は locals.waf_managed_rules への 1 エントリ操作で完結し、コードの重複を排除できます。

# terraform/waf.tf

terraform {
  required_providers {
 aws = { source = "hashicorp/aws"; version = "~> 5.60" }
  }
}
provider "aws" { alias = "us_east_1"; region = "us-east-1" }

locals {
  waf_managed_rules = {
 common  = { name = "AWSManagedRulesCommonRuleSet",  vendor_name = "AWS", priority = 10, override_action = "count" }
 bad_inputs = { name = "AWSManagedRulesKnownBadInputsRuleSet",vendor_name = "AWS", priority = 20, override_action = "count" }
 ip_reputation = { name = "AWSManagedRulesAmazonIpReputationList",  vendor_name = "AWS", priority = 30, override_action = "none"  }
 anonymous_ip  = { name = "AWSManagedRulesAnonymousIpList",vendor_name = "AWS", priority = 40, override_action = "none"  }
 sqli = { name = "AWSManagedRulesSQLiRuleSet", vendor_name = "AWS", priority = 50, override_action = "count" }
  }
}

resource "aws_wafv2_web_acl" "main" {
  provider = aws.us_east_1
  name  = "${var.project_name}-cf-waf"
  scope = "CLOUDFRONT"
  description = "Vol3 WAF: managed-5 + custom-3"

  default_action { allow {} }

  dynamic "rule" {
 for_each = local.waf_managed_rules
 content {
name  = rule.value.name
priority = rule.value.priority

override_action {
  dynamic "none"  { for_each = rule.value.override_action == "none"  ? [1] : []; content {} }
  dynamic "count" { for_each = rule.value.override_action == "count" ? [1] : []; content {} }
}

statement {
  managed_rule_group_statement {
 name  = rule.value.name
 vendor_name = rule.value.vendor_name
  }
}

visibility_config {
  cloudwatch_metrics_enabled = true
  metric_name = rule.key
  sampled_requests_enabled= true
}
 }
  }

  visibility_config {
 cloudwatch_metrics_enabled = true
 metric_name = "${var.project_name}-cf-waf"
 sampled_requests_enabled= true
  }

  tags = var.common_tags
}

# Vol1 aws_cloudfront_distribution.main に 1 行追記:
#web_acl_id = aws_wafv2_web_acl.main.arn

override_actiondynamic 分岐: none {}count {} は排他的ブロック型のため for_each = [1] : [] パターンで動的制御します(Terraform 1.9 + provider 5.60 確認済)。

2-3. カスタムルール 3 種

# aws_wafv2_web_acl.main の rule ブロックとして追加

# カスタムルール 1: リクエストボディ サイズ制限(8 KB 上限)
rule {
  name  = "BodySizeLimit"
  priority = 60
  action{ block {} }
  statement {
 size_constraint_statement {
comparison_operator = "GT"
size = 8192
field_to_match { body { oversize_handling = "CONTINUE" } }
text_transformation { priority = 0; type = "NONE" }
 }
  }
  visibility_config { cloudwatch_metrics_enabled = true; metric_name = "BodySizeLimit"; sampled_requests_enabled = true }
}
# カスタムルール 2: 地理 allowlist(JP/US のみ許可)
rule {
  name  = "GeoAllowlist"
  priority = 70
  action{ block {} }
  statement {
 not_statement {
statement { geo_match_statement { country_codes = var.allowed_countries } }
 }
  }
  visibility_config { cloudwatch_metrics_enabled = true; metric_name = "GeoAllowlist"; sampled_requests_enabled = true }
}
# カスタムルール 3: IP-set ブロック(悪意 IP の手動管理)
rule {
  name  = "IPSetBlock"
  priority = 80
  action{ block {} }
  statement { ip_set_reference_statement { arn = aws_wafv2_ip_set.blocklist.arn } }
  visibility_config { cloudwatch_metrics_enabled = true; metric_name = "IPSetBlock"; sampled_requests_enabled = true }
}

# IP ブロックリスト(tfvars で管理 → PR レビューで追加・削除)
resource "aws_wafv2_ip_set" "blocklist" {
  provider  = aws.us_east_1
  name= "${var.project_name}-blocklist"
  scope  = "CLOUDFRONT"
  ip_address_version = "IPV4"
  addresses = var.blocked_ips
  tags= var.common_tags
}

2-4. Count → Block 昇格運用

1 週間の誤検知分析後、locals.waf_managed_rulesoverride_action"count""none" に変更して terraform apply するだけで Block 昇格が完了します。

# CloudWatch Logs Insights で誤検知分析(WAF full log 有効化後に実行)
fields @timestamp, action, ruleGroupList.0.terminatingRule.ruleId, httpRequest.uri
| filter action = "COUNT"
| stats count(*) as cnt by ruleGroupList.0.terminatingRule.ruleId, httpRequest.uri
| sort cnt desc
| limit 20
# Block 昇格差分: locals の override_action を変更するだけ
locals {
  waf_managed_rules = {
 common  = { ..., override_action = "none" }# "count" → "none"
 bad_inputs = { ..., override_action = "none" }# "count" → "none"
 sqli = { ..., override_action = "none" }# "count" → "none"
 # ip_reputation / anonymous_ip は初期から "none" のため変更不要
  }
}

2-5. WCU 設計表(§3/§4 への WCU 予算配分)

WAF v2 WebACL の容量上限は 1500 WCU。§2 で採用したルール構成の消費量と残量を以下に示します。

ルール名種別WCUoverride_action
AWSManagedRulesCommonRuleSetマネージド700count → none
AWSManagedRulesKnownBadInputsRuleSetマネージド200count → none
AWSManagedRulesAmazonIpReputationListマネージド25none(即 Block)
AWSManagedRulesAnonymousIpListマネージド50none(即 Block)
AWSManagedRulesSQLiRuleSetマネージド200count → none
BodySizeLimit(size constraint)カスタム10block
GeoAllowlist(geo match)カスタム1block
IPSetBlock(IP-set reference)カスタム1block
§2 合計1187
§3/§4 向け残量313 WCU
WCU 残量: 313 / 1500(§3 Bot Control + §4 Rate Limit v2 に充当)

  • Bot Control Common は 50 WCU を追加消費(§3 で確定)
  • Rate-based rule v2(CUSTOM_KEYS 3 キー)は 2 WCU(§4 で確定)
  • §3/§4 適用後の見込み残量: 261 WCU(1500 WCU 上限に対して十分なマージン)
  • WCU が逼迫した場合は CommonRuleSet(700 WCU)を別 Rule Group に分離することを検討

apply 後の確認コマンド

# WebACL の WCU 消費量確認(us-east-1 固定)
aws wafv2 get-web-acl \
  --name your-project-cf-waf \
  --scope CLOUDFRONT \
  --id <WebACL-ID> \
  --region us-east-1 \
  --query 'WebACL.Capacity'

# CloudFront distribution への WebACL 紐付け確認
aws cloudfront get-distribution-config \
  --id <DISTRIBUTION-ID> \
  --query 'DistributionConfig.WebACLId'

apply 成功時のレスポンス例:

{ "Capacity": 1187 }

次章(§3)では Bot Control Common / Targeted の選択基準と Terraform 組込みを解説します。WCU 残量(313 WCU)のうち 50 WCU を充当します。

3. Bot Control 運用設計

Bot Control判定フロー Common/Targetedの差分

AWS WAF の Bot Control マネージドルールを使うと、検索エンジン・スクレイパー・スキャナー等の既知 Bot を自動判別してフィルタリングできる。本章ではスコープ選択・ルール override・Terraform 実装・運用観察まで一気通貫で解説する。

3-1. Common vs Targeted — スコープ選択の判断基準

Bot Control には 2 つのスコープがある。

項目CommonTargeted
判定方式静的シグネチャ(UA・IP・ヘッダー照合)動的チャレンジ(CAPTCHA / JavaScript challenge)
料金$1/100万リクエスト + $10/月$10/100万リクエスト + $10/月
誤検知リスク低(シグネチャ固定)中(challenge 失敗で正常ユーザーも影響)
対応 Bot 種別既知 Bot(静的署名で識別可能)ヘッドレスブラウザ・動的回避型高度 Bot
WCU 消費25 WCU50 WCU
推奨用途まず導入・コスト最小高度 Bot 被害が確認済みの場合のみ移行

推奨: まず Common から始め、CloudWatch メトリクスで Bot 量と種別を 2 週間観察してから Targeted への移行を検討する。

【Bot Control Targetedのコスト警告】

Targeted有効化で月$2,600+になる場合があります。まずCommonから始めることを推奨します。

料金換算の例: リクエスト 2 億件/月のサービスで Targeted を有効化すると、$10 × 200 + $10 = $2,010/月 の Bot Control 費用が加算される。WAF リクエスト費用($0.60/1M × 200M = $120)と合算すると月額 $2,130+ になる。

3-2. Bot Control ルール名と個別ルール override

マネージドルールグループ名は固定値 AWSManagedRulesBotControlRuleSet。スコープを inspection_level パラメータで指定する。

# waf-bot-control.tf — Bot Control マネージドルールグループ追加
resource "aws_wafv2_web_acl" "main" {
  # §2 の WebACL の設定を継承し、以下の rule ブロックを追加する

  rule {
 name  = "BotControlCommon"
 priority = 60  # §2 の managed rules(priority 10-50)の後に配置

 override_action {
count {}  # 初期は Count モードで観察・2 週間後に none へ昇格
 }

 statement {
managed_rule_group_statement {
  name  = "AWSManagedRulesBotControlRuleSet"
  vendor_name = "AWS"

  managed_rule_group_configs {
 aws_managed_rules_bot_control_rule_set {
inspection_level = "COMMON"  # COMMON / TARGETED
 }
  }

  # 検索エンジン Bot(Googlebot 等)は allow に override
  rule_action_override {
 action_to_use { allow {} }
 name = "CategorySearchEngine"
  }

  # スクレイピングフレームワーク(Scrapy 等)は明示的に block
  rule_action_override {
 action_to_use { block {} }
 name = "CategoryScrapingFramework"
  }
}
 }

 visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "BotControlCommon"
sampled_requests_enabled= true
 }
  }
}

主要なルール名と推奨 action を確認しておく。

ルール名対象 Bot 種別推奨 action
CategorySearchEngineGooglebot / Bingbot 等の正規検索エンジンallow
CategoryMonitoringDatadog / UptimeRobot 等の監視 Botallow(自社 Bot 確認済みなら)
CategoryScrapingFrameworkScrapy / Playwright headless 等block
CategoryLinkCheckerSEO ツールのリンクチェッカーcount(観察後判断)
CategoryHttpLibrarycurl / python-requests / axios 等count(API クライアントに混在可能性)
CategorySocialMediaTwitterbot / Slackbot 等の OGP 取得 Botallow

3-3. Challenge / CAPTCHA アクション

block の代替として challenge / captcha action が使用できる。

# Targeted スコープで challenge action を使う例
rule_action_override {
  action_to_use {
 challenge {}  # JavaScript パズル(自動 Bot 排除・人間は透過)
  }
  name = "CategoryScrapingFramework"
}

# 人間確認が必要な高リスクエンドポイント向け captcha action
rule_action_override {
  action_to_use {
 captcha {}  # 画像認証(人間に表示・Targeted scope のみ使用可)
  }
  name = "SignalAutomatedBrowser"
}
アクション仕組みユーザー体験への影響推奨用途
challengeJavaScript パズル(透過的)ほぼなし(50-100ms 遅延)自動 Bot 排除の第一段階
captcha画像認証(人間に表示)あり(認証操作が必要)会員登録・決済等の高価値 EP
block即時 403 返却ブロックページ表示悪意明確な Bot(明確根拠がある場合)

3-4. Bot フィンガープリント — §4 Rate Limit への橋渡し

Bot Control は判定根拠として以下のフィンガープリント情報を収集する。これらは §4 の Rate-based rule v2 の aggregate key に直接活用できる。

フィンガープリント§4 Rate Limit での活用
JA3 hash(TLS Client Hello の cipher suite 組合せ)aggregate_key_type = "JA3" で同一 TLS プロファイルのバースト検知
User-Agent(HTTP ヘッダー・偽装可・補助的使用)aggregate_key_type = "HEADER" + name = "User-Agent"
IP アドレス(NAT/CDN 越えは X-Forwarded-For)aggregate_key_type = "IP" (最も基本)
{
  "terminatingRuleId": "BotControlCommon",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "action": "COUNT",
  "labels": [
 { "name": "awswaf:managed:aws:bot-control:bot:category:scraping_framework" },
 { "name": "awswaf:managed:aws:bot-control:bot:name:scrapy" },
 { "name": "awswaf:managed:aws:bot-control:signal:known_bot_data_center_ip" }
  ],
  "httpRequest": {
 "clientIp": "203.0.113.45",
 "headers": [
{ "name": "user-agent", "value": "Scrapy/2.11 (+https://scrapy.org)" }
 ]
  }
}

§4 との接続: ラベル awswaf:managed:aws:bot-control:bot:category:scraping_framework が付いたリクエストが JA3 hash A(特定ヘッドレスブラウザ固有値)で大量発生している場合、§4 の Rate-based rule v2 で JA3 を aggregate key に加えることで、Bot Control をすり抜けた高度 Bot も閾値超過で block できる。詳細は §4 で解説する。

3-5. Terraform 組込 — locals + dynamic への統合

§2 の aws_wafv2_web_acl に Bot Control 設定を locals として分離して管理する。managed_rule_group_configs が必要なため、汎用の waf_managed_rules map とは別ブロックで定義する。

# locals.tf — Bot Control を専用 local で管理
locals {
  bot_control_config = {
 enabled = true
 inspection_level = "COMMON"  # "COMMON" / "TARGETED" — 切替は変数化推奨
 priority= 60
 override_action  = "count"# 初期観察期間は count / 本番移行後は none
 search_engine_allow = true# CategorySearchEngine を allow override
  }
}
# waf-bot-control.tf — Bot Control ルール dynamic 展開
# aws_wafv2_web_acl.main の rule ブロックとして追加(§2 の WebACL を拡張)

dynamic "rule" {
  for_each = local.bot_control_config.enabled ? [local.bot_control_config] : []
  content {
 name  = "BotControl-${rule.value.inspection_level}"
 priority = rule.value.priority

 override_action {
dynamic "count" {
  for_each = rule.value.override_action == "count" ? [1] : []
  content {}
}
dynamic "none" {
  for_each = rule.value.override_action == "none" ? [1] : []
  content {}
}
 }

 statement {
managed_rule_group_statement {
  name  = "AWSManagedRulesBotControlRuleSet"
  vendor_name = "AWS"

  managed_rule_group_configs {
 aws_managed_rules_bot_control_rule_set {
inspection_level = rule.value.inspection_level
 }
  }

  dynamic "rule_action_override" {
 for_each = rule.value.search_engine_allow ? [1] : []
 content {
action_to_use { allow {} }
name = "CategorySearchEngine"
 }
  }
}
 }

 visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "BotControl-${rule.value.inspection_level}"
sampled_requests_enabled= true
 }
  }
}

WCU 注意: Common = 25 WCU・Targeted = 50 WCU。§2 の managed rules 5 群と合算して 1500 WCU 上限内に収まるか確認する。超過時は不要なマネージドルールを削除して予算確保する(CloudFront に紐付けられる WebACL は 1 つのみ)。

3-6. 運用観察 — CloudWatch サンプリングとログ分析

sampled_requests_enabled = true は全ルールの visibility_config で有効化済み(前節 Terraform 参照)。これにより CloudWatch に 5 分粒度でサンプルリクエストが記録される。

CloudWatch Logs Insights でBot Control のカウント数を確認するクエリ:

fields @timestamp, terminatingRuleId, action, httpRequest.clientIp
| filter terminatingRuleId like /BotControl/
| filter action = "COUNT" or action = "BLOCK"
| stats count() as requests by action, bin(5m)
| sort @timestamp desc
| limit 100

Count → Block 昇格の判断フロー: (1) Week 1-2 Count で稼働し sampled_requests から主要カテゴリと送信元 IP を特定。(2) Week 3 に CategorySearchEngine の誤検知がないことを確認。(3) Week 4+ は override_action = "none" に変更して Block 昇格。(4) 昇格後 48h は BlockedRequests メトリクスを 5 分ごとに確認。

# Bot Control ルールのサンプルリクエスト取得
aws wafv2 get-sampled-requests \
  --web-acl-arn "arn:aws:wafv2:us-east-1:123456789012:global/webacl/example-cf-waf/xxxxxxxx" \
  --rule-metric-name "BotControlCommon" \
  --scope CLOUDFRONT \
  --time-window "StartTime=2026-04-22T00:00:00Z,EndTime=2026-04-22T03:00:00Z" \
  --max-items 100 \
  --region us-east-1

CloudFront ログとの突合: Bot Control の action=BLOCK を確認したら、CloudFront アクセスログ(S3 または CloudWatch)の同時刻帯で x-edge-result-type = Error が増加していないかクロスチェックする。正常ユーザーへの影響がなければ Block 運用を維持する。§7 では WAF full log → Firehose → S3 → Athena の経路で長期ログを分析する方法を解説する。

4. Rate Limit — IP レピュテーション・地理ブロック・Rate-based rule v2

Rate Limitの3層構造 IPレピュテーション/地理/Rate-based v2

§2 の WebACL に WCU 残量 313 WCU があります。本章ではその予算から rate-based rule v2 CUSTOM_KEYS 2 WCU を追加し、IP レピュテーション → 地理ブロック → Rate-based v2 の 3 層防御を完成させます。§3 の Bot Control が検出した JA3 フィンガープリントを aggregate key に加えることで、Bot が IP を変えても同一フィンガープリントで制限できます。

§4 WCU 予算(適用後残量: 261 / 1500)

  • §2 確定消費: 1187 WCU(マネージド 5 群 + カスタム 3 本)
  • §3 Bot Control Common: 50 WCU
  • §4 Rate-based rule v2(CUSTOM_KEYS 2 キー): 2 WCU
  • 残量: 261 WCU(§5 Lambda@Edge は WAF WCU を消費しない)

4-1. IP レピュテーション — §2 で設定済みルールの再確認

AWSManagedRulesAmazonIpReputationList(25 WCU)と AWSManagedRulesAnonymousIpList(50 WCU・VPN/Tor 出口)は §2 で override_action = "none"(即 Block)として priority 30/40 に配置済みです。これらは Rate-based rule より前段で評価され、既知の悪意 IP を最上流で排除します。

B2B API など VPN 利用者が多い環境では AnonymousIpList を一時 Count にして誤検知を確認してから Block 昇格させてください。IP レピュテーションリストは AWS 側で週次更新されるため手動メンテ不要です。

4-2. 地理ブロック — allowlist vs blocklist

geo_match_statement で ISO 3166-1 alpha-2 を指定します。WCU は 1 のまま(§2 既計上)です。

# allowlist 方式: JP + US のみ許可(not_statement でラップ)
rule {
  name  = "GeoAllowlist"
  priority = 70
  action{ block {} }
  statement {
 not_statement {
statement {
  geo_match_statement {
 country_codes = var.allowed_countries  # tfvars で ["JP", "US"]
  }
}
 }
  }
  visibility_config {
 cloudwatch_metrics_enabled = true
 metric_name = "GeoAllowlist"
 sampled_requests_enabled= true
  }
}

blocklist 方式(高リスク国を明示 Block)が必要な場合は以下のように記述します。

# blocklist 方式: 高リスク国を明示 Block
rule {
  name  = "GeoBlocklist"
  priority = 70
  action{ block {} }
  statement {
 geo_match_statement {
country_codes = ["CN", "RU", "KP", "IR"]
 }
  }
  visibility_config {
 cloudwatch_metrics_enabled = true
 metric_name = "GeoBlocklist"
 sampled_requests_enabled= true
  }
}

allowlist = not_statement + geo_match が最小権限の設計です。運用地域が明確なサービスでは allowlist 方式を優先してください。

4-3. Rate-based rule v2(2024 GA)— CUSTOM_KEYS 複合集計

2024 年 6 月 GA の Rate-based rule v2 は aggregate_key_type = "CUSTOM_KEYS"最大 5 キーを組み合わせられます。IP + JA3 の 2 キー構成(WCU: 2)を例示します。

rule {
  name  = "RateLimitV2"
  priority = 100
  action{ block {} }

  statement {
 rate_based_statement {
limit  = 500
aggregate_key_type = "CUSTOM_KEYS"
evaluation_window_sec = 300

custom_key { ip {} }
custom_key {
  ja3_fingerprint {
 fallback_behavior = "MATCH"  # JA3 非対応クライアントは一致扱い
  }
}

# /api/ 配下のみ対象(誤検知抑制)
scope_down_statement {
  byte_match_statement {
 search_string= "/api/"
 positional_constraint = "STARTS_WITH"
 field_to_match  { uri_path {} }
 text_transformation{ priority = 0; type = "NONE" }
  }
}
 }
  }

  visibility_config {
 cloudwatch_metrics_enabled = true
 metric_name = "RateLimitV2"
 sampled_requests_enabled= true
  }
}

fallback_behavior = "MATCH" は JA3 非対応端末を一致扱いにする保守的設定です。"NO_MATCH" にするとカウント対象外になります。利用可能な CUSTOM_KEYS: ip / header / query_argument / cookie / ja3_fingerprint / label_namespace(Bot Control ラベル)。

§3 Bot Control との相乗効果

  • Bot Control が付与した JA3 ラベルを aggregate key に組み込むと、IP ローテーションしても同一 Bot ツールを 1 つの集計単位として扱える
  • label_namespace { namespace = "awswaf:managed:aws:bot-control:bot:category:" } でカテゴリ別レート分離も可能

4-4. 評価窓の選び方

evaluation_window_sec60 / 120 / 300 / 600 の 4 値から選択します。

評価窓感度誤検知推奨ユースケース
60s最高ログイン試行ブルートフォース
120s認証 API・パスワードリセット
300s標準一般的な REST API
600s最低バッチ API・CDN 多用サービス

4-5. 閾値設計 — p99 × 3 の計算式

-- CloudWatch Logs Insights: エンドポイント別 p99 リクエストレート
fields @timestamp, httpRequest.uri, httpRequest.clientIp
| filter action = "ALLOW"
| stats count(*) as req by bin(5m), httpRequest.uri, httpRequest.clientIp
| stats pct(req, 99) as p99 by httpRequest.uri
| sort p99 desc
| limit 20

計算例: p99 = 8 req/min → 閾値 = p99 × 3 = 24 req/min → 300s 窓なら limit = 120(24 × 5)。WAF Rate-based は内部的に 1.5 倍のバーストを許容するため、実効最大は 180 req。正常トラフィックへの影響は出ません。

{
  "threshold_design": {
 "p99_req_per_min": 8,
 "multiplier": 3,
 "window_sec": 300,
 "limit": 120,
 "burst_factor": 1.5,
 "effective_max": 180
  }
}

4-6. Token bucket との関係

WAF Rate-based は固定窓(Fixed Window)で動作します。窓境界をまたいだバースト攻撃には理論的な抜け穴がありますが、1.5 倍バースト許容ScopeDownStatement の組み合わせで実用上は問題ありません。滑動窓・Token Bucket が必要な場合は API Gateway の Usage Plan またはアプリケーション層で実装してください。

4-7. Terraform 実装 — dynamic rule 展開

# locals でエンドポイント別設定を管理
locals {
  rate_limit_rules = {
 api_general = { priority = 100, limit = 500, window = 300, path = "/api/",constraint = "STARTS_WITH" }
 auth_login  = { priority = 90,  limit = 20,  window = 60,  path = "/auth/login", constraint = "EXACTLY"  }
 admin = { priority = 95,  limit = 50,  window = 120, path = "/admin/", constraint = "STARTS_WITH" }
  }
}

dynamic "rule" {
  for_each = local.rate_limit_rules
  content {
 name  = "RateLimit-${rule.key}"
 priority = rule.value.priority
 action{ block {} }

 statement {
rate_based_statement {
  limit  = rule.value.limit
  aggregate_key_type = "CUSTOM_KEYS"
  evaluation_window_sec = rule.value.window

  custom_key { ip {} }
  custom_key { ja3_fingerprint { fallback_behavior = "MATCH" } }

  scope_down_statement {
 byte_match_statement {
search_string= rule.value.path
positional_constraint = rule.value.constraint
field_to_match  { uri_path {} }
text_transformation{ priority = 0; type = "LOWERCASE" }
 }
  }
}
 }

 visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimit-${rule.key}"
sampled_requests_enabled= true
 }
  }
}
# Rate Limit 動作確認: 閾値超過で 403 が返ることを確認
for i in $(seq 1 30); do
  curl -s -o /dev/null -w "%{http_code}\n" \
 -H "Authorization: Bearer test" \
 "https://your-distribution.cloudfront.net/api/items"
done
§4 実装チェックリスト

  • ☑ IP レピュテーション 2 ルールが priority 30/40 で即 Block 済み(§2)
  • ☑ 地理ブロック方式(allowlist / blocklist)を tfvars で管理
  • ☑ Rate-based v2 の evaluation_window_sec をユースケース別に設定
  • ☑ CUSTOM_KEYS に JA3 を含めて §3 Bot Control と連携
  • ☑ ScopeDownStatement で対象 URL を絞り誤検知を抑制
  • ☑ 閾値 = p99 × 3・バースト 1.5 倍を考慮

apply 後は以下で WCU を確認し(期待値: 1239)、1500 以内であることを検証してください。

aws wafv2 get-web-acl --name your-project-cf-waf \
  --scope CLOUDFRONT --id <WebACL-ID> --region us-east-1 \
  --query 'WebACL.Capacity'
# 期待出力: {"Capacity": 1239}  (1187 + 50 + 2 = 1239)

次章(§5)では Lambda@Edge の 4 トリガーと JWT 署名検証・A/B テスト・画像リサイズの 3 ユースケースを実装します。

5. Lambda@Edge — viewer-request / origin-request ユースケース実装

Lambda@Edgeの4トリガー位置とCFFとの差分

Lambda@Edge(L@E)は CloudFront エッジで Node.js Lambda 関数を実行する仕組みで、WAF では判定しきれない複雑ロジック(JWT 署名検証・動的振り分け・ストリームリサイズ等)をリクエスト/レスポンスの 4 点に挿入できる。本章では 3 つの典型ユースケースを Terraform とともに実装する。

5-1. 4 トリガーの特性

トリガー実行タイミングメモリ上限タイムアウト
viewer-requestリクエスト受信後・キャッシュ参照前128 MB5 秒
viewer-responseレスポンスをクライアントへ返す直前128 MB5 秒
origin-requestキャッシュミス時・オリジンへ転送する直前10,240 MB30 秒
origin-responseオリジンからレスポンス受信後・キャッシュ格納前10,240 MB30 秒

選択指針: キャッシュより前で遮断 → viewer-request。キャッシュヒット時スキップ → origin-request。レスポンスボディを加工 → origin-response(10 GB で重い処理が可能)。Cold start は初回 100–500 ms、頻出リージョンではウォームコンテナ維持で体感しにくい。

5-2. ユースケース 1 — URL 署名検証(viewer-request × JWT)

API Gateway / ALB の前段で JWT を検証し、署名が正しくない場合はオリジンへ到達させず 401 を返す。WAF で実現できない「公開鍵 JWKS 参照が必要な非対称署名」に最適。

{
  "name": "edge-auth",
  "type": "module",
  "dependencies": {
 "jsonwebtoken": "^9.0.2",
 "jwks-rsa": "^3.1.0"
  }
}
// lambda-edge-auth-check.mjs (viewer-request)
// Bearer JWT を RS256 で検証し、無効なら 401 を即返却する
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  cacheMaxAge: 600_000, // 10 分キャッシュ — viewer は 5 秒制限なので JWKS 再取得を抑制
});

function getKey(header, cb) {
  client.getSigningKey(header.kid, (err, key) => {
 err ? cb(err) : cb(null, key.getPublicKey());
  });
}

export const handler = async (event) => {
  const request = event.Records[0].cf.request;
  const auth = request.headers['authorization']?.[0]?.value ?? '';

  if (!auth.startsWith('Bearer ')) {
 return { status: '401', statusDescription: 'Unauthorized',
 headers: { 'content-type': [{ value: 'text/plain' }] },
 body: 'Authorization header missing' };
  }

  try {
 await new Promise((resolve, reject) => {
jwt.verify(auth.slice(7), getKey, { algorithms: ['RS256'] },
  (err, decoded) => err ? reject(err) : resolve(decoded));
 });
 return request; // 検証 OK → オリジンへ転送
  } catch {
 return { status: '401', statusDescription: 'Invalid Token',
 headers: { 'content-type': [{ value: 'text/plain' }] },
 body: 'JWT verification failed' };
  }
};

10 % のトラフィックを B オリジンへ送り、Cookie で 2 回目以降は同じオリジンに固定する。L@E で origin オブジェクトを直接書き換える方法。

// lambda-edge-ab-test.mjs (viewer-request)
// 10 % を B オリジンへ振り分け、Cookie で固定する
export const handler = async (event) => {
  const request = event.Records[0].cf.request;
  const cookies = parseCookies(request.headers['cookie']?.[0]?.value ?? '');

  // 既存バケット Cookie を優先し、なければ抽選
  const bucket = cookies['x-ab-bucket'] ?? (Math.random() < 0.1 ? 'B' : 'A');

  // バケットをオリジンへ伝播(オリジン側で Set-Cookie を付与)
  request.headers['x-ab-bucket'] = [{ key: 'X-Ab-Bucket', value: bucket }];

  if (bucket === 'B') {
 request.origin = {
custom: {
  domainName: 'origin-b.example.com',
  protocol: 'https', port: 443, path: '',
  sslProtocols: ['TLSv1.2'],
  readTimeout: 30, keepaliveTimeout: 5, customHeaders: {},
},
 };
 request.headers['host'] = [{ key: 'Host', value: 'origin-b.example.com' }];
  }

  return request;
};

const parseCookies = (s) =>
  Object.fromEntries(s.split(';').map(c => c.trim().split('=').map(decodeURIComponent)));

viewer-request を使う理由: A/B の振り分けはキャッシュより前で決定しないとキャッシュキー設計が崩れる。origin-request では一度キャッシュを参照した後になるため不適切。

5-4. ユースケース 3 — 画像リサイズ(origin-response × Sharp)

S3 から取得した画像を origin-response で Sharp リサイズして返す。クエリパラメーター(?w=320)をキャッシュキーに加えることで多サイズをキャッシュできる。

// lambda-edge-image-resize.mjs (origin-response)
// S3 画像を Sharp でリサイズし WebP 変換してキャッシュに格納させる
import sharp from 'sharp';

export const handler = async (event) => {
  const response = event.Records[0].cf.response;
  const request  = event.Records[0].cf.request;

  const contentType = response.headers['content-type']?.[0]?.value ?? '';
  if (response.status !== '200' || !contentType.startsWith('image/')) return response;

  const width = parseInt(new URLSearchParams(request.querystring).get('w') ?? '0', 10);
  if (!width || width > 2000) return response;

  const src  = Buffer.from(response.body, response.bodyEncoding === 'base64' ? 'base64' : 'utf8');
  const resized = await sharp(src).resize({ width, withoutEnlargement: true })
  .toFormat('webp', { quality: 85 }).toBuffer();

  response.body= resized.toString('base64');
  response.bodyEncoding = 'base64';
  response.headers['content-type']= [{ key: 'Content-Type',value: 'image/webp' }];
  response.headers['content-length'] = [{ key: 'Content-Length', value: String(resized.length) }];
  return response;
};

origin-response のメリット: キャッシュヒット時は L@E が実行されない(CPU 負荷の高い Sharp 変換をスキップ)。origin 系は メモリ 10 GB・30 秒タイムアウト なので大容量画像も余裕がある。

{
  "name": "edge-image-resize",
  "type": "module",
  "dependencies": { "sharp": "^0.33.4" }
}

5-5. Terraform 実装

L@E 関数は us-east-1 で定義し publish = true で発行されたバージョンのみ CloudFront に紐付けできる。

# providers.tf — us-east-1 エイリアス(WAF の CLOUDFRONT scope と共用)
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}
# lambda_edge.tf
data "archive_file" "edge_auth" {
  type  = "zip"
  source_dir  = "${path.module}/lambda/edge-auth"
  output_path = "${path.module}/.build/edge-auth.zip"
}

resource "aws_iam_role" "lambda_edge" {
  name = "${var.project_name}-lambda-edge-role"
  assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Principal = { Service = ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] }
Action = "sts:AssumeRole"
 }]
  })
}

resource "aws_iam_role_policy_attachment" "edge_basic" {
  role = aws_iam_role.lambda_edge.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_lambda_function" "edge_auth" {
  provider= aws.us_east_1  # L@E は us-east-1 固定
  function_name = "${var.project_name}-edge-auth"
  role = aws_iam_role.lambda_edge.arn
  handler = "lambda-edge-auth-check.handler"
  runtime = "nodejs20.x"
  filename= data.archive_file.edge_auth.output_path
  source_code_hash = data.archive_file.edge_auth.output_base64sha256
  publish = true# qualified_arn のために必須
  memory_size= 128
  timeout = 5# viewer-request 上限
}
# cloudfront.tf(抜粋)— distribution への紐付け
resource "aws_cloudfront_distribution" "main" {
  default_cache_behavior {
 lambda_function_association {
event_type= "origin-request"
lambda_arn= aws_lambda_function.edge_auth.qualified_arn  # $LATEST は不可
include_body = false
 }
  }
}

qualified_arnarn:...:function:xxx:1 形式)を使う点に注意。$LATEST は CloudFront に紐付けできない。

5-6. デプロイ特性と削除手順

関数は us-east-1 に定義後、CloudFront が世界中のエッジに 自動レプリケート(反映 3–5 分)する。

デプロイ確認コマンド:

# Lambda 関数のバージョン確認(qualified_arn の末尾が :1 以上であることを確認)
aws lambda list-versions-by-function \
  --function-name my-project-edge-auth \
  --region us-east-1 \
  --query 'Versions[*].FunctionArn' --output table
# 削除手順 (必ず 2 ステップ)
# Step 1: lambda_function_association を外して apply
terraform apply  # distribution から L@E 紐付けを除去

# Step 2: 24 時間後に関数を削除 (レプリカ解除待ち)
terraform destroy -target=aws_lambda_function.edge_auth

【Lambda@Edge 削除時の注意】

L@E 関数の削除後、CloudFront レプリカが解除されるまで 24 時間かかります。即日撤去はできません。手順は「① distribution から lambda_function_association を外して apply → ② 24 時間待機 → ③ 関数を削除」の 3 ステップです。

5-7. コスト感

トリガー種別リクエスト料金コンピュート料金
viewer 系$0.60 / 100 万リクエスト$0.00000625125 / GB-秒
origin 系$0.60 / 100 万リクエスト通常 Lambda 料金と同額

注意: viewer-request はキャッシュヒット時も実行されるため、キャッシュヒット率が高いほどコストが増加する。高トラフィックの静的アセットには origin-request の方が割安になるケースが多い。

5-8. CloudFront Functions との使い分け(概要)

詳細は次章(§6)で決定木を提示するが、3 原則だけ先行して示す。

L@E を選ぶべきケース理由
重い処理(暗号演算・Sharp 変換)CFF は 1 ms・2 MB メモリ上限
外部 SDK 使用(jsonwebtoken・Sharp・axios 等)CFF は Web API のみ(npm パッケージ不可)
KVS より複雑なストア連携(SSM Parameter Store・DynamoDB)CFF は CloudFront KeyValueStore のみ

「単純なヘッダー書き換え・リダイレクト・IP allowlist(Vol2 の CFF 事例)」は CFF が正解。§6 では 5 分岐の決定木として整理する。

6. CloudFront Functions vs Lambda@Edge — 使い分け基準

§5 で実装した Lambda@Edge(L@E)と、Vol2 で使った CloudFront Functions(CFF)は、どちらも CloudFront エッジで JavaScript を実行するが、適用領域が明確に異なる。本章では 5 分岐の決定木・8 軸マトリクス・実例照合・同居設計・運用上の注意を整理し、設計時に迷わない判断基準を提供する。

6-1. 決定木(5 分岐)

「CFF か L@E か」を迷ったときは、以下の問いを上から順番に評価する。最初に “Yes” になった行がそのまま回答となる。

#問いYes →No →
処理が 1 ms 以内で完結するか?CFF次へ ↓
npm パッケージ / 外部 SDK が必要か?(jsonwebtoken、Sharp 等)L@E次へ ↓
CloudFront KeyValueStore 参照のみで完結するか?(1 KVS / 1 function 制約を許容)CFF次へ ↓
外部 HTTP 呼び出しが必要か?(JWKS・DynamoDB・SSM 等)L@E次へ ↓
レスポンスボディの加工が必要か?L@ECFF

: viewer-request の CFF は外部 HTTP 呼び出し不可。origin-request の L@E は 30 秒タイムアウト内であれば外部呼び出し可能。

判定早見まとめ: ヘッダー書き換え・リダイレクト・IP allowlist(KVS lookup) → CFF。暗号演算・大容量変換・SDK 依存・外部 API 参照 → L@E

6-2. 機能比較表(8 軸マトリクス)

比較軸CloudFront Functions (CFF)Lambda@Edge (L@E)
実行時間上限1 msviewer 系 5 s / origin 系 30 s
メモリ上限2 MB(コード含む)viewer 系 128 MB / origin 系 10,240 MB
言語JavaScript(ECMAScript 5.1+)Node.js 20.x / Python 3.12 等
npm パッケージ / SDK不可(Web API のみ)可(zip にバンドル)
ネットワーク外部呼び出し不可可(viewer 系 / origin 系とも制限付きで対応)
対応 event type 数viewer-request / viewer-response の 2 種のみ4 種(viewer-request / viewer-response / origin-request / origin-response)
料金$0.10 / 1M req(安価)viewer 系 $0.60 / 1M req + GB-sec / origin 系 $0.60 / 1M req + 通常 Lambda 料金
反映時間数秒(グローバル一斉反映)3–5 分(us-east-1 → CloudFront global replicate)

補足: CFF・L@E とも、同一 distribution の同一 event type には 1 関数しか紐付けできない(関数のチェーン実行は非サポート)。

6-3. 実例対応 — Vol2 と Vol3 で照合する

理論より実例で身につけるために、本シリーズの過去実装から 2 例を照合する。

Vol2 — IP allowlist(CFF が正解)

// cloudfront-functions-allowlist.js(CFF / viewer-request)
// KVS でブロック IP を参照し、存在すれば 403 を返す
import cf from 'cloudfront';

const kvs = cf.kvs('ip-allowlist-kvs');

async function handler(event) {
  const clientIp = event.viewer.ip;
  try {
 await kvs.get(clientIp);// KVS ヒット → ブロック対象
 return { statusCode: 403, statusDescription: 'Forbidden' };
  } catch {
 return event.request;// KVS ミス → 許可 IP → 通過
  }
}

CFF が正解の理由:
– KVS lookup は 1 ms 以内で完了 — 外部 HTTP 呼び出し不要
cloudfront モジュールは CFF 組込み — npm パッケージ不要
– viewer-request 実行 → キャッシュ参照前にリクエストを遮断できる
– 料金 $0.10 / 1M req は L@E の約 1/6

Vol3 — JWT 署名検証(L@E が正解)

// lambda-edge-auth-check.mjs(L@E / viewer-request)
// RS256 JWT を JWKS で検証 — CFF では次の 2 点で実装不可
import jwt from 'jsonwebtoken'; // ① npm パッケージ → CFF 不可
import jwksClient from 'jwks-rsa'; // ② 外部 HTTPS 呼び出し → CFF 不可

const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true, cacheMaxAge: 600_000,
});
// 全実装は §5-2 参照

L@E が正解の理由:
jsonwebtoken / jwks-rsa は npm パッケージ → CFF では読み込めない
– JWKS エンドポイントへの外部 HTTPS 呼び出しが必要 → CFF では不可
– RS256 の暗号演算は 1 ms を大幅超過 → CFF の実行時間上限を超える

テスト方法の比較

# CFF: コンソール「Test function」タブで即時テスト(デプロイ不要)
# CloudFront → Functions → 対象関数 → Test tab → イベント JSON を貼り付けて実行

# L@E: aws lambda invoke でローカルテスト(us-east-1 指定必須)
aws lambda invoke \
  --function-name my-project-edge-auth \
  --region us-east-1 \
  --payload file://test-event.json \
  response.json
cat response.json

【判断チェックポイント】

「npm パッケージを使いたい」「外部 API を呼びたい」のいずれか 1 つでも当てはまれば、迷わず Lambda@Edge を選択する。CloudFront Functions は KVS lookup・ヘッダー操作・リダイレクト の 3 用途に特化して使うのが運用上の最善策。

6-4. 同居設計 — 1 distribution で CFF + L@E を併用する

同一 CloudFront distribution で CFF と L@E を 異なる event type に 配置するのは公式サポートされている。CFF で軽量処理を担い、L@E で複雑処理を担う分業構成が最も効率的。

# cloudfront.tf(抜粋)— CFF を viewer-request、L@E を origin-request に分離
resource "aws_cloudfront_distribution" "main" {
  default_cache_behavior {
 # CFF: viewer-request でヘッダー書き換え / リダイレクト
 function_association {
event_type= "viewer-request"
function_arn = aws_cloudfront_function.redirect.arn
 }

 # L@E: origin-request で JWT 検証 / A/B 振り分け
 lambda_function_association {
event_type= "origin-request"
lambda_arn= aws_lambda_function.edge_auth.qualified_arn
include_body = false
 }
  }
}

同居の可否一覧:

組合せ可否理由
CFF viewer-request + L@E origin-request✅ 可event type が異なる
CFF viewer-request + L@E viewer-request❌ 不可同一 event type には 1 関数のみ
CFF viewer-response + L@E origin-response✅ 可event type が異なる
L@E origin-request + L@E origin-response✅ 可L@E 同士でも event type が異なれば共存可

評価順序: 同一 request に CFF viewer-request と L@E origin-request の両方が設定されている場合、CFF が先に実行される。CFF がレスポンスを短絡返却(return { statusCode: 403, ... })すると、後段の L@E origin-request は実行されない。キャッシュヒット時も L@E origin-request はスキップされる(origin へのリクエストが発生しないため)。

6-5. 運用上の使い分け

障害時の fallback パス

例外発生箇所挙動対策
CFF 例外(実行時エラー)リクエストをそのままオリジンへ素通し(CFF は「通す / 止める」のみ・エラー時は通過)try-catch で 403 / 301 を明示的に返す
L@E 例外(viewer 系)503 Service Unavailable をクライアントへ即返却try-catch で 401 / 403 を自前返却し、例外を伝播させない(§5-2 実装参照)
L@E 例外(origin 系)503 + CloudFront エラーページVol1 §4 の origin group failover と組み合わせ、例外時に sorry ページへ自動切替

CFF の「例外で素通し」仕様は重要: セキュリティ用途(IP allowlist)では必ず明示的な deny ロジックを書く。KVS ミスヒット = 許可 IP = 通過、と設計する際は try-catch の catch 側で return event.request が正解(KVS ヒット = ブロック対象 = 403 を返す)。

開発体験の差

観点CFFL@E
テスト方法コンソール「Test function」タブで即時実行(デプロイ不要)aws lambda invoke(us-east-1 指定)でローカルテスト可
デプロイ速度数秒(グローバル一斉反映)3–5 分(レプリケーション待ち)
ログ確認先CloudWatch /aws/cloudfront/function/<name>CloudWatch /aws/lambda/us-east-1.<function-name> の最寄りリージョン
バージョン管理ETag ベース・単一バージョン(ロールバックは再デプロイ)$LATEST と published version を区別・CloudFront には published version のみ紐付け可
削除手順function_association を外して apply → 即削除可紐付け解除 apply → 24 時間待機 → 関数削除(§5-6 参照)

【L@E 削除の 24 時間ルール】

Lambda@Edge 関数の削除前に CloudFront distribution から lambda_function_association を外し、CloudFront レプリカが解除されるまで 24 時間待機が必要です。CFF は即削除可能なため、試作・廃棄を繰り返すプロトタイプ用途には CFF が適しています。

【§6 まとめ — 3 原則】

1 ms / npm / KVS の 3 軸で即判断: 1 ms 以内かつ SDK 不要かつ KVS で完結 → CFF。それ以外 → L@E。
同居は event type を分けて共存可: CFF viewer-request + L@E origin-request の組合せが推奨パターン。
CFF は例外で素通し・L@E は例外で 503: 両者の障害モードを把握して fallback を設計する。

7. 運用 — WAF ログ可視化・アラーム・監査

CloudFront + WAF の防御層を本番運用するには、「何を遮断したか」を継続的に可視化し、閾値超過でアラートを受け取れる体制が必要です。本章では Kinesis Firehose を経由した WAF full log の S3 書き出しと、CloudWatch Logs Insights・Athena による 2 経路の分析方法を解説します。

WAF full log→Firehose→S3→Athena/Logs Insights 2経路

本章のポイント

  • WAF full log は Kinesis Firehose(名前プレフィックス aws-waf-logs- 必須)経由で S3 に保存し、2 経路で分析する
  • CloudWatch Logs Insights クエリ 3 本で「blocked IP」「Bot カテゴリ」「Rate 超過」を即把握
  • BlockedRequests アラームを SNS → Slack に連携し、攻撃急増を 5 分以内に検知する

7-1. WAF full log 設定(Terraform)

WAF ログには sampled log(CloudWatch Metrics に自動記録・5 分粒度)と full log(全リクエストを Firehose 経由で S3 出力)の 2 種類があります。本番運用では full log を有効化し、Athena での任意期間・任意条件のアドホック分析を可能にします。

# Kinesis Firehose(名前は "aws-waf-logs-" プレフィックス必須)
resource "aws_kinesis_firehose_delivery_stream" "waf_logs" {
  provider = aws.us_east_1
  name  = "aws-waf-logs-${var.project_name}"

  destination = "extended_s3"

  extended_s3_configuration {
 role_arn= aws_iam_role.firehose_waf.arn
 bucket_arn = aws_s3_bucket.waf_logs.arn
 prefix  = "waf-logs/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/"
 error_output_prefix = "waf-logs-errors/!{firehose:error-output-type}/"
 buffering_size= 128
 buffering_interval  = 300
 compression_format  = "GZIP"
  }
  tags = var.common_tags
}

resource "aws_s3_bucket_lifecycle_configuration" "waf_logs" {
  bucket = aws_s3_bucket.waf_logs.id
  rule {
 id  = "waf-logs-lifecycle"
 status = "Enabled"
 transition { days = 30;  storage_class = "STANDARD_IA" }
 transition { days = 90;  storage_class = "GLACIER"  }
 expiration { days = 365 }
  }
}

# WAF → Firehose 紐付け(logging_filter で ALLOW は除外しコストを抑える)
resource "aws_wafv2_web_acl_logging_configuration" "main" {
  provider = aws.us_east_1
  log_destination_configs = [aws_kinesis_firehose_delivery_stream.waf_logs.arn]
  resource_arn= aws_wafv2_web_acl.main.arn

  logging_filter {
 default_behavior = "KEEP"
 filter {
behavior = "DROP"
requirement = "MEETS_ANY"
condition { action_condition { action = "ALLOW" } }
 }
  }
}

# Firehose 用 IAM ロール(最小権限:PutObject + GetBucketLocation のみ)
resource "aws_iam_role_policy" "firehose_waf" {
  name = "firehose-waf-s3-policy"
  role = aws_iam_role.firehose_waf.id
  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect= "Allow"
Action= ["s3:PutObject", "s3:GetBucketLocation", "s3:ListBucket"]
Resource = [aws_s3_bucket.waf_logs.arn, "${aws_s3_bucket.waf_logs.arn}/*"]
 }]
  })
}

7-2. CloudWatch Logs Insights クエリ(3 本)

WAF sampled log は CloudWatch Logs グループ aws-waf-logs-<name> に自動収集されます。以下の 3 クエリをコンソールに保存しておくとインシデント時に即座に状況を把握できます。

クエリ 1 — action=BLOCK の Top 10 IP アドレス

fields @timestamp, httpRequest.clientIp, action, terminatingRuleId
| filter action = "BLOCK"
| stats count(*) as blocked_count by httpRequest.clientIp, terminatingRuleId
| sort blocked_count desc
| limit 10

クエリ 2 — Bot カテゴリ別リクエスト数(Bot Control 分析)

fields @timestamp, labels.0.name, action
| filter labels.0.name like /awswaf:managed:aws:bot-control/
| stats count(*) as req_count by labels.0.name, action
| sort req_count desc
| limit 20

クエリ 3 — Rate-based rule 超過時刻 × エンドポイント

fields @timestamp, httpRequest.uri, httpRequest.clientIp, terminatingRuleId
| filter terminatingRuleId = "RateLimitV2"
| stats count(*) as rate_exceeded by bin(5min), httpRequest.uri
| sort @timestamp desc
| limit 50

スキャン料金は $0.005/GB です。クエリ期間を絞るとコストを抑えられます。

7-3. Athena で S3 ログをクエリ

30 日以上前のログや大量データの横断分析には Athena を使います。

CREATE EXTERNAL TABLE waf_logs (
  timestampBIGINT,
  actionSTRING,
  terminatingruleid STRING,
  httprequest STRUCT<
 clientip: STRING,
 country:  STRING,
 uri:STRING,
 httpmethod: STRING
  >,
  labels ARRAY<STRUCT<name:STRING>>
)
PARTITIONED BY (year STRING, month STRING, day STRING)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://YOUR-BUCKET/waf-logs/';

MSCK REPAIR TABLE waf_logs;  -- パーティション追加(日次バッチ)

SELECT httprequest.clientip, terminatingruleid, COUNT(*) AS block_count
FROM waf_logs
WHERE year = '2026' AND action = 'BLOCK'
GROUP BY httprequest.clientip, terminatingruleid
ORDER BY block_count DESC
LIMIT 20;

Athena は $5/TB スキャンです。PARTITIONED BY で日付パーティションを切ることで不要スキャンを回避し、コストを大幅に削減できます。

7-4. sampled_requests_enabled による 5 分粒度モニタリング

各ルールの visibility_configsampled_requests_enabled = true を設定すると、CloudWatch 名前空間 AWS/WAFV2 に 5 分粒度のメトリクスが自動記録されます。

メトリクス意味確認頻度
BlockedRequestsルール別 BLOCK 数毎日
CountedRequestsCount モード件数(Block 昇格候補)毎週
AllowedRequests通過リクエスト数異常値のみ

7-5. 攻撃傾向ダッシュボード + アラーム設計

# CloudWatch アラーム(Block 急増検知)
resource "aws_cloudwatch_metric_alarm" "waf_block_spike" {
  provider= aws.us_east_1
  alarm_name = "${var.project_name}-waf-block-spike"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name= "BlockedRequests"
  namespace  = "AWS/WAFV2"
  period  = 300
  statistic  = "Sum"
  threshold  = 500  # 5分間で500件超過
  alarm_actions = [aws_sns_topic.waf_alerts.arn]

  dimensions = {
 WebACL = "${var.project_name}-cf-waf"
 Region = "us-east-1"
 Rule= "ALL"
  }
  tags = var.common_tags
}

resource "aws_sns_topic" "waf_alerts" {
  provider = aws.us_east_1
  name  = "${var.project_name}-waf-alerts"
}

閾値の設定指針: 正常時の BlockedRequests 平均値を 2 週間分の CloudWatch データから算出し、p99 × 3 倍を初期閾値として設定します。Bot Control の CAPTCHA 成功率(CaptchaRequests メトリクス)が通常の 70% を下回った場合は人手確認を推奨します。

7-6. 監査要件 — 90 日保管 + Glacier 移行

コンプライアンス要件として「いつ・どの IP が・何回 block されたか」を 90 日間保管する場合は、7-1 の Lifecycle 設定で対応します。

期間ストレージクラス目的
0-30 日STANDARD即時クエリ(Athena / Logs Insights)
30-90 日STANDARD_IA月次レビュー・インシデント調査
90-365 日GLACIER監査保管(取得に 3-5 時間)
365 日超削除コスト最適化
IAM 最小権限チェックポイント

  • Firehose delivery role は s3:PutObject + s3:GetBucketLocation + s3:ListBucket のみ(s3:* は付与しない)
  • Athena 実行ロールには s3:GetObject(ログバケット READ)と athena:StartQueryExecution のみ付与
  • WAF logging configuration 設定には wafv2:PutLoggingConfiguration が必要(Terraform 実行ロールに追加)

7-7. 運用フロー — 日次・週次・月次レビュー

WAF ログを活用した継続的な運用フローを以下のサイクルで回します。

日次(自動)
– CloudWatch アラームの発火状況を Slack で確認(BlockedRequests 急増・Rate Limit 超過)
– 新規 BLOCK IP を Logs Insights クエリ 1 で確認、既知の攻撃 IP なら対応不要

週次(手動 15 分)
– Count モードのルールを Logs Insights クエリで確認 → 誤検知なければ Block へ昇格
– Bot カテゴリ別トレンドを確認(クエリ 2)→ 想定外カテゴリの急増は Bot Control ルール調整検討

月次(手動 30 分)
– Athena で 30 日間の top blocked IP を集計し IP-set に追加候補を抽出
– WAF の WCU 使用量を確認(aws wafv2 describe-web-aclCapacity フィールド参照)→ 1500 上限に対して 80% を超えたら追加申請を検討
– Cost Explorer で WAF + Firehose + Bot Control の合計コストを確認

# WAF の現在の WCU 使用量を確認
aws wafv2 describe-web-acl \
  --name "${PROJECT_NAME}-cf-waf" \
  --scope CLOUDFRONT \
  --id $(aws wafv2 list-web-acls --scope CLOUDFRONT --query 'WebACLs[?Name==`'"${PROJECT_NAME}-cf-waf"'`].Id' --output text) \
  --region us-east-1 \
  --query 'WebACL.Capacity'

7-8. トラブルシューティング — よくある WAF 設定ミス

ハンズオン中に遭遇しやすいエラーと対処法をまとめます。

症状原因対処
terraform applyInvalidParameterException: scope で失敗WAF WebACL を ap-northeast-1 provider で apply しているprovider = aws.us_east_1 を明示する
Firehose stream が作成できないストリーム名が aws-waf-logs- プレフィックスではない名前を aws-waf-logs-<suffix> に変更する
CloudWatch メトリクスが表示されないsampled_requests_enabled = false各ルールの visibility_configtrue に変更
Bot Control が全リクエストを BLOCK しているoverride_actionnone のまま Count モードで運用できていないoverride_action { count {} } に変更して観察期間を設ける
logging_configuration の apply が失敗するFirehose stream の ARN が WAF の us-east-1 provider と一致していないFirehose も provider = aws.us_east_1 で作成する
Athena クエリが HIVE_PARTITION_SCHEMA_MISMATCH エラーMSCK REPAIR TABLE が未実行MSCK REPAIR TABLE waf_logs; を実行してパーティションを追加する
# WAF logging configuration の設定確認
aws wafv2 get-logging-configuration \
  --resource-arn $(aws wafv2 list-web-acls \
 --scope CLOUDFRONT --region us-east-1 \
 --query 'WebACLs[0].ARN' --output text) \
  --region us-east-1

8. Vol1/Vol2 との連携と次回予告

8-1. Vol1 ↔ Vol3 連携 — web_acl_id を 1 行追加

Vol1 で構築した aws_cloudfront_distribution に WAF を紐付けるには、web_acl_id を 1 行追加するだけです。

# Vol1 の main.tf に以下の差分を追加
resource "aws_cloudfront_distribution" "main" {
  # ... 既存の設定は変更不要 ...

  # WAF v2 WebACL を紐付け(Vol3 で新規追加する行)
  web_acl_id = aws_wafv2_web_acl.main.arn

  # ... 残りの設定 ...
}

web_acl_id には WAF WebACL の ARNaws_wafv2_web_acl.main.arn)を指定します。リソース ID ではなく ARN である点に注意してください。また WAF は us-east-1 provider で作成していますが、CloudFront distribution 自体のリソース定義は通常の provider(ap-northeast-1 等)で問題ありません。ARN で参照するため provider 違いは影響しません。

Terraform の状態管理として、Vol1/Vol2/Vol3 を 同一 Terraform state(単一 backend) で管理する方法と、モジュール分離(module per vol) で管理する方法のどちらも採用できます。本シリーズのサンプルコードは単一 state を前提にしていますが、チーム開発では terraform workspace または S3 backend を Vol ごとに分割することを検討してください。

Vol1 のコードベースに Vol3 モジュールを追加した後の apply 手順:

cd infra/  # Vol1 の Terraform ディレクトリ
terraform plan# web_acl_id 追加と WAF 新規リソースが plan に含まれることを確認
terraform apply  # CloudFront distribution の更新のみ(数分でデプロイ完了)

8-2. Vol2 ↔ Vol3 連携 — CFF と WAF の役割分担

Vol2 で実装した CloudFront Functions(IP allowlist + メンテナンス切替)と Vol3 WAF(IP-set rule + マネージドルール)は評価レイヤーが異なるため、同一の CloudFront distribution で共存できます。

機能実装場所評価タイミング主な用途
IP allowlistVol2 CFF(viewer-request)CloudFront edge(WAF より前)社内 IP のメンテ画面アクセス許可
IP ブロックVol3 WAF(IP-set rule)CloudFront edge(CFF と並行)悪意ある IP の完全遮断
Bot 遮断Vol3 WAF(Bot Control)WAF 評価チェーンBot フィンガープリント判定

同一 IP を両層に設定する是非: CFF の allowlist(許可対象)と WAF の IP-set(ブロック対象)は用途が異なるため、同一 IP が両方に含まれることは通常ありません。ただし、WAF の地理ブロック + CFF の IP allowlist の組み合わせでは「ブロック対象国の社内 IP を allowlist で許可したい」というシナリオが起きることがあります。この場合は WAF に scope-down statement を追加して、CFF allowlist 対象の IP-set をスコープ外にする方法が有効です。

8-3. Vol1 → Vol2 → Vol3 — プロダクション最小構成完成

3 部作を完走すると、以下のプロダクション最小構成が完成します。

Internet
 │
 ▼
CloudFront (Vol1: CDN 3層構成)
 ├─ CloudFront Functions (Vol2: IP allowlist + メンテ切替)
 ├─ AWS WAF v2 (Vol3: マネージド+カスタム+Bot Control+Rate Limit)
 └─ Lambda@Edge (Vol3: JWT検証 / A/B test / 画像リサイズ)
 │
 ├─▶ ALB → EC2/ECS (Vol1: アプリケーション層)
 └─▶ S3 (Vol1: 静的コンテンツ / sorry ページ)

この構成が実現する防御能力:

  • DDoS 耐性: AWS Shield Standard(CloudFront に自動付属)+ Rate Limit v2 で多層防御
  • Bot 対策: Bot Control Common/Targeted でスクレイピング・クレデンシャルスタッフィングを遮断
  • アプリ層攻撃対策: WAF マネージドルール(SQLi / XSS / 既知悪意 IP)で OWASP Top 10 をカバー
  • 柔軟な認証・ルーティング: Lambda@Edge で複雑なビジネスロジックを edge で実行
  • 運用切替: CloudFront Functions でメンテナンスモードを PR 駆動でゼロダウンタイム実施

ハンズオン後は忘れずに terraform destroy でリソースを削除してください。Bot Control の月額 $10 は有効化時間に応じた日割り請求です。

destroy 推奨順序: Lambda@Edge は CloudFront distribution から lambda_function_association を外した後、最低 24 時間待機してから削除します。先に Lambda を削除しようとしても「レプリカが存在する」エラーが返り、手動削除が必要になります。

# Step 1: lambda_function_association を CloudFront から外して apply
# Step 2: 24 時間後に Lambda 関数を削除
terraform destroy -target=aws_lambda_function.edge_auth  # 24h 経過後に実行

# WAF・Firehose・S3 は依存関係の逆順で destroy(通常は一括 destroy で OK)
terraform destroy

Vol1 + Vol2 + Vol3 の全リソースを destroy すると AWS 課金はゼロになります(S3 バケット内のオブジェクトは aws s3 rm s3://BUCKET --recursive で別途削除が必要です)。

コスト確認: AWS Cost Explorer で「Service = AWS WAF」「Service = Amazon Kinesis」「Service = AWS Lambda」を絞り込み、Bot Control 有効化期間の日割り料金を確認してください。Bot Control Targeted($10/M req)は誤って有効化したまま放置すると予想外の請求が発生します。

8-4. 次回候補 — Vol4 以降の展望

Vol3 完了時点でプロダクション最小構成は完成しています。Vol4 以降の候補を読者フィードバックを踏まえてリストアップしています。

候補内容難易度
Vol4-A: AWS Shield AdvancedDDoS 特化型保護・WAF との統合・コスト試算★★★
Vol4-B: ACM 自動証明書回転CloudFront TLS 証明書のゼロタッチ自動更新★★
Vol4-C: Edge Functions MCP 連携AI エージェントからの CloudFront 設定変更★★★

次回テーマは記事末尾のコメント欄でフィードバックをお寄せください。

Vol4 着手前に確認すべき公式情報

  • AWS Shield Advanced: 料金(月額 $3,000 + DRT サポート込み)を必ず確認。WAF と組み合わせた DDos-cost-protect も有効。
  • ACM 自動証明書回転: CloudFront は ACM 証明書を自動更新するため、通常は手動作業不要。ただしカスタムドメイン + Route53 のバリデーション自動化は明示設定が必要。
  • Edge Functions MCP: 2026 年 GA 予測のため、本番採用は GA 後に改めて検証推奨。

8-5. Vol1-Vol3 全通し学習スキル棚卸

Vol1 → Vol2 → Vol3 で習得できる 24 スキル

  • CDN 基礎(Vol1): CloudFront × ALB × S3 の 3 層構成 / VPC Origins / OAC / origin group failover / Terraform one-shot apply / GHA OIDC デプロイ / CloudFront ログ有効化 / カスタムエラーページ
  • 運用切替(Vol2): CloudFront Functions 実装 / KeyValueStore 設計 / IP allowlist 管理 / PR 駆動 tfvars 切替 / CFF テスト API / KVS 更新 API / メンテナンスモード実施
  • 攻撃防御(Vol3): WAF v2 マネージドルール 5 群 / locals+for_each+dynamic 宣言的設計 / Bot Control Common/Targeted / Rate Limit v2 複合キー設計 / Lambda@Edge 4 トリガー実装 / CFF vs L@E 使い分け判断 / WAF full log → Firehose → S3 可視化 / CloudWatch Logs Insights クエリ設計 / Athena パーティション分析 / WAF アラーム → SNS → Slack 通知

8-6. シリーズバックリンク

Vol1・Vol2 を未読の場合は先にお読みいただくと、本記事の WAF 設定が「どの CloudFront distribution に適用されるか」がより明確になります。Vol1 で構築した aws_cloudfront_distribution と Vol2 で実装した aws_cloudfront_function は、Vol3 の WAF 設定とすべて同一の distribution 上で動作します。

Vol1: CloudFront × ALB × S3 デュアルオリジン基礎編

Vol2: CloudFront Functions IP allowlist × メンテナンス切替 Terraform

Vol1 → Vol2 → Vol3 の順にハンズオンすることで、CDN 基礎から運用自動化、攻撃防御まで一貫したスキルセットが身に付きます。各記事は独立して読めますが、Terraform コードは Vol1 の main.tf を起点に差分追加していく設計のため、順序通りに進めることを推奨します。

本シリーズへのご意見・ご質問はコメント欄までお寄せください。「次は Shield Advanced を解説してほしい」「Lambda@Edge の実案件事例を知りたい」など、読者の声が Vol4 以降のテーマ選定に直結します。