- 1 §1: メンテ切替の要件整理
- 2 §2: 実現方式の比較と採用判断
- 3 §3: CloudFront Functions コード実装(viewer-request × KVS lookup)
- 4 §4: Terraform tfvars 設計(maintenance_mode / allowlist_ips)
- 4.1 前提: Terraform provider バージョン
- 4.2 variables.tf 設計
- 4.3 aws_cloudfront_key_value_store リソース
- 4.4 aws_cloudfrontkeyvaluestore_key リソース
- 4.5 etag 管理と lifecycle の設計判断
- 4.6 aws_cloudfront_function リソース
- 4.7 CF Distribution への function_association 追加
- 4.8 terraform.tfvars の切替操作
- 4.9 動的切替フロー(tfvars → KVS → CF 即時反映)
- 4.10 環境分離(env/prod.tfvars / env/stg.tfvars)推奨構造
- 5 §5: 切替運用(PR → plan → apply / git revert → apply / branch 保護)
- 6 §6: 動作確認(5シナリオ × curl + KVS CLI)
- 6.1 事前準備: 自分の IP アドレスを確認する
- 6.2 シナリオ 1: maintenance_mode=false — 全 IP が本番到達
- 6.3 シナリオ 2: maintenance_mode=true + 自 IP を allowlist に含む → 本番到達
- 6.4 シナリオ 3: maintenance_mode=true + 自 IP を allowlist に含まない → sorry 到達
- 6.5 シナリオ 4: X-Forwarded-For 偽装試行 → clientIp 優先で無効化
- 6.6 シナリオ 5: Console から KVS 直接変更 → 数秒でリアルタイム反映(drift 教材)
- 6.7 複数拠点テスト推奨
- 7 §7: 監査・ログ(CloudFront ログ / CF Functions 実行ログ / WAF 連携)
- 8 §8. コスト・制約・TOC 超過リスク
- 9 §9. まとめ — CloudFront Functions × KVS × Terraform で作るメンテ切替基盤
§1: メンテ切替の要件整理
本記事では、CloudFront Functions と KeyValueStore を組み合わせて IP allowlist ベースのメンテナンスモード切替を実装します。メンテナンス中は一般ユーザーを sorry ページに誘導しつつ、allowlist に登録した運用者 IP からは本番オリジンにそのままアクセスできる構成です。切替は tfvars 1 行の変更 + PR → plan → apply で完結し、コードの再デプロイも ALB の設定変更も不要です。
メンテ切替の現場ニーズ — 3 パターン
Web サービスの運用現場でメンテナンスモードが必要になるシナリオは、大きく 3 種類に分けられます。
パターン1: 計画メンテナンス
DB マイグレーション、大型機能リリース、インフラ変更など、あらかじめ日程が決まっているメンテナンスです。本記事が扱う主なユースケースです。
- 開始・終了の時刻が事前に確定している
- 担当者は本番動作を確認しながら作業を進めたい
- 一般ユーザーには sorry ページで案内を表示したい
- 対象 IP だけ本番オリジンに通すことで、デプロイ後の動作検証が可能
パターン2: 障害時の手動切替
予期しない障害や大規模エラー発生時に、手動で sorry ページに切り替える対応です。
- オリジンが応答不能または不安定な状態
- 運用者が状況を把握するまでユーザーへの影響を局所化したい
- 第1弾 §6 で実装した origin group の自動 failover が先に動作するため、通常は自動で sorry に切り替わる
障害時の自動フォールバックは第1弾の origin group(CF → ALB 障害 → S3 sorry)で対応済みです。本記事のメンテ切替は意図的・計画的な手動操作のレイヤです。
パターン3: 検証期間のアクセス制限
新機能のリリース後、特定の IP からのみ本番動作を確認できる限定公開期間を設けるケースです。
- カナリアリリースの初期フェーズ
- ステージング環境が本番に昇格した直後の品質確認
- 社内 IP からのみアクセスを許可し、一般には「準備中」と案内する
allowlist ベース切替の意義
メンテナンス中のアクセス制御には複数の方式がありますが、本記事では CloudFront Functions の viewer-request フックで clientIp を評価し、allowlist に含まれる IP は本番オリジンへ、それ以外は sorry S3 へルーティングする設計を採用しています。
この方式の利点を整理します。
①運用者が本番確認できる
sorry ページに切り替えた状態でも、allowlist に自分の IP を追加しておけば本番オリジンにアクセスできます。ALB や EC2/Fargate を再起動した後の動作確認、DB マイグレーション後のデータ確認など、メンテナンス作業の検証フェーズで実用的です。
②オリジン側の変更ゼロ
ALB のターゲットグループ、EC2/ECS の設定、S3 のバケットポリシーを一切変更しません。切替ロジックは CloudFront の CDN レイヤだけに閉じるため、アプリケーション側への影響がありません。
③IP 偽装への耐性
CloudFront Functions の event.request.clientIp はビューアーの実 IPです。X-Forwarded-For ヘッダを見ないため、IP 偽装による allowlist 突破ができません。WAF よりも前段で評価されるため、悪意あるリクエストをオリジンまで到達させません。
本記事の範囲と対象外
本記事が扱う内容と、扱わない内容を明示します。
本記事の範囲(計画メンテのみ):
- CloudFront Functions(viewer-request)+ KeyValueStore による IP allowlist 判定
- Terraform による KVS と CF Functions リソース管理
tfvars変更 → PR → plan → apply → git revert ロールバックの運用フロー- GitHub Actions OIDC による Terraform apply の自動化(cmd_040 シリーズの再利用)
対象外(本記事では扱わない):
- 障害時自動 failover → 第1弾 §6 の origin group で対応済み
- Lambda@Edge / AWS WAF / Route53 weighted routing による同等の実装 → §2 で比較のみ
- KVS 50件超の IP allowlist 管理 → §7 で WAF IPSet との併用を紹介
所要時間とハンズオンコスト
| 項目 | 目安 |
|---|---|
| 初回ハンズオン所要時間 | 60〜90 分 |
| 2回目以降(再適用・ロールバック練習) | 15 分 |
| 検証コスト(1時間) | $0.05 未満 |
| 常時稼働月額(第1弾 + 第2弾合計) | $20〜25 |
CloudFront Functions の呼び出しコストは $0.10 / 100万回です。1日 10万リクエストのサービスでも月額 $0.30 以下に収まります。KVS の読み取りコストは $0.06 / 100万回、ストレージは $0.0009 / GB / 月のため、allowlist 管理 1 件あたりのコストは無視できるレベルです。
前提知識チェックリスト
本記事を読み進める前に、下記の理解があることを推奨します。
- 第1弾読了: CF distribution / cache behavior / ALB VPC Origins / S3 OAC の動作を把握していること
- Terraform 1.9.x:
variable/locals/outputの基本構文。hashicorp/aws ~> 5.60を想定 - GitHub Actions OIDC: cmd_040 シリーズの PR → plan → apply ワークフローの基礎
- CloudFront Functions の存在は知っている: KVS は未体験でも可
- JavaScript の基礎:
import/async/await/ ビット演算が読める程度
本記事で実装する機能ロードマップ
本記事の全体像を把握するために、実装するコンポーネントとその対応セクションをまとめます。
| セクション | 内容 | クリティカルパス |
|---|---|---|
| §1(本節) | メンテ切替の要件整理 | — |
| §2 | 実現方式比較(CFF vs Lambda@Edge vs WAF vs Route53) | — |
| §3 | CloudFront Functions コード実装(viewer-request × KVS lookup) | ✅ |
| §4 | Terraform tfvars 設計(maintenance_mode / allowlist_ips) | ✅ |
| §5 | 切替運用(PR → plan → apply / git revert / branch 保護) | — |
| §6 | 動作確認(5 シナリオ / curl / AWS CLI) | — |
| §7 | 監査・ログ(CF standard logs / real-time logs / WAF 連携余地) | — |
| §8 | コスト・制約・TOC 超過リスク | — |
| §9 | まとめ + 第1弾バックリンク | — |
クリティカルパスは §3(CF Functions コード)と §4(Terraform 設計) です。§3 の JavaScript 実装コードと §4 の Terraform リソース定義が本記事の核心であり、他のセクションはこの 2 節を補足する位置づけです。
第1弾で構築した基盤に CloudFront Functions と KVS を追加するだけで、メンテナンスモードの切替が運用自動化されます。次節では、この構成を選んだ理由を方式比較で検証します。
§2: 実現方式の比較と採用判断
- 「IP allowlist ベースのメンテナンスモード切替」を実現する 4 つの方式を比較する
- CloudFront Functions + KeyValueStore を採用した技術的根拠を理解する
- Lambda@Edge・WAF IPSet・Route53 の各方式が「適切な用途」を正確に把握する
第1弾で組んだ CloudFront + ALB + S3 の 3 層構成に「メンテナンスモード切替」を追加するとき、AWS が提供する主要な方式は 4 つあります。それぞれ実行レイヤが異なり、IP 制御の粒度・レイテンシ・コスト・運用コストも大きく変わります。本セクションでは評価マトリクスを用いてどの方式を選ぶべきかを整理し、第2弾で採用した理由を示します。
4方式の概観
4 つの方式を実行レイヤと IP 制御の仕組みで整理すると以下のようになります。
| # | 方式 | 実行レイヤ | IP 制御の仕組み |
|---|---|---|---|
| (a) | CloudFront Functions + KVS | CDN エッジ(viewer-request) | JS コードが KVS から allowlist を読み振り分け |
| (b) | Lambda@Edge | CDN エッジ(origin-request / viewer-request) | Lambda 関数がリクエストを検査し書き換え |
| (c) | AWS WAF IPSet | CDN エッジ / ALB の前段 | IPSet にマッチしないリクエストを 403 でブロック |
| (d) | Route53 Failover / Weighted Routing | DNS 解決レイヤ | DNS の向き先を切り替え sorry ドメインへ転送 |
評価マトリクス(5 軸・25 点満点)
各方式を「IP allowlist メンテ切替」ユースケースに特化して 5 軸で評価します。
| 評価軸 | 重みpt | (a) CFF+KVS | (b) Lambda@Edge | (c) WAF IPSet | (d) Route53 |
|---|---|---|---|---|---|
| 反映速度(切替〜エッジ到達) | 5 | 5(KVS 更新→秒以内) | 4(デプロイ数分) | 3(IPSet 伝播数秒〜数十秒) | 1(DNS TTL 60s+) |
| コスト(月額常時稼働) | 5 | 5($0.10/1M・無料枠内) | 3($0.60/1M + 実行時間) | 4($1.00/month/WebACL) | 5(hosted zone のみ) |
| IP 制御上限(CIDR 件数) | 5 | 4(KVS 1KB制約で実用50件) | 5(コード内自由) | 3(IPSet 1件あたり10,000 IP) | 1(IP 指定不可) |
| Terraform 対応(IaC 完結度) | 5 | 5(provider 5.37+で完全対応) | 4(関数コードは外部ファイル) | 4(IPSet + WebACL リソースあり) | 5(aws_route53_record) |
| 運用複雑度(メンテ切替操作) | 5 | 5(tfvars 1行 → PR → apply) | 2(関数コード変更 or 環境変数) | 4(IPSet エントリ追加削除) | 1(加重値・フェイルオーバー手動) |
| 合計 | 25 | 24 | 18 | 16 | 12 |
採用: (a) CloudFront Functions + KVS(24/25 点)
4方式の実装量比較
採用方式を選ぶ際は「評価スコア」だけでなく、実際の実装コード量と操作手順の差を体感することが重要です。同じ「IP 判定してメンテページへ振り分ける」ロジックでも、方式によって実装量が 5 〜 10 倍変わります。
(a) CloudFront Functions + KVS(採用):
// 関数本体は §3 で完全版を展開 — ここでは骨格のみ
import cf from 'cloudfront';
async function handler(event) {
const kvsHandle = cf.kvs('<kvsId>');
const config = JSON.parse(await kvsHandle.get('config'));
const clientIp = event.request.clientIp;
if (config.maintenance_mode && !isAllowed(clientIp, config.allowlist)) {
return { statusCode: 302, headers: { location: { value: '/sorry/' } } };
}
return event.request;
}
切替操作: tfvars 1 行変更 → PR → apply(コンソール不要)
(b) Lambda@Edge(参考: 不採用):
// origin-request ハンドラ — デプロイは CloudFront へ関連付けが必要
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const clientIp = request.clientIp;
// SSM / DynamoDB への外部 API 呼び出しが必要
const config = await ssm.getParameter({ Name: '/maintenance/config' }).promise();
const { maintenanceMode, allowlist } = JSON.parse(config.Parameter.Value);
if (maintenanceMode && !isAllowed(clientIp, allowlist)) {
return { status: '302', headers: { location: [{ value: '/sorry/' }] } };
}
return request;
};
切替操作: SSM パラメータ更新 + Lambda 再デプロイ(コンソール or AWS CLI 必要)
(c) WAF IPSet(参考: 不採用・sorry 振り分け不可):
# Terraform: WAF は 403 ブロックのみ。URI 書き換えは CF Functions が別途必要
resource "aws_wafv2_ip_set" "allowlist" {
name= "maintenance-allowlist"
scope = "CLOUDFRONT"
ip_address_version = "IPV4"
addresses = var.allowlist_ips # 最大 10,000 件
}
切替操作: addresses を更新して terraform apply(CFF なしでは sorry 誘導不可)
スコア差(24 vs 18 vs 16)の直感:
– 50 件以下 CIDR でのメンテ切替なら CFF+KVS が圧倒的にシンプル
– WAF は「ブロックしたい」ニーズに強力・「誘導したい」ニーズには CFF が必要
– Lambda@Edge は外部 API 連携が必要なユースケースで真価を発揮
(a) CloudFront Functions + KVS を採用した理由
理由1: 切替速度とコードレス運用の両立
CloudFront Functions はviewer-request イベントで実行されます。リクエストがエッジロケーションに到達した瞬間、オリジンへの転送前に JavaScript コードが走ります。KVS(KeyValueStore)はエッジロケーションのメモリ内に展開されるため、ネットワーク往復なしで await kvsHandle.get('config') が完了します。
メンテ切替の操作は tfvars の 1 行変更だけです。
# env/prod.tfvars — メンテ開始時
maintenance_mode = true
allowlist_ips = ["203.0.113.10/32", "198.51.100.0/24"]
PR を作成し、terraform plan でプレビューを確認し、approve → merge → apply。コンソール操作もシェルスクリプトも不要です。切替操作の全履歴が git log に残る点も運用監査の観点で重要です。
理由2: 1ms 制限内で IP CIDR 判定が現実的に完結する
CF Functions には 1ms の CPU タイムアウトがあります。これはコード全体の実行時間ではなく CPU 時間で、I/O 待ち(KVS lookup の await)は含まれません。KVS から取得した JSON を JSON.parse() し、allowlist 最大 50 件を純粋な JS ビット演算でループする実装は AWS 公式サンプル実績でも 1ms 以内に収まります。
// CIDR 判定の骨格(詳細は §3 で展開)
function ipInCidr(ip, cidr) {
const [base, bits] = cidr.split('/');
const mask = ~(2 ** (32 - Number(bits)) - 1) >>> 0;
return (ipToInt(ip) & mask) === (ipToInt(base) & mask);
}
function ipToInt(ip) {
return ip.split('.').reduce((acc, o) => (acc << 8) + Number(o), 0) >>> 0;
}
50 件超過が見込まれる場合は CIDR の事前集約(スーパーネット化)か、後述する WAF IPSet との併用を検討してください(§7 で詳述)。
理由3: KVS によるコードとデータの分離
Lambda@Edge では allowlist を環境変数やコード内の定数として持つか、SSM Parameter Store / DynamoDB への外部アクセスが必要です。CF Functions はネットワーク I/O を禁止しているため外部 API は呼べませんが、KVS がエッジに共置されているため同等の「コードとデータの分離」を達成できます。
KVS は Terraform の aws_cloudfront_key_value_store + aws_cloudfrontkeyvaluestore_key で管理でき、key='config' に JSON 文字列を書き込む設計にすることで 一切のコード変更なしにメンテ状態・allowlist を動的に更新できます。
(b) Lambda@Edge — 不採用の理由と適切な用途
Lambda@Edge は CloudFront のオリジンリクエスト・レスポンスイベントで実行できる、より高機能な選択肢です。Node.js / Python の実行環境を持ち、外部 API 呼び出しや複雑なヘッダ操作も可能です。
不採用の理由は 3 点です。
1. 実行レイテンシ: コールドスタートで 50-200ms 追加。CF Functions の ~1ms と比較して
体感レイテンシへの影響が無視できない。
2. コスト: $0.60 / 100万リクエスト(CF Functions の 6 倍)+
実行時間 $0.00000625 / 100ms。トラフィックが増えるほど差が拡大する。
3. デプロイ時間: 関数コードを変更するたびに CloudFront への関連付けが必要で
Deployed 状態になるまで 5-10 分かかる。tfvars 変更だけで切替したい本ユースケース
と相性が悪い。
Lambda@Edge が適切な場面: JWT 検証や OAuth コールバック処理など、外部 API 呼び出しが必要な認証・認可ロジック、複数オリジンを動的に切り替える A/B テスト実装、レスポンスヘッダの動的書き換えなど、CF Functions の 1ms / 10KB / no-network 制約では実現できないケースで選択してください。
(c) AWS WAF IPSet — 不採用の理由と適切な用途
WAF IPSet は CloudFront WebACL に紐付け、IPSet にマッチしないリクエストを 403 Forbidden でブロックします。IP アドレスによる制御という点では本ユースケースと重なりますが、メンテナンスモード切替との相性は悪いです。
不採用の理由:
1. sorry への振り分け不可: WAF はブロック(403)しかできない。
「メンテ中の一般ユーザーを /sorry/* に誘導する」リダイレクト・書き換えは
WAF の設計外で、別途 CF Functions か Lambda@Edge が必要になる。
2. 運用複雑度: allowlist 外のリクエストを sorry に向けるには
WAF + CF Functions の 2 層が必要になり、単独ではユースケースを満たせない。
3. コスト: WebACL $5.00/月 + ルール $1.00/月 が固定費として発生。
小規模ハンズオンでは CF Functions 単独(実質無料枠内)より割高。
WAF IPSet が適切な場面: allowlist の CIDR 件数が 50 件を大幅に超える場合(WAF IPSet は 1 セットあたり最大 10,000 IP アドレスを格納可能)や、DDoS・SQLi・XSS フィルタリングと IP 制御を一元管理したい場合に活用してください。CF Functions と WAF は相互排他ではなく補完関係にあります。50 件超 CIDR は WAF IPSet に委譲し、CF Functions はメンテフラグ判定のみに専念する分担も有効です(§8 で cost 比較)。
(d) Route53 Failover / Weighted Routing — 不採用の理由と適切な用途
Route53 の加重ルーティングやフェイルオーバールーティングで DNS の向き先を切り替える方式は、IP allowlist に非対応という根本的な制限があります。
不採用の理由:
1. IP allowlist 非対応: DNS は FQDN の解決しかできない。
「このIPは本番へ、それ以外は sorry へ」という振り分けは DNS レイヤでは不可能。
2. TTL による反映遅延: Route53 の最小 TTL は 60 秒。実際には各 ISP の DNS
リゾルバキャッシュが TTL を延ばすことがあり、切替後 5-10 分間は古い向き先に
アクセスするユーザーが残る。緊急メンテで即時切替が必要な場面では致命的。
3. 双方向切替の非対称性: フェイルオーバーは「プライマリ異常 → セカンダリ」の
一方向ヘルスチェック前提。「計画メンテ → 手動切替 → 計画終了 → 手動復旧」
という双方向の任意タイミング切替とは設計思想が異なる。
Route53 が適切な場面: リージョン間フェイルオーバー(ap-northeast-1 が落ちたら us-east-1 に切り替える)や、Blue/Green デプロイでの重み付きトラフィック移行(90% → 50% → 10% → 0% と段階的に旧バージョンを縮退させる)など、IP 粒度の制御ではなくオリジン全体の向き先を変えるユースケースに最適です。
方式選択のまとめ
4 方式の役割を整理すると、それぞれ設計上の責務が明確に異なります。
| 方式 | 責務 | 本シリーズでの位置づけ |
|---|---|---|
| CF Functions + KVS | エッジでの IP 判定と URI 書き換え | 本記事の主役(§3-§4) |
| Lambda@Edge | 認証・複雑なヘッダ操作・外部 API 連携 | 将来拡張候補(本記事スコープ外) |
| WAF IPSet | 大量 IP のブロック・DDoS / SQLi 防御 | 50件超 CIDR 時の補完策(§8) |
| Route53 Failover | リージョン障害時の自動切替 | 第1弾 origin group との対比(§1) |
- CloudFront Functions + KVS を唯一の IP 判定レイヤとして採用(24/25 点)
- 50 件以下の CIDR allowlist は KVS に格納し、CFF が viewer-request でリアルタイム判定
- 50 件超が必要になった場合は WAF IPSet との併用に切り替える(移行パスを §8 で提示)
- Terraform 管理対象:
aws_cloudfront_key_value_store/aws_cloudfrontkeyvaluestore_key/aws_cloudfront_function(provider 5.37.0+)
次のセクションでは、採用した CF Functions の JavaScript 実装(KVS lookup + CIDR 判定 + URI 書き換え)を本番投入可能なコードとして展開します。
§3: CloudFront Functions コード実装(viewer-request × KVS lookup)
この §3 では、CloudFront Functions(CFF)が viewer-request イベントを受け取ってから、KVS に格納された設定を参照し、sorry ページへのリダイレクトまたは本番オリジンへのパス通しを実行するコードを完成させます。
CFF を動かすのが初めての方でも、「なぜこのコードがこの形になっているのか」を理解しながら読み進められるよう、実装のポイントを細かく分解して解説します。
CF Functions の実行コンテキスト
まず、CF Functions がどのタイミングでどのような情報を持って動作するかを確認します。
CloudFront には4つのイベントポイントがありますが、本記事では viewer-request だけを使います。
| イベント | タイミング | 主な用途 |
|---|---|---|
| viewer-request | CloudFront がリクエストを受信した直後(キャッシュ参照前) | URL 書換、ヘッダ付与、アクセス制御 |
| origin-request | キャッシュミスでオリジンへ転送する直前 | オリジン切替、認証ヘッダ付与 |
| origin-response | オリジンからレスポンスが返ってきた直後 | レスポンスヘッダ操作 |
| viewer-response | CloudFront がクライアントにレスポンスを返す直前 | Cookie 付与、CSP ヘッダ追加 |
viewer-request を使う重要な理由があります。IP allowlist によるアクセス制御はキャッシュヒット前に実行しなければならないからです。origin-request だとキャッシュヒット時に関数が呼ばれず、allowlist チェックがスキップされてしまいます。
event.viewer.ip vs event.request.clientIp
CF Functions runtime 2.0 では event.viewer.ip を使います。
// runtime 1.0 (非推奨)
const clientIp = event.request.clientIp; // deprecated
// runtime 2.0 (推奨)
const clientIp = event.viewer.ip; // viewer の実 IP
event.viewer.ip は CloudFront エッジロケーションに到達した実際のクライアント IP です。これは X-Forwarded-For ヘッダとは別物で、ヘッダ値の偽造を受けません。
X-Forwarded-For を使ってしまうと、攻撃者が X-Forwarded-For: 203.0.113.1 のように偽装したヘッダを送信して allowlist をすり抜けることができます。event.viewer.ip はこの偽装に影響されません。
runtime 1.0 の event.request.clientIp は runtime 2.0 でも動作しますが、AWS Developer Guide では event.viewer.ip の使用が推奨されています。
JavaScript runtime 2.0 の import 構文
CF Functions runtime 2.0 では ES modules の import 構文が利用できます。KVS を使うには cloudfront モジュールを import する必要があります。
import cf from 'cloudfront';
この1行がないと KVS ハンドルを取得できません。runtime 1.0 には import 構文がなく、KVS も使えません。KVS と連携する CF Functions は必ず runtime 2.0 にする必要があります。
KVS ハンドルの取得
KVS ハンドルは関数コードの外側(グローバルスコープ)で初期化します。
import cf from 'cloudfront';
const KVS_ID = 'REPLACE_WITH_KVS_ID'; // Terraform の templatefile で注入
const kvsHandle = cf.kvs(KVS_ID);
KVS_ID はデプロイ時に Terraform の templatefile 関数で実際の KVS ID(UUID 形式)に置換されます(§4 で詳説)。
グローバルスコープで初期化するのはパフォーマンス上の理由です。CF Functions はリクエストごとに関数を呼び出しますが、グローバル初期化は関数コンテナの初回起動時に一度だけ実行されます。handler 内で毎回 cf.kvs() を呼ぶよりも効率的です。
KVS からの lookup
KVS からデータを取得するには kvsHandle.get() を使います。
const raw = await kvsHandle.get('config', { format: 'json' });
引数のポイント:
- key:
'config'— 本記事では allowlist 設定を単一キーで管理します。KVS に複数のキーを持たせることも可能ですが、CIDR リストと maintenance_mode フラグをひとつの JSON オブジェクトにまとめることで、lookup が 1 回で完結します { format: 'json' }: KVS value は保存時も取得時も文字列ですが、format: 'json'を指定すると KVS API 側でJSON.parse()した結果を返します。ただし、環境によっては文字列のまま返ることがあるため、後続コードでtypeof raw === 'string'チェックを入れます
KVS lookup が失敗した場合(KVS 未設定・ID 誤り・API 障害)は try/catch で捕捉し、fail-open ポリシー(本番通し)を採用します。この設計判断については「§3 解説ポイント」で詳述します。
IPv4 CIDR 判定ロジック
allowlist は ["203.0.113.10/32", "198.51.100.0/24"] のような IPv4 CIDR リストです。クライアント IP がこのリストのいずれかに含まれるかを判定するには、ビット演算を使います。
function ipv4ToInt(ip) {
const parts = ip.split('.');
return ((+parts[0] << 24) | (+parts[1] << 16) | (+parts[2] << 8) | +parts[3]) >>> 0;
}
function ipInCidrList(clientIp, cidrList) {
const ipInt = ipv4ToInt(clientIp);
for (let i = 0; i < cidrList.length; i++) {
const [net, prefixStr] = cidrList[i].split('/');
const prefix = parseInt(prefixStr, 10);
const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
const netInt = ipv4ToInt(net) & mask;
if ((ipInt & mask) === netInt) return true;
}
return false;
}
>>> 0 は JavaScript の 符号なし右シフトで、32 ビット符号なし整数に変換します。~0 << 28 のようなビット演算で負の値が生まれることがありますが、>>> 0 で確実に正の整数に正規化できます。
このアルゴリズムは CF Functions の 1ms 制約内に収まります。allowlist 50 件程度のループ + ビット演算は数十マイクロ秒で完了します(AWS サンプル実績ベース)。
完成版関数コード
§3 骨格に基づく本番投入可能な完成版です。
// maintenance.js — viewer-request hook
// CF Functions runtime: cloudfront-js-2.0
import cf from 'cloudfront';
const KVS_ID = '${kvs_id}'; // terraform templatefile で注入
const kvsHandle = cf.kvs(KVS_ID);
function ipv4ToInt(ip) {
const parts = ip.split('.');
return ((+parts[0] << 24) | (+parts[1] << 16) | (+parts[2] << 8) | +parts[3]) >>> 0;
}
function ipInCidrList(clientIp, cidrList) {
const ipInt = ipv4ToInt(clientIp);
for (let i = 0; i < cidrList.length; i++) {
const [net, prefixStr] = cidrList[i].split('/');
const prefix = parseInt(prefixStr, 10);
const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
const netInt = ipv4ToInt(net) & mask;
if ((ipInt & mask) === netInt) return true;
}
return false;
}
async function handler(event) {
const request = event.request;
const clientIp = event.viewer.ip;
let config;
try {
const raw = await kvsHandle.get('config', { format: 'json' });
config = typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (e) {
// KVS lookup 失敗時は本番通し(fail-open)
return request;
}
// 平常モード
if (!config.maintenance_mode) {
return request;
}
// メンテモード: allowlist 内 IP は本番へ
if (ipInCidrList(clientIp, config.allowlist || [])) {
return request;
}
// allowlist 外 → sorry へ URI 書換
request.uri = '/sorry/index.html';
return request;
}
ファイル名: maintenance.js.tpl(Terraform の templatefile で ${kvs_id} を置換するためテンプレートファイルとして管理)
Step-by-step 解説
関数の処理フローを 4 ステップで整理します。
Step 1: KVS lookup
const raw = await kvsHandle.get('config', { format: 'json' });
config = typeof raw === 'string' ? JSON.parse(raw) : raw;
KVS から config キーの値を取得します。KVS に格納された JSON の構造は次のとおりです。
{
"maintenance_mode": false,
"allowlist": ["203.0.113.10/32"],
"updated_at": "2026-04-20T15:30:00Z",
"updated_by": "terraform-apply"
}
updated_at / updated_by は監査目的のフィールドで、CF Functions のルーティングロジックには影響しません。
Step 2: maintenance_mode 判定
if (!config.maintenance_mode) {
return request;
}
maintenance_mode: false の場合はここで即座に return し、以降のチェックをスキップします。平常時のリクエストは全て ALB へ転送されます。
Step 3: clientIp CIDR match
if (ipInCidrList(clientIp, config.allowlist || [])) {
return request;
}
メンテモード中でも allowlist に含まれる IP は本番アクセスを許可します。config.allowlist が undefined の場合に備えて || [] でフォールバックしています。
Step 4: URI 書換(sorry へ)
request.uri = '/sorry/index.html';
return request;
allowlist 外のリクエストは request.uri を /sorry/index.html に書き換えて return します。これは HTTP リダイレクトではなく CloudFront 内部での URI rewrite です。
クライアント側では URL が変化せず、CloudFront が内部的に /sorry/index.html を cache behavior /sorry/* に照合し、S3 sorry オリジンから sorry ページを取得します。S3 sorry オリジンと cache behavior /sorry/* の設定は第1弾 §4 で構築済みです。
§3 解説ポイント 6 点
- event.viewer.ip を使う: runtime 2.0 推奨。X-Forwarded-For 偽装に影響されない実クライアント IP。
- ビット演算 CIDR 判定: IPv4 のみ対応(IPv6 は §7 の発展課題)。allowlist 50 件でも 1ms 制約内に収まる。
- fail-open ポリシー: KVS 読み取り失敗時は本番通し。メンテ誤発動(全アクセス遮断)より本番停止(全アクセス通過)の方が被害が少ないという設計判断。
- URI rewrite(302 redirect ではない): クライアントの URL は変わらず、CloudFront 内部で処理が完結。302 だと sorry ページの URL がブラウザに記録される問題がある。
- 1ms / 10KB 制約を満たす設計: ループ + ビット演算の組み合わせはシンプルかつ高速。allowlist 50 件程度まではコードサイズも 10KB 未満に収まる。
- stateless / no network: CF Functions はリクエスト間で状態を持てず、HTTP 呼び出しも不可。KVS が唯一の外部データソースで、lookup は非同期 API として提供されている。
CF Functions の制約まとめ
本番運用前に必ず把握しておくべき制約を整理します。
| 制約項目 | 値 | 備考 |
|---|---|---|
| CPU 実行時間 | 1 ms | 超過すると Function execution error → リクエストはオリジンに fallback |
| コードサイズ | 10 KB | minify 不要だが allowlist 100件超になると超過の恐れ |
| メモリ | 2 MB | 通常の IP allowlist チェックでは問題にならない |
| ネットワーク I/O | 不可 | 外部 HTTP 呼び出し不可。KVS が唯一の外部ストレージ |
| ステートレス | 必須 | リクエスト間でグローバル変数は保持されない |
| KVS 関連付け | 1 KVS / 1 function | 1つの関数に関連付けられる KVS は最大1つ |
| イベントタイプ | viewer-request / viewer-response のみ | origin-request / origin-response は Lambda@Edge |
TOC(実行時間超過)挙動: 1ms を超過しても関数はエラーを返さず、AWS の既定仕様でリクエストはオリジンにフォールバックします。メンテモード中に allowlist チェックが超過した場合、本番アクセスが許可されてしまうリスクがあります。allowlist が 50 件を超えてきたら CIDR を事前に集約(supernet)するか、§7 の WAF IPSet 連携を検討してください。
KVS の制約と実運用上の注意
| 制約項目 | 値 |
|---|---|
| KVS ストレージ上限 | 5 MB / store |
| key サイズ | 最大 512 bytes |
| value サイズ | 最大 1 KB |
| key 数 | ストレージ上限以内 |
value 1KB 制約が最も重要です。allowlist を CIDR 文字列のリストとして格納する場合、203.0.113.100/32 は 16 文字です。JSON のオーバーヘッドを含めると、実用上の allowlist 上限は 50 件程度が目安です。
50 件を超える場合の対処:
- CIDR 集約:
/32のホスト単位の CIDR を/24のサブネット単位にまとめ、エントリ数を削減 - WAF IPSet 連携: allowlist が大規模になった場合は §7 で紹介する WAF IPSet との併用を検討
§4: Terraform tfvars 設計(maintenance_mode / allowlist_ips)
§3 で完成させた maintenance.js.tpl を Terraform で管理・デプロイします。この §4 では KVS リソース、CF Function リソース、CloudFront Distribution への関連付け、variables.tf 設計、terraform.tfvars の切替操作手順までを網羅します。
前提: Terraform provider バージョン
aws_cloudfront_key_value_store と aws_cloudfrontkeyvaluestore_key リソースは hashicorp/aws provider 5.37.0 以降で追加されました。本記事では第1弾と揃えて ~> 5.60 を使用します。
# versions.tf(第1弾から変更なし・確認のみ)
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
}
5.36.x 以前では KVS リソースが存在しないため terraform init がエラーになります。terraform version と terraform providers でバージョンを確認してください。
variables.tf 設計
3つの変数を追加します。第1弾の variables.tf に追記する形で管理します。
# variables.tf — 第2弾追加分
variable "maintenance_mode" {
type = bool
default = false
description = "true = allowlist 外は sorry へ振替 / false = 全 IP 本番通過"
}
variable "allowlist_ips" {
type = list(string)
default = []
description = "メンテ中に本番アクセス可とする IPv4 CIDR のリスト(例: 203.0.113.10/32)"
validation {
condition = alltrue([
for cidr in var.allowlist_ips :
can(regex("^([0-9]{1,3}\\.){3}[0-9]{1,3}/(3[0-2]|[1-2]?[0-9])$", cidr))
])
error_message = "allowlist_ips は IPv4 CIDR 形式のみ許可(例: 203.0.113.10/32)"
}
validation {
condition = length(var.allowlist_ips) <= 50
error_message = "allowlist_ips は最大 50 件(KVS value 1KB 制約)"
}
}
variable "actor" {
type = string
default = "terraform-apply"
description = "apply 実行者識別子(監査用)。GHA では github.actor を渡す"
}
2つの validation block のポイント:
- CIDR 形式チェック:
can(regex(...))で IPv4 CIDR の形式を強制します。terraform plan時点で不正な CIDR を検出し、デプロイ前に弾きます - 50件上限バリデーション: KVS value 1KB 制約に対する安全装置です。
length(var.allowlist_ips) <= 50を超えるとplanがエラーになります
validation block は Terraform 0.13.0 以降で利用可能で、Terraform 1.9.x では完全にサポートされています。
aws_cloudfront_key_value_store リソース
# modules/cloudfront/maintenance.tf
resource "aws_cloudfront_key_value_store" "maintenance_config" {
name = "${var.project_name}-maintenance-config"
comment = "Maintenance mode allowlist config for ${var.environment}"
}
KVS 自体はシンプルなリソースです。name は AWS アカウント内でグローバルにユニークである必要があります。${var.project_name}-${var.environment}-maintenance-config のように環境名を含めると、staging / production で同一 AWS アカウントを使う場合も安全です。
KVS はリージョンレスのグローバルリソースです。provider "aws" の region 設定に関わらず、KVS は CloudFront と同様にグローバルに作成されます。
aws_cloudfrontkeyvaluestore_key リソース
resource "aws_cloudfrontkeyvaluestore_key" "config" {
key_value_store_arn = aws_cloudfront_key_value_store.maintenance_config.arn
key = "config"
value = jsonencode({
maintenance_mode = var.maintenance_mode
allowlist = var.allowlist_ips
updated_at = timestamp()
updated_by = var.actor
})
}
jsonencode() で Terraform の型から JSON 文字列を生成し、KVS value として格納します。
timestamp() の注意点: Terraform の timestamp() は plan / apply 実行時刻を返します。plan と apply で値が変わるため、毎回 aws_cloudfrontkeyvaluestore_key リソースに差分が生じます。これは意図した挙動で、「いつ誰が apply したか」を KVS に記録するためです。
timestamp() が毎回 diff を生むのを避けたい場合は lifecycle { ignore_changes = [value] } を使う方法もありますが、本記事では動的更新を目的とした設計のため採用しません。動的更新パターンと静的パターンの比較は次の節で解説します。
etag 管理と lifecycle の設計判断
KVS value の管理には 2 つのパターンがあります。
パターン A: Terraform が value を常に管理(本記事採用)
resource "aws_cloudfrontkeyvaluestore_key" "config" {
key_value_store_arn = aws_cloudfront_key_value_store.maintenance_config.arn
key = "config"
value= jsonencode({
maintenance_mode = var.maintenance_mode
allowlist = var.allowlist_ips
updated_at = timestamp()
updated_by = var.actor
})
}
maintenance_modeやallowlist_ipsの変更はterraform applyで即座に KVS に反映されるupdated_at/updated_byも自動更新される- 監査証跡が git history に残る(tfvars の変更がコミットに記録)
パターン B: lifecycle ignore_changes で Terraform 外の更新を許容
resource "aws_cloudfrontkeyvaluestore_key" "config" {
key_value_store_arn = aws_cloudfront_key_value_store.maintenance_config.arn
key = "config"
value= jsonencode({
maintenance_mode = false
allowlist = []
})
lifecycle {
ignore_changes = [value]
}
}
- AWS コンソールや CLI から KVS を直接変更しても
planで差分が出ない - 緊急時の console 操作を許容したい場合に有効
- 一方で、terraform state と実際の KVS 状態がずれる(drift)リスクがある
§5 の切替運用では パターン A(Terraform が value を管理)を原則とし、コンソール直接変更は Q4(緊急・非推奨)の位置付けで紹介します。
aws_cloudfront_function リソース
resource "aws_cloudfront_function" "maintenance" {
name = "${var.project_name}-maintenance-viewer-request"
runtime = "cloudfront-js-2.0"
publish = true
code = templatefile("${path.module}/maintenance.js.tpl", {
kvs_id = aws_cloudfront_key_value_store.maintenance_config.id
})
key_value_store_associations = [
aws_cloudfront_key_value_store.maintenance_config.arn
]
}
各フィールドの解説:
runtime = "cloudfront-js-2.0": KVS を使うには runtime 2.0 が必須publish = true:falseにすると DEVELOPMENT ステージに保存されます。本番に反映するにはtrueが必要code = templatefile(...):maintenance.js.tplの${kvs_id}プレースホルダーを KVS の実際の ID(UUID)で置換したコードを埋め込みますkey_value_store_associations: 1 つの関数に関連付けられる KVS は最大 1 つ(AWS 公式制約)。ARN のリスト形式ですが実質 1 エントリのみ
templatefile を使う理由: maintenance.js.tpl 内の '${kvs_id}' を Terraform apply 時の KVS ID で置換することで、コードに KVS ID をハードコードせずに済みます。KVS を再作成しても templatefile が自動で最新 ID を反映します。
CF Distribution への function_association 追加
第1弾で作成した aws_cloudfront_distribution リソースに function_association を追加します。
# modules/cloudfront/distribution.tf — 第1弾からの差分のみ
resource "aws_cloudfront_distribution" "main" {
# ... 第1弾の設定は変更なし ...
default_cache_behavior {
# ... 既存設定は変更なし ...
# 第2弾で追加
function_association {
event_type= "viewer-request"
function_arn = aws_cloudfront_function.maintenance.arn
}
}
}
default_cache_behavior の function_association は viewer-request と viewer-response の 2 種類が使えます。本記事では viewer-request のみ使用します。
/sorry/* cache behavior には function_association は不要です。sorry ページへのアクセスは CF Functions の URI rewrite の結果として発生するもので、ユーザーが直接 /sorry/ を叩いても適切に処理されます。
terraform.tfvars の切替操作
メンテナンスモードの切替は terraform.tfvars を編集して terraform apply するだけです。
平常時(デフォルト):
# env/prod.tfvars
maintenance_mode = false
allowlist_ips = []
actor= "terraform-apply"
メンテ開始時(allowlist に自分の IP を追加):
# env/prod.tfvars
maintenance_mode = true
allowlist_ips = [
"203.0.113.10/32",# 担当者A 自宅 IP
"198.51.100.5/32",# 担当者B オフィス IP
]
actor= "yamada-taro"
メンテ終了時(平常に戻す):
# env/prod.tfvars
maintenance_mode = false
allowlist_ips = []
actor= "yamada-taro"
apply コマンド:
# staging 環境で動作確認
terraform plan -var-file=env/stg.tfvars
terraform apply -var-file=env/stg.tfvars
# 本番環境への適用
terraform plan -var-file=env/prod.tfvars
terraform apply -var-file=env/prod.tfvars
terraform plan で KVS value のみ変更(Distribution の変更なし)であることを確認してから apply してください。CF Distribution の設定変更は反映に数分かかりますが、KVS value の変更は数秒〜10 秒で CF 層に反映されます。
動的切替フロー(tfvars → KVS → CF 即時反映)
tfvars 変更
↓
terraform apply
↓
aws_cloudfrontkeyvaluestore_key value 更新(数秒)
↓
CloudFront エッジが KVS 変更を検知
↓
次のリクエストから新しい maintenance_mode / allowlist が反映(数秒〜10 秒)
CF Function コードの変更は不要です。maintenance_mode と allowlist_ips は KVS から読み取るため、tfvars の変更と KVS 更新だけで切替が完了します。関数を再デプロイ(publish = true で再 apply)する必要はありません。
これが CFF + KVS アーキテクチャの最大のメリットです。コードの変更なしに運用パラメータを変更でき、コードレビュー不要で運用チームが tfvars のみを操作できます。
環境分離(env/prod.tfvars / env/stg.tfvars)推奨構造
infrastructure/
├── modules/
│└── cloudfront/
│ ├── main.tf
│ ├── distribution.tf
│ ├── maintenance.tf# § 4 で追加(KVS + CF Function)
│ ├── maintenance.js.tpl # § 3 のテンプレートコード
│ ├── variables.tf # § 4 で追加分含む
│ └── outputs.tf
├── env/
│├── prod.tfvars # 本番: maintenance_mode = false
│└── stg.tfvars # ステージング: maintenance_mode / allowlist テスト用
├── versions.tf
└── backend.tf
env/prod.tfvars と env/stg.tfvars をそれぞれ git で管理することで、メンテ切替のすべての操作がコミットとして git history に残ります。「いつ誰がメンテを開始し、いつ終了したか」を監査証跡として保持できます。
- provider バージョン確認: hashicorp/aws ~> 5.60(5.37.0 以降で KVS 対応)
- validation block: CIDR 形式チェック(regex)と 50 件上限チェック(length)で plan 時に弾く
- templatefile で KVS ID を注入: ハードコードせずに動的に解決
- key_value_store_associations は 1 エントリのみ: AWS の 1 KVS / 1 function 制約
- KVS value のみ変更→数秒反映: Distribution 再デプロイ不要でスピーディな切替
- env/prod.tfvars を git 管理: 監査証跡を git history に自動保存
← 第1弾: CloudFront × ALB × S3 基礎へ戻る
§5: 切替運用(PR → plan → apply / git revert → apply / branch 保護)
§4 で設計した maintenance_mode と allowlist_ips の tfvars を安全に操作するための運用フローを確立します。GitHub Actions ワークフロー、CODEOWNERS、branch protection、緊急ロールバックまで整備します。
GitHub Actions ワークフロー設計
2 本のワークフローで PR 駆動の apply を実現します。
| ワークフロー | トリガ | 実行内容 |
|---|---|---|
terraform-plan.yml | PR open / synchronize | plan + 結果を PR コメント投稿 |
terraform-apply.yml | main マージ | apply(OIDC Role assume) |
.github/workflows/terraform-plan.yml
name: Terraform Plan
on:
pull_request:
paths: ['infrastructure/**', 'env/*.tfvars']
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/TerraformPlanRole
aws-region: ap-northeast-1
- uses: hashicorp/setup-terraform@v3
with: { terraform_version: "~> 1.7" }
- run: terraform init -backend-config=backend.hcl
working-directory: infrastructure
- name: Terraform Plan
id: plan
working-directory: infrastructure
run: |
terraform plan -var-file=env/prod.tfvars \
-no-color -out=tfplan.binary 2>&1 | tee plan_output.txt
continue-on-error: true
- name: Post Plan to PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const output = fs.readFileSync('infrastructure/plan_output.txt', 'utf8');
await github.rest.issues.createComment({
...context.repo, issue_number: context.issue.number,
body: `## Terraform Plan\n\`\`\`\n${output.substring(0,60000)}\n\`\`\``
});
.github/workflows/terraform-apply.yml
name: Terraform Apply
on:
push:
branches: [main]
paths: ['infrastructure/**', 'env/*.tfvars']
permissions:
id-token: write
contents: read
jobs:
apply:
runs-on: ubuntu-latest
environment: production# GitHub Environment で追加承認を強制可
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/TerraformApplyRole
aws-region: ap-northeast-1
- uses: hashicorp/setup-terraform@v3
with: { terraform_version: "~> 1.7" }
- run: terraform init -backend-config=backend.hcl
working-directory: infrastructure
- run: terraform apply -var-file=env/prod.tfvars -auto-approve -no-color
working-directory: infrastructure
Plan Role は読み取り権限のみ、Apply Role は KVS value 更新を含む書き込み権限を持ちます。PR ブランチから直接 apply を実行させないことで、未レビューの変更が本番に適用されるリスクを排除します。
environment: production を apply ジョブに設定すると、GitHub の Environment 保護ルール(Required reviewers)を経由してからワークフローが実行されます。「main マージ → GHA approve → apply」の 2 段階承認が必要なシチュエーション(金融系・個人情報を扱うシステム等)ではこの設定が有効です。通常のメンテ切替では CODEOWNERS による PR レビューで十分です。
CODEOWNERS 設定
# .github/CODEOWNERS
*.tf@infra-team
env/*.tfvars @infra-team @oncall-lead
infrastructure/modules/cloudfront/maintenance.js.tpl @infra-team
env/*.tfvars に @oncall-lead を追加することで、メンテナンス切替の承認者が常にオンコール担当者に限定されます。
branch protection 設定(main ブランチ)
Settings > Branches > Branch protection rules で設定します。
Branch name pattern: main
☑ Require a pull request before merging
☑ Require approvals: 1
☑ Dismiss stale reviews when new commits are pushed
☑ Require review from Code Owners
☑ Require status checks to pass before merging
Required: plan (terraform-plan.yml)
☑ Require conversation resolution before merging
☑ Restrict who can push to matching branches
→ 全員の直 push を禁止(Actions service account を含む)
「Restrict who can push」による直 push 禁止が特に重要です。これがないと焦ったオペレーターが branch protection を飛ばして plan レビューなしに apply を走らせる事故が起きます。
GitHub Enterprise をお使いの場合は Ruleset(新 UI)での設定も可能です。Ruleset は Organization レベルで複数リポジトリに一括適用できるため、インフラリポジトリが複数存在する場合は Ruleset での統一管理を推奨します。
Q1: 平常 → メンテナンス切替
# 1. feature ブランチ作成
git checkout -b maintenance/20240520-scheduled-maint
# 2. tfvars 編集
# env/prod.tfvars:
#maintenance_mode = true
#allowlist_ips = ["203.0.113.10/32", "198.51.100.0/24"]
#actor= "yamada-taro"
git add env/prod.tfvars
git commit -m "maintenance: enable 2024-05-20 02:00-04:00 JST (yamada-taro)"
git push origin maintenance/20240520-scheduled-maint
# 3. PR 作成(terraform-plan.yml が自動起動)
gh pr create --title "maintenance: enable 2024-05-20 02:00-04:00 JST"
# 4. plan 確認ポイント: "Plan: 0 to add, 0 to change, 1 to update."
# KVS value のみ変更であることを確認 → @oncall-lead に approve 依頼
# 5. PR マージ → terraform-apply.yml が自動 apply
# CF 反映確認(数秒〜10 秒)
curl -s -o /dev/null -w "%{http_code}" https://www.example.com/
# → 503(Sorry Page)が返ることを確認
Q2: メンテナンス → 平常復帰
git checkout -b maintenance/20240520-restore
# env/prod.tfvars: maintenance_mode = false, allowlist_ips = []
git add env/prod.tfvars
git commit -m "maintenance: restore normal mode 2024-05-20 03:45 JST"
git push origin maintenance/20240520-restore
gh pr create --title "maintenance: restore normal mode"
# → plan 確認 → approve → merge → apply
Q3: ロールバック(git revert → apply)
マージ後に設定ミスを発見した場合は git revert で即時ロールバックします。
# マージコミットのハッシュを確認
git log --oneline -5 main
# revert コミット作成(マージコミットは -m 1 が必要)
git revert -m 1 <merge-commit-hash>
git push origin main
# → terraform-apply.yml が自動起動し、前の状態に apply される
git revert は新しいコミットを追加するため元の変更が git history に残り、監査証跡が保持されます。
git reset --hard でマージコミットを消すには force push が必要になり、remote history を破壊します。ロールバックには必ず git revert を使ってください。
Q4: 緊急対応(Console から KVS 直接変更・非推奨)
PR を出す時間がない夜間障害などの緊急手段です。通常運用では使用禁止。
# KVS value を直接変更
aws cloudfront-keyvaluestore put-key \
--kvs-arn arn:aws:cloudfront::123456789012:key-value-store/<KVS_ID> \
--key config \
--value '{"maintenance_mode":false,"allowlist":[]}' \
--if-match $(aws cloudfront-keyvaluestore describe-key-value-store \
--kvs-arn <KVS_ARN> --query ETag --output text)
# 事後: tfvars を実態に合わせて更新 → terraform apply(plan で差分 0 を確認)
直接変更は数秒で CF に反映されます。ただし Terraform state と乖離(drift)するため翌朝必ず apply し直すこと。
よくある事故: allowlist 記述ミスで全アクセス遮断
maintenance_mode = true 状態で allowlist_ips に意図した IP が含まれないと社内 IP を含む全アクセスが sorry ページに到達します。
# NG 例(validation で plan エラー)
allowlist_ips = ["203.0.113.10/332"] # /332 は無効 CIDR
# NG 例(validation 通過するが意図と乖離)
allowlist_ips = ["203.0.113.0/32"] # .0 はネットワークアドレスの場合あり
全遮断時の復旧手順:
1. Q4 緊急手順で maintenance_mode: false に直接変更 → 全 IP 解放
2. 正しい allowlist で Q1 手順を再実行(plan の CIDR 変更内容を目視確認)
CODEOWNERS によるレビューが全遮断の最後の防波堤です。plan コメントの allowlist 値をレビュアーが確認する習慣をつけることが重要です。
また、§4 の validation block により CIDR 形式が不正な場合は terraform plan 段階でエラーになります。PR を出した段階で CI が弾いてくれるため、「plan コメントが投稿される = CIDR 形式は最低限正しい」 が保証されています。レビュアーは形式チェックより意図した IP アドレスが正しく含まれているかの確認に集中できます。
- GHA Plan: PR open で自動起動、plan 結果を PR コメントに投稿
- GHA Apply: main マージ後に自動 apply(OIDC Role assume)
- CODEOWNERS:
env/*.tfvars変更は@oncall-lead承認必須 - branch protection: PR review / status check / 直 push 禁止
- ロールバック:
git revert -m 1 <merge-commit>が正規手順 - 緊急時: KVS 直接変更 → 事後 drift 修正を必ず実施
curl で検証する動作確認 5 シナリオを実施します。偽装ヘッダ・KVS 直接変更のリアルタイム反映まで網羅します。第 1 弾(基礎構築編)の §6 アーキテクチャ解説と合わせて読むとより深く理解できます。§6: 動作確認(5シナリオ × curl + KVS CLI)
§5 で構築した PR 駆動の tfvars 運用が正しく機能するかを 5 つのシナリオで検証します。curl による HTTP ステータス確認と、KVS CLI によるリアルタイム反映確認を組み合わせます。
事前準備: 自分の IP アドレスを確認する
# 現在の送信元 IP(IPv4)を確認
MY_IP=$(curl -s https://checkip.amazonaws.com)
echo "My IP: $MY_IP"
# → 例: 203.0.113.10
# CIDR 形式に変換(/32 でホスト単体を指定)
MY_CIDR="${MY_IP}/32"
確認先の URL を変数に入れておきます。
# CloudFront ディストリビューションの URL(自分の環境に合わせて変更)
CF_URL="https://www.example.com"
SORRY_PATH="/sorry/index.html"
シナリオ 1: maintenance_mode=false — 全 IP が本番到達
平常モードでは allowlist の有無に関わらず全 IP がオリジン(ALB)に到達します。
# tfvars 確認
grep "maintenance_mode" env/prod.tfvars
# → maintenance_mode = false
# HTTP ステータス確認
curl -s -o /dev/null -w "%{http_code}" "${CF_URL}/"
# → 200(本番コンテンツ)
curl -s -o /dev/null -w "%{http_code}" "${CF_URL}/sorry/index.html"
# → 200(sorry ページ自体は直アクセスで表示可。CF Functions は通常リクエストのみ介入)
確認ポイント: レスポンスヘッダ x-cache: Hit from cloudfront または Miss from cloudfront が返ればCF Functions のコードパスを通過しています。
curl -sI "${CF_URL}/" | grep -i "x-cache\|x-amz-cf-id"
シナリオ 2: maintenance_mode=true + 自 IP を allowlist に含む → 本番到達
メンテナンス中でも作業担当者の IP は本番にアクセスできることを確認します。
# tfvars 編集してメンテ有効化(PR 経由 or テスト環境で直接変更)
cat env/prod.tfvars
# maintenance_mode = true
# allowlist_ips = ["203.0.113.10/32"]← MY_IP を含む
# actor= "yamada-taro"
# apply 後 CF 反映待ち(通常 5-10 秒)
sleep 10
# 自 IP からのアクセス → 本番到達
curl -s -o /dev/null -w "status: %{http_code}\n" "${CF_URL}/"
# → status: 200(本番コンテンツ)
CF Functions は event.viewer.ip(JS runtime 2.0)でCloudFront が認識する送信元 IP を取得します。NAT ゲートウェイや VPN 出口 IP を allowlist に登録している場合はその IP が判定されます。
シナリオ 3: maintenance_mode=true + 自 IP を allowlist に含まない → sorry 到達
非 allowlist IP からアクセスすると URI が /sorry/index.html に書き換えられ、S3 sorry オリジンのコンテンツが返ります。
# モバイルテザリング or 別拠点(allowlist 外の IP)からテスト
# VPN をオフにした状態でもよい
OTHER_IP=$(curl -s https://checkip.amazonaws.com)# 別拠点 IP
echo "Testing from: $OTHER_IP"
curl -s -o /dev/null -w "status: %{http_code}\nurl: %{url_effective}\n" \
-L "${CF_URL}/"
# → status: 200(sorry ページ)
#url_effective は変わらない(302 redirect ではなく URI rewrite のため)
# sorry コンテンツが返っているか body で確認
curl -s "${CF_URL}/" | grep -i "sorry\|メンテナンス\|maintenance"
-L フラグを付けても URL が変わらないことに注目してください。CF Functions は302 リダイレクトではなく URI 書換(rewrite)のため、ブラウザのアドレスバーは / のまま sorry コンテンツが返ります。
シナリオ 4: X-Forwarded-For 偽装試行 → clientIp 優先で無効化
悪意あるリクエストが X-Forwarded-For に allowlist の IP を偽装しても、CF Functions は event.viewer.ip(CloudFront が直接認識する送信元 IP)を使うため無効です。
# X-Forwarded-For に許可 IP を偽装して送信
ALLOWED_IP="203.0.113.10"
curl -s -o /dev/null -w "status: %{http_code}\n" \
-H "X-Forwarded-For: ${ALLOWED_IP}" \
"${CF_URL}/"
# → status: 200(sorry ページ)
# X-Forwarded-For ヘッダは CF Functions の ipInCidrList() では参照されない
なぜ偽装が無効か: event.viewer.ip は CloudFront のエッジが TCPレベルで確認した接続元 IP です。HTTP ヘッダとは独立しており、クライアント側から改ざんできません。event.request.headers['x-forwarded-for'] を参照するコードを書けば偽装可能になるため、必ず event.viewer.ip を使うことが重要です。
シナリオ 5: Console から KVS 直接変更 → 数秒でリアルタイム反映(drift 教材)
§5 Q4 の緊急手順が実際に数秒で反映されることを確認し、drift の発生も体験します。
# ① KVS ARN を取得
KVS_ARN=$(aws cloudfront describe-functions \
--query "FunctionList.Items[?Name=='<PROJECT>-maintenance-viewer-request'].FunctionMetadata.FunctionARN" \
--output text | xargs -I{} aws cloudfront get-function \
--name {} --query 'FunctionSummary.FunctionMetadata.FunctionARN' --output text)
# または terraform output で取得
KVS_ARN=$(cd infrastructure && terraform output -raw kvs_arn)
# ② 現在の ETag を取得
ETAG=$(aws cloudfront-keyvaluestore describe-key-value-store \
--kvs-arn "${KVS_ARN}" \
--query ETag --output text)
# ③ maintenance_mode を false に直接変更
aws cloudfront-keyvaluestore put-key \
--kvs-arn "${KVS_ARN}" \
--key config \
--value '{"maintenance_mode":false,"allowlist":[],"updated_at":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","updated_by":"console-direct"}' \
--if-match "${ETAG}"
# ④ 数秒後に確認(CF 反映は通常 5 秒以内)
sleep 5
curl -s -o /dev/null -w "status: %{http_code}\n" "${CF_URL}/"
# → status: 200(本番コンテンツに戻る)
# ⑤ terraform plan で drift を確認
cd infrastructure
terraform plan -var-file=env/prod.tfvars | grep "# aws_cloudfrontkeyvaluestore_key"
# → "# aws_cloudfrontkeyvaluestore_key.config will be updated in-place"
# terraform state と KVS 実態が乖離(drift)していることが確認できる
この drift を放置すると次の terraform apply 時に tfvars の値で上書きされます。事後に必ず apply し直して state を揃えるのが §5 Q4 の正規手順です。
複数拠点テスト推奨
| テスト拠点 | IP の特性 | 確認内容 |
|---|---|---|
| 自宅固定回線 | 固定 IP(allowlist 登録候補) | シナリオ 2 |
| モバイルテザリング | 携帯キャリア IP(allowlist 外) | シナリオ 3 |
| AWS EC2(ap-northeast-1) | EC2 パブリック IP | CIDR /32 登録で動作確認 |
| VPN オン/オフ | 出口 IP が切り替わる | どちらの挙動になるか確認 |
複数拠点から確認することで、CIDR の範囲設定ミス(/24 で意図より広い範囲を許可している等)を早期に発見できます。
- シナリオ 1:
maintenance_mode=false→ 全 IP 本番到達 ✓ - シナリオ 2: メンテ中 + allowlist 内 IP → 本番到達 ✓
- シナリオ 3: メンテ中 + allowlist 外 IP → sorry 表示(URI rewrite)✓
- シナリオ 4:
X-Forwarded-For偽装 →event.viewer.ipで無効化 ✓ - シナリオ 5: KVS 直接変更 → 5 秒以内反映 + drift 確認 ✓
§7: 監査・ログ(CloudFront ログ / CF Functions 実行ログ / WAF 連携)
§6 の動作確認が完了したら、誰がいつ何をしたかを記録・追跡できる監査体制を整えます。CloudFront のアクセスログ、CF Functions の実行ログ、KVS 変更履歴の 3 層で監査要件を満たします。
ログ体系の全体像
| ログ種別 | 収集先 | 遅延 | 主な用途 |
|---|---|---|---|
| CloudFront standard logs | S3 バケット | 約 5 分 | アクセス分析・コスト把握 |
| CloudFront real-time logs | Kinesis Data Streams | ほぼ即時 | インシデント調査・sorry 到達ユーザ特定 |
| CF Functions ログ | CloudWatch Logs | 数秒〜1 分 | 関数実行エラー・console.log デバッグ |
| KVS 変更履歴 | git log + terraform apply ログ | – | 監査証跡・いつ誰がメンテ切替したか |
CloudFront Standard Logs(S3 配信)
Standard Logs は CloudFront ディストリビューションの設定で有効化します。
# infrastructure/cloudfront.tf(既存 distribution に追記)
resource "aws_cloudfront_distribution" "main" {
# ... 既存設定 ...
logging_config {
include_cookies = false
bucket = "${aws_s3_bucket.cf_logs.bucket_regional_domain_name}"
prefix = "cloudfront/"
}
}
resource "aws_s3_bucket" "cf_logs" {
bucket = "${var.project_name}-cf-access-logs-${var.environment}"
}
resource "aws_s3_bucket_lifecycle_configuration" "cf_logs" {
bucket = aws_s3_bucket.cf_logs.id
rule {
id = "expire-old-logs"
status = "Enabled"
filter { prefix = "cloudfront/" }
expiration { days = 90 }
}
}
S3 に届いたログを Athena でクエリする例(sorry ページへのアクセスを抽出):
-- Athena: sorry ページアクセスを時系列で確認
SELECT date, time, "c-ip", "cs-uri-stem", "sc-status"
FROM cloudfront_logs
WHERE "cs-uri-stem" = '/sorry/index.html'
AND date >= date '2024-05-20'
ORDER BY date, time
LIMIT 1000;
監査要件への対応: 「いつ・どの IP が sorry ページを見たか」は standard logs のみで追跡可能です。ただし約 5 分の遅延があるため、インシデント発生中のリアルタイム調査には real-time logs が必要です。
CloudFront Real-Time Logs(Kinesis Data Streams)
Real-Time Logs はほぼ即時(数秒以内)に Kinesis Data Streams にレコードを配信します。インシデント発生中に「今まさに sorry を見ているユーザ数」を確認できます。
# infrastructure/realtime_logs.tf
resource "aws_kinesis_stream" "cf_realtime" {
name = "${var.project_name}-cf-realtime-logs"
shard_count= 1# 通常トラフィックは 1 シャードで十分
retention_period = 24 # 時間
stream_mode_details {
stream_mode = "PROVISIONED"
}
}
resource "aws_cloudfront_realtime_log_config" "maintenance" {
name = "${var.project_name}-maintenance-realtime"
sampling_rate = 100# 100% サンプリング(監査用途のため全件)
fields = [
"timestamp",
"c-ip", # 送信元 IP(event.viewer.ip と同値)
"cs-uri-stem",# リクエスト URI(/ または /sorry/index.html)
"sc-status", # HTTP ステータスコード
"x-edge-result-type",# Hit/Miss/Error
"x-edge-location",# エッジロケーション
]
endpoint {
stream_type = "Kinesis"
kinesis_stream_config {
role_arn= aws_iam_role.cf_realtime_log.arn
stream_arn = aws_kinesis_stream.cf_realtime.arn
}
}
}
# distribution の cache_behavior に realtime_log_config_arn を追加
# default_cache_behavior {
#realtime_log_config_arn = aws_cloudfront_realtime_log_config.maintenance.arn
# }
コスト見積り: Kinesis 1 シャード = $0.015/時間 + $0.014/100万 PUT。月 730 時間稼働で約 $11〜$15/月。Real-Time Logs はオプションのため、監査要件がない場合は Standard Logs のみで十分です。
CF Functions 実行ログ(CloudWatch Logs)
CF Functions では console.log() の出力が CloudWatch Logs に書き込まれます。関数名に対応したロググループ /aws/cloudfront/function/<FUNCTION_NAME> に蓄積されます。
// maintenance.js(§3 コードに console.log を追加したデバッグ版)
async function handler(event) {
const request = event.request;
const clientIp = event.viewer.ip;
let config;
try {
const raw = await kvsHandle.get('config', { format: 'json' });
config = typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (e) {
console.log(JSON.stringify({
event: "kvs_error",
clientIp,
error: e.message
}));
return request;
}
if (config.maintenance_mode && !ipInCidrList(clientIp, config.allowlist || [])) {
console.log(JSON.stringify({
event: "sorry_redirect",
clientIp,
maintenance_mode: config.maintenance_mode
}));
request.uri = '/sorry/index.html';
}
return request;
}
CloudWatch Logs Insights でログを確認するクエリ例:
# sorry リダイレクトを時系列で確認(over last 1 hour)
fields @timestamp, @message
| filter @message like /sorry_redirect/
| parse @message '"clientIp":"*"' as clientIp
| stats count(*) as redirect_count by clientIp
| sort redirect_count desc
| limit 20
注意: 本番コードに console.log を入れると CF Functions の実行時間が増加します。1ms 制限のある CFF ではログは最小限にするか、KVS エラー等の例外時のみ出力するよう設計してください。
test-function API によるローカル単体テスト
本番に deploy する前に CF Functions の動作を API で検証できます。
# テストイベントを JSON ファイルで用意
cat > /tmp/test-event.json << 'EOF'
{
"version": "1.0",
"context": {
"eventType": "viewer-request"
},
"viewer": {
"ip": "198.51.100.50"
},
"request": {
"method": "GET",
"uri": "/",
"headers": {},
"cookies": {},
"querystring": {}
}
}
EOF
# CF Functions のテスト実行
aws cloudfront test-function \
--name "<PROJECT>-maintenance-viewer-request" \
--if-match $(aws cloudfront describe-function \
--name "<PROJECT>-maintenance-viewer-request" \
--query ETag --output text) \
--event-object file:///tmp/test-event.json \
--stage LIVE
# レスポンス例(sorry リダイレクト時)
# {
#"TestResult": {
# "FunctionSummary": { ... },
# "ComputeUtilization": "5",← 1ms 制限に対する使用率(%)
# "FunctionOutput": "{\"request\":{\"uri\":\"/sorry/index.html\", ...}}",
# "FunctionErrorMessage": ""
#}
# }
ComputeUtilization が 80% を超える場合は 1ms 制限に接近しています。allowlist の件数が増えてきたら定期的に確認してください。
KVS 変更履歴(terraform plan/apply ログ・git log)
Terraform + GitHub Actions によるメンテ切替は、git 履歴と GHA ログの 2 層で監査証跡が残ります。
# git log でメンテ切替履歴を確認
git log --oneline --all -- env/prod.tfvars
# 例:
# a1b2c3d maintenance: restore normal mode 2024-05-20 03:45 JST
# d4e5f6a maintenance: enable 2024-05-20 02:00-04:00 JST (yamada-taro)
# 9a8b7c6 init: initial tfvars setup
# 特定コミットの変更内容を確認
git show d4e5f6a -- env/prod.tfvars
# diff: maintenance_mode: false → true, allowlist_ips: [] → ["203.0.113.10/32"]
# GHA apply ログ(GitHub CLI で取得)
gh run list --workflow terraform-apply.yml --limit 10
gh run view <RUN_ID> --log | grep -E "Apply complete|Error"
actor 変数(var.actor)を GHA で ${{ github.actor }} に渡すことで、KVS value 内にも誰が apply したかが記録されます。
# KVS value の updated_by フィールドを確認
aws cloudfront-keyvaluestore get-key \
--kvs-arn "${KVS_ARN}" \
--key config \
--query Value --output text | python3 -m json.tool | grep updated_by
# → "updated_by": "yamada-taro"
WAF 連携の余地(allowlist 50件超過時のフォールバック)
CF Functions の allowlist は KVS value の 1KB 制限により最大約 50 件(IPv4 /32 換算)が実用上限です。件数が増えてきた場合は WAF との連携を検討します。
| 方式 | allowlist 上限 | 変更反映 | コスト概算/月 |
|---|---|---|---|
| CF Functions + KVS(本構成) | ~50 件 | 数秒 | 無料枠内 |
| WAF IPSet | 10,000 件 | 数秒 | $5〜 (WAF WebACL) |
| WAF IPSet + CF Functions(ハイブリッド) | 10,000 件 | 数秒 | $5〜 |
ハイブリッド構成のアイデア: WAF IPSet で allowlist を管理し、CF Functions は maintenance_mode フラグのチェックのみに専念します。WAF Rule で allowlist に含まれる IP のリクエストを ALLOW し、WAF がブロックしたリクエストのみ sorry URI に到達させる構成です。ただし WAF の WebACL + ルール料金が追加されます(CloudFront + WAF は月 $5 程度〜)。
- Standard Logs: S3 配信(5分遅延)→ Athena で sorry アクセスを時系列確認
- Real-Time Logs: Kinesis 配信(即時)→ インシデント中のリアルタイム調査
- CFF ログ: CloudWatch Logs → KVS エラー・sorry リダイレクト記録(
console.log) - KVS 変更履歴: git log + GHA apply ログ → 誰がいつメンテ切替したか
- test-function API: 本番 deploy 前の単体テスト・
ComputeUtilization確認 - WAF 連携: allowlist 50件超過時の発展オプション
§8. コスト・制約・TOC 超過リスク
本章では CloudFront Functions + KVS 構成の月額コスト試算と、実装上の制約を体系的に整理する。さらに TOC(Time of Completion: 実行時間制限)超過時の挙動と、allowlist 50 件上限を超えた場合の対処パターンを解説する。
8-1. コスト試算
第1弾(CloudFront × ALB × S3)に第2弾(CF Functions × KVS)を重ねた場合の月額コストを試算する。
各コンポーネントの料金単価(2024年時点・東京リージョン)
| コンポーネント | 料金 | 無料枠 |
|---|---|---|
| CF Functions | $0.10 / 1M 実行 | 2M 実行/月 |
| KVS reads | $0.10 / 1M reads | 10M reads/月 |
| KVS ストレージ | $0.0009 / GB・月 | — |
| real-time logs(Kinesis 1 shard) | $0.015/h + $0.014/1M PUT | — |
| CloudFront データ転送 | $0.085〜$0.114 / GB | 1TB/月 |
| ALB | $0.008/h + $0.008/LCU | — |
| S3(sorry バケット) | $0.025 / GB | 5GB/月 |
月次コスト試算(1M リクエスト/月の小規模サービス想定)
CF Functions 実行: 1M req → 無料枠内 → $0
KVS reads : 1M reads → 無料枠内 → $0
KVS ストレージ : config key 1件 (< 1KB) → $0
real-time logs : $0.015 × 24h × 30日 ≈ $10.8 + PUT ほぼ $0
CloudFront: 第1弾構成で既に稼働中
ALB : $0.008 × 24h × 30日 ≈ $5.76
S3 sorry pages : < 1MB → 無料枠内
▶ 第1弾 + 第2弾 合計常時稼働月額: 約 $20〜25
▶ 1時間ハンズオン検証のみ: $0.05 未満
補足: real-time logs を有効にしない場合(standard logs のみ)、Kinesis コストが不要となり月額 $10 前後に抑えられる。real-time logs は §7 で解説した運用監視に有用だが、コスト予算に応じて省略可能。
8-2. CF Functions 制約一覧
CloudFront Functions は Edge での超低レイテンシ実行を実現する一方、意図的に厳しい制約が課されている。
| 制約項目 | 値 | 補足 |
|---|---|---|
| 実行時間(CPU) | 1ms | viewer-request / viewer-response のみ |
| コードサイズ | 10KB(圧縮後) | — |
| メモリ | 2MB | ヒープ + スタック合計 |
| ネットワーク | 禁止 | 外部 API 呼び出し不可 |
| ステート | Stateless | KVS 読み取りは可・書き込み不可 |
| イベントタイプ | viewer-request / viewer-response のみ | origin-request/response は Lambda@Edge |
| KVS 関連付け | 1 function あたり 1 KVS | AWS 公式制約 |
1ms 制約について
本ハンズオンの CIDR 判定ロジック(§3 完成版コード)は、allowlist 50 件程度であれば 0.1〜0.3ms 程度で完了する。CIDR 件数が増加するにつれて実行時間は線形増加するため、100 件を超え始めると 1ms に接近するケースがある。
8-3. KVS 制約と allowlist 上限
KVS 自体にも以下の制約がある。
| 制約項目 | 値 |
|---|---|
| ストア全体 | 最大 5MB |
| key | 最大 512B |
| value | 最大 1KB |
| KVS 数 | アカウントあたり上限あり(AWS コンソール確認) |
allowlist CIDR 50 件上限の根拠
value サイズ上限 1KB から逆算する。
JSON フォーマット例:
{"maintenance_mode":true,"allowlist":["203.0.113.0/24","198.51.100.0/24",...]}
CIDR 1件あたりの文字数: "xxx.xxx.xxx.xxx/xx", → 最大 20 文字
JSON オーバーヘッド(キー名・括弧等): 約 60 文字
50 件 × 20 文字 + 60 文字 = 1060 文字 ≈ 1KB(上限到達)
結論として、allowlist の実運用上限は約 50 件。この範囲内であれば 1ms 制約・value サイズ制約ともに余裕を持って満たせる。
8-4. TOC 超過時の挙動
1ms の CPU 時間を超過した場合、CF Functions はエラーとなる。この挙動は AWS 公式仕様として以下のように定められている。
Function execution error(TOC 超過)→ request は origin に fallback
— AWS CloudFront Developer Guide: Troubleshooting CloudFront Functions
つまり、CF Functions が異常終了した場合でもリクエストはオリジン(ALB)に素通しされる。これは fail-open 動作であり、メンテ期間中に TOC 超過が発生すると IP 制限が機能しなくなる点に注意が必要だ。
⚠️ TOC 超過は「メンテ抜け」を引き起こす
TOC 超過時は allowlist 判定がスキップされ、メンテ外ユーザも本番環境に到達する。allowlist が肥大化した状態でのメンテ切替は、CFF ログ(§7)で実行時間を事前確認してから行うこと。
8-5. allowlist 50 件超過時の対処パターン
運用上 allowlist が 50 件を超えそうな場合、以下 2 つのパターンで対処できる。
パターン A — WAF IPSet 併用
CF Functions は allowlist の代わりに WAF IPSet で IP 判定を行い、Functions 自身はメンテモード判定とルーティングのみを担当する構成。
[Viewer] → [CF Functions: maintenance_mode 判定のみ]
↓ maintenance_mode=true
[WAF: IPSet でホワイトリスト判定]
↓ IP 一致 → 通過 / 不一致 → BLOCK
[Origin: ALB]
- allowlist 件数上限: WAF IPSet は 10,000 CIDR まで対応
- KVS value サイズ制約から完全に解放される
- 追加コスト: WAF WebACL $5/月 + IPSet 判定 $1/1M リクエスト
パターン B — CIDR supernet 集約
複数の隣接 CIDR を上位のスーパーネットに集約し、件数を削減する。
# 例: 4件の /24 を 1件の /22 にまとめる
203.0.113.0/24
203.0.113.4/24 → 203.0.112.0/22 (1件)
203.0.113.8/24
203.0.113.12/24
- 追加コストなし・構成変更も不要
- 集約により「許可したくない IP 帯域」まで含む可能性があるため、慎重なレビューが必要
選択基準
| 状況 | 推奨パターン |
|---|---|
| 50〜200 件程度・社内 VPN / 拠点 IP | パターン B(supernet 集約) |
| 200 件超・複数組織・細かい制御が必要 | パターン A(WAF IPSet) |
| コスト優先・小規模チーム | パターン B |
| セキュリティ要件が厳しい | パターン A |
§9. まとめ — CloudFront Functions × KVS × Terraform で作るメンテ切替基盤
第1弾で構築した CloudFront × ALB × S3 デュアルオリジン基盤に、CloudFront Functions と KeyValueStore を組み合わせることで、IP allowlist ベースのメンテナンスモード切替を Terraform tfvars 1 行で安全に運用できる体制を実現した。計画メンテ中も運用者だけが本番コンテンツを確認でき、一般ユーザーには sorry page を返すという現場ニーズを、コードレス・低コスト・高速反映の 3 点でクリアしている。
本記事でたどった実装ステップを振り返ると次の通りだ。
- §1: メンテ切替の現場ニーズ 3 パターン(計画メンテ・障害時切替・検証期間制限)を整理し、本記事の対象スコープを計画メンテに絞った。
- §2: CloudFront Functions / Lambda@Edge / WAF / Route53 の 4 方式を 5 軸評価し、CFF + KVS が最適解であることを確認した。
- §3: viewer-request フックで KVS から config を取得し、IP CIDR 判定を行う本番投入可能な JavaScript 関数を実装した。
- §4:
maintenance_modeとallowlist_ipsを tfvars 変数に落とし込み、aws_cloudfront_key_value_storeで KVS を Terraform 管理下に置く設計を確立した。 - §5: PR → plan → approve → apply の安全なフローと
git revertロールバック手順、CODEOWNERS・branch 保護によるガードレールを整備した。 - §6: 5 シナリオの動作確認(通常 / allowlist 内 / allowlist 外 / XFF 偽装 / Console 直接変更)で実挙動を検証した。
- §7: CloudFront real-time logs と KVS 単体テスト API を活用した監査・デバッグ手法を整理した。
- §8: ハンズオン 1 時間で $0.05 未満、常時稼働で月 $20-25 のコスト構造と CF Functions の制約体系を確認した。
第1弾 + 第2弾で身につくスキル棚卸(14 要素)
本シリーズ 2 記事を通じて、以下のスキルセットが実装レベルで習得できる。
| # | スキル | 習得記事 |
|---|---|---|
| 1 | CloudFront Distribution 設計(デュアルオリジン・cache behavior 分離) | 第1弾 |
| 2 | ALB との統合(forward rules / target groups / 5xx fallback) | 第1弾 |
| 3 | S3 OAC(Origin Access Control)設定と sorry page 配信 | 第1弾 |
| 4 | CloudFront cache behaviors パスパターン設計(/sorry/* 分離) | 第1弾 |
| 5 | Terraform 1.9 で one-shot apply する CDN 構成管理 | 第1弾 |
| 6 | GHA OIDC による AWS ロール Assume(キーレス認証) | 第1弾 |
| 7 | origin group による障害時自動 failover 設計 | 第1弾 |
| 8 | CloudFront Functions viewer-request コード実装(IP CIDR 判定) | 第2弾 |
| 9 | KeyValueStore 設計・運用(key/value 構造・1KB 制約・反映タイミング) | 第2弾 |
| 10 | Terraform での KVS 管理(aws_cloudfront_key_value_store + etag) | 第2弾 |
| 11 | tfvars 1 行切替フロー(maintenance_mode = true/false) | 第2弾 |
| 12 | PR → plan → approve → apply の安全な切替運用 | 第2弾 |
| 13 | branch 保護・CODEOWNERS によるレビュープロセス設計 | 第2弾 |
| 14 | git revert <merge-commit> → apply によるロールバック手順 | 第2弾 |
運用上の注意点まとめ
本構成を本番運用する上で押さえておくべきポイントを整理する。
CIDR 50 件上限
: CF Functions は CPU 1ms・コードサイズ 10KB の制約を持つ。IPv4 CIDR ループ判定は Pure JS で 50 件程度まで余裕を持って処理できるが、それ以上は事前に supernet 集約するか、WAF IPSet との併用を検討すること(§7 参照)。CIDR 数が増えるにつれてコードサイズも増大するため、allowlist_ips を tfvars で管理する場合は定期的に wc -c maintenance.js でサイズ確認を行う習慣を持つこと。
KVS 反映タイミング
: aws_cloudfrontkeyvaluestore_key の value 更新は 関数コードの deploy なしに即時反映される。これは KVS の最大の強みだが、Console から直接変更した場合は Terraform state とのドリフトが発生する。§5 Q4 の緊急手順後は必ず terraform apply で drift を解消すること。
コールドスタートなし
: CF Functions は Lambda@Edge と異なり コールドスタートが存在しない。PoP(エッジロケーション)でリクエストごとに同一レイテンシで実行されるため、メンテ切替直後の最初のリクエストから安定した挙動が期待できる。
allowlist 漏れ事故の復旧
: allowlist 記述ミスで運用者自身が sorry に誘導されるケースは頻発する。発生時は Console から KVS の config key を直接編集(maintenance_mode: false に戻す)→ 数秒で復旧→ 事後 terraform apply で state 整合を取る手順が最速。
学習ロードマップ
本シリーズを踏まえた次のステップと前提知識を以下に整理する。
【前提知識】
├─ AWS CloudFront・ALB・S3 の基礎
├─ Terraform 1.9.x の基本操作
└─ GitHub Actions の YAML 記述
↓
【本シリーズ(第1弾 + 第2弾)】
├─ 第1弾: CDN 前段配信 3 層構成 + sorry page + origin group
└─ 第2弾: CF Functions × KVS × PR 駆動 apply によるメンテ切替運用
↓
【発展トピック】
├─ Lambda@Edge: 複雑なヘッダ操作・地域別コンテンツ出し分け・認証処理
├─ AWS WAF + IPSet: ブロック用途・大規模 allowlist(5000 件超)・レートリミット
├─ CloudFront Continuous Deployment: トラフィック重み付けによる canary リリース
└─ Amazon CloudFront KeyValueStore 拡張: A/B テスト設定・機能フラグ管理
Lambda@Edge は CF Functions の上位互換ではなく別用途であることに注意。レイテンシ・コスト・コールドスタートのトレードオフを理解した上で使い分けることが運用品質向上につながる。
本シリーズで取り上げた CF Functions + KVS の組み合わせは、メンテ切替以外にも A/B テスト・機能フラグ管理・地域別コンテンツ出し分けへ応用できる。KVS の value を JSON で管理するパターンを理解しておけば、ロジック変更なしに設定だけで挙動を制御できる範囲が大幅に広がる。
関連シリーズ
本シリーズと組み合わせることで AWS 運用基盤をさらに強化できるシリーズを紹介する。
[cmd_040] GitHub Actions × AWS OIDC 認証 — キーレス CI/CD 設計
: 本シリーズ第2弾の PR → apply フローで前提としている GHA OIDC 認証の詳細実装を解説。IAM ロール設計・trust policy・permissions boundary まで網羅。AWS アクセスキーを一切発行しない安全な CI/CD パイプライン構築を目指すならまずこちらを読んでほしい。
[cmd_052] ECS Blue/Green デプロイ — CodeDeploy × ALB × Terraform
: 本シリーズで構築した ALB を応用し、ECS タスクの Blue/Green 切替を CodeDeploy で管理する構成。計画デプロイと本シリーズのメンテ切替を組み合わせることで、ダウンタイムゼロのリリースフローが完成する。ALB ターゲットグループの重み付けルーティングから始まり、CodeDeploy フック・ロールバック自動化まで実装レベルで解説している。
本シリーズ(cmd_056)で習得した PR 駆動 apply・branch 保護・CODEOWNERS のパターンは、ECS・RDS・API Gateway など他の AWS リソース管理にもそのまま転用できる。まず CloudFront という比較的変更リスクが低い CDN 設定で運用フローを確立し、その後本番データを扱う ECS / RDS へと適用範囲を広げていくアプローチを推奨する。
まとめに代えて
第1弾の「まず動かす」から第2弾の「安全に運用する」へ。この 2 ステップが CloudFront を中心とした AWS CDN 基盤構築の核心だ。
インフラ変更を Pull Request で管理し、計画されたプロセスを経て本番反映する文化は、チームの規模や経験を問わず導入すべき習慣である。本シリーズが、その第一歩を踏み出す助けになれば幸いだ。