- 1 1. Vol3 位置付けとアーキテクチャ全体像
- 2 2. WAF v2 マネージドルール + カスタムルール(コンソール + Terraform)
- 3 3. Bot Control 運用設計
- 4 4. Rate Limit — IP レピュテーション・地理ブロック・Rate-based rule v2
- 5 5. Lambda@Edge — viewer-request / origin-request ユースケース実装
- 6 6. CloudFront Functions vs Lambda@Edge — 使い分け基準
- 7 7. 運用 — WAF ログ可視化・アラーム・監査
- 8 8. Vol1/Vol2 との連携と次回予告
1. 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 の前段・内部に重ねる。各層は独立して機能し、組み合わせることで多層防御を実現する。
| 層 | サービス | 役割 | 主な対象脅威 |
|---|---|---|---|
| 1 | AWS WAF v2 | マネージドルール + カスタムルールでリクエスト内容を検査・遮断 | SQLi / XSS / 既知の不正ペイロード |
| 2 | Bot Control | AWS 管理のシグネチャで既知 Bot を分類・遮断 | スクレイパー / クローラー / 不正自動ツール |
| 3 | Rate Limit v2 | IP・ヘッダー・Cookie の複合キーで閾値超過リクエストを遮断 | HTTP Flood / DDoS / スクレイピング |
| 4 | Lambda@Edge | viewer-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)

AWS WAF v2 は CloudFront に直接アタッチできる Web Application Firewall です。scope=CLOUDFRONT を指定した WebACL は us-east-1 固定で作成する必要があります。本章では、マネージドルール 5 群とカスタムルール 3 本を Terraform の locals + for_each + dynamic で宣言的に管理する手順を解説します。
- ap-northeast-1 の provider では apply が失敗する(
InvalidParameterException: scope) - Terraform で
provider = aws.us_east_1alias を必ず設定すること - Vol1 main.tf に
provider "aws" { alias = "us_east_1" region = "us-east-1" }を追記してから apply する
2-1. コンソール操作:WebACL 作成と CloudFront への紐付け
手順(所要 10〜15 分)
- WAF & Shield コンソール → Create web ACL → Resource type:
Amazon CloudFront distributions(自動で us-east-1 に切替) - Name:
your-project-cf-waf/ Default action: Allow - Add managed rule groups で以下 5 群を追加し、各ルールの Override を Count に設定(初期観察用):
- Core rule set(AWSManagedRulesCommonRuleSet)
- Known bad inputs(AWSManagedRulesKnownBadInputsRuleSet)
- Amazon IP reputation list(AWSManagedRulesAmazonIpReputationList)
- Anonymous IP list(AWSManagedRulesAnonymousIpList)
- SQL database(AWSManagedRulesSQLiRuleSet)
- カスタムルール 2 本追加(2-3 参照)
- Add associated AWS resources → 対象の CloudFront distribution を選択 → Create web ACL
- 初回は全マネージドルールを 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_action の dynamic 分岐: 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_rules の override_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 で採用したルール構成の消費量と残量を以下に示します。
| ルール名 | 種別 | WCU | override_action |
|---|---|---|---|
| AWSManagedRulesCommonRuleSet | マネージド | 700 | count → none |
| AWSManagedRulesKnownBadInputsRuleSet | マネージド | 200 | count → none |
| AWSManagedRulesAmazonIpReputationList | マネージド | 25 | none(即 Block) |
| AWSManagedRulesAnonymousIpList | マネージド | 50 | none(即 Block) |
| AWSManagedRulesSQLiRuleSet | マネージド | 200 | count → none |
| BodySizeLimit(size constraint) | カスタム | 10 | block |
| GeoAllowlist(geo match) | カスタム | 1 | block |
| IPSetBlock(IP-set reference) | カスタム | 1 | block |
| §2 合計 | 1187 | ||
| §3/§4 向け残量 | 313 WCU |
- 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 運用設計

AWS WAF の Bot Control マネージドルールを使うと、検索エンジン・スクレイパー・スキャナー等の既知 Bot を自動判別してフィルタリングできる。本章ではスコープ選択・ルール override・Terraform 実装・運用観察まで一気通貫で解説する。
3-1. Common vs Targeted — スコープ選択の判断基準
Bot Control には 2 つのスコープがある。
| 項目 | Common | Targeted |
|---|---|---|
| 判定方式 | 静的シグネチャ(UA・IP・ヘッダー照合) | 動的チャレンジ(CAPTCHA / JavaScript challenge) |
| 料金 | $1/100万リクエスト + $10/月 | $10/100万リクエスト + $10/月 |
| 誤検知リスク | 低(シグネチャ固定) | 中(challenge 失敗で正常ユーザーも影響) |
| 対応 Bot 種別 | 既知 Bot(静的署名で識別可能) | ヘッドレスブラウザ・動的回避型高度 Bot |
| WCU 消費 | 25 WCU | 50 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 |
|---|---|---|
CategorySearchEngine | Googlebot / Bingbot 等の正規検索エンジン | allow |
CategoryMonitoring | Datadog / UptimeRobot 等の監視 Bot | allow(自社 Bot 確認済みなら) |
CategoryScrapingFramework | Scrapy / Playwright headless 等 | block |
CategoryLinkChecker | SEO ツールのリンクチェッカー | count(観察後判断) |
CategoryHttpLibrary | curl / python-requests / axios 等 | count(API クライアントに混在可能性) |
CategorySocialMedia | Twitterbot / Slackbot 等の OGP 取得 Bot | allow |
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"
}
| アクション | 仕組み | ユーザー体験への影響 | 推奨用途 |
|---|---|---|---|
challenge | JavaScript パズル(透過的) | ほぼなし(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

§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 を変えても同一フィンガープリントで制限できます。
- §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 ラベル)。
- Bot Control が付与した JA3 ラベルを aggregate key に組み込むと、IP ローテーションしても同一 Bot ツールを 1 つの集計単位として扱える
label_namespace { namespace = "awswaf:managed:aws:bot-control:bot:category:" }でカテゴリ別レート分離も可能
4-4. 評価窓の選び方
evaluation_window_sec は 60 / 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
- ☑ 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(L@E)は CloudFront エッジで Node.js Lambda 関数を実行する仕組みで、WAF では判定しきれない複雑ロジック(JWT 署名検証・動的振り分け・ストリームリサイズ等)をリクエスト/レスポンスの 4 点に挿入できる。本章では 3 つの典型ユースケースを Terraform とともに実装する。
5-1. 4 トリガーの特性
| トリガー | 実行タイミング | メモリ上限 | タイムアウト |
|---|---|---|---|
viewer-request | リクエスト受信後・キャッシュ参照前 | 128 MB | 5 秒 |
viewer-response | レスポンスをクライアントへ返す直前 | 128 MB | 5 秒 |
origin-request | キャッシュミス時・オリジンへ転送する直前 | 10,240 MB | 30 秒 |
origin-response | オリジンからレスポンス受信後・キャッシュ格納前 | 10,240 MB | 30 秒 |
選択指針: キャッシュより前で遮断 → 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' };
}
};
5-3. ユースケース 2 — A/B テスト振り分け(viewer-request × Cookie)
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_arn(arn:...: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@E | CFF |
注:
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 ms | viewer 系 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 を返す)。
開発体験の差
| 観点 | CFF | L@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 は 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_config で sampled_requests_enabled = true を設定すると、CloudWatch 名前空間 AWS/WAFV2 に 5 分粒度のメトリクスが自動記録されます。
| メトリクス | 意味 | 確認頻度 |
|---|---|---|
BlockedRequests | ルール別 BLOCK 数 | 毎日 |
CountedRequests | Count モード件数(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 日超 | 削除 | コスト最適化 |
- 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-acl で Capacity フィールド参照)→ 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 apply が InvalidParameterException: 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_config を true に変更 |
| Bot Control が全リクエストを BLOCK している | override_action が none のまま 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 の ARN(aws_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 allowlist | Vol2 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 Advanced | DDoS 特化型保護・WAF との統合・コスト試算 | ★★★ |
| Vol4-B: ACM 自動証明書回転 | CloudFront TLS 証明書のゼロタッチ自動更新 | ★★ |
| Vol4-C: Edge Functions MCP 連携 | AI エージェントからの CloudFront 設定変更 | ★★★ |
次回テーマは記事末尾のコメント欄でフィードバックをお寄せください。
- AWS Shield Advanced: 料金(月額 $3,000 + DRT サポート込み)を必ず確認。WAF と組み合わせた DDos-cost-protect も有効。
- ACM 自動証明書回転: CloudFront は ACM 証明書を自動更新するため、通常は手動作業不要。ただしカスタムドメイン + Route53 のバリデーション自動化は明示設定が必要。
- Edge Functions MCP: 2026 年 GA 予測のため、本番採用は GA 後に改めて検証推奨。
8-5. Vol1-Vol3 全通し学習スキル棚卸
- 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 以降のテーマ選定に直結します。