CloudFrontのキャッシュ設定を全放置してたらLCPが4.5秒になった話
CDNの設定ってデフォルトのままになってませんか?ECサイトのCore Web Vitalsが突然悪化して気づいた失敗談と、3ヶ月でLCP1.2秒まで改善したCloudFront実践知見をまとめました。
CloudFront CDN最適化2026|キャッシュ戦略・オリジンシールド・エッジコンピューティング実践ガイド
先日、担当していたECサイトのCore Web Vitalsが突然悪化して、原因を追いかけたらCDN設定がほぼデフォルトのままだったことが発覚した。LCP(Largest Contentful Paint)が4.5秒を超えていて、Googleのインデックスにも影響が出始めていたという、地味に最悪な状況だった。
そこから3ヶ月かけてCloudFrontのキャッシュ戦略を全面的に見直して、LCPを1.2秒まで改善できた。その過程で学んだことを今回まとめておきたい。2026年現在、CloudFrontはかなり機能が充実してきていて、正直3年前の知識のままだと設定ミスを量産できる水準になってる。同じ轍を踏んでほしくないので、実務で効いた知見を共有する。
CloudFront 2026年の全体像を把握する
まず今の構成をどう考えるかだけど、2026年時点でのCloudFrontはざっくりこういう構成で動いてる。
flowchart TB
subgraph Client["クライアント"]
U[ユーザーブラウザ]
end
subgraph Edge["CloudFront エッジ(200+PoP)"]
EL[エッジロケーション]
CF_FUNC[CloudFront Functions]
L_EDGE[Lambda@Edge]
end
subgraph Shield["オリジンシールド"]
OS[オリジンシールドノード]
end
subgraph Origin["オリジン"]
subgraph VPC_AZ["VPC / AZ"]
ALB[Application Load Balancer]
EC2[EC2 / ECS / EKS]
end
S3[S3バケット]
APIGW[API Gateway]
end
U -->|HTTPS| EL
EL --> CF_FUNC
EL --> L_EDGE
EL -->|キャッシュHIT| EL
EL -->|キャッシュMISS| OS
OS -->|キャッシュMISS| ALB
OS -->|キャッシュMISS| S3
OS -->|キャッシュMISS| APIGW
ALB --> EC2
この構成で特に重要なのが、エッジロケーションとオリジンシールドの間にキャッシュの二層構造があること。昔はエッジが直接オリジンを叩いていたけど、オリジンシールドを挟むことでオリジンへのリクエスト数を大幅に削減できる。
2026年に入ってからCloudFrontのContinuous Deploymentが正式GAになって、キャッシュポリシーのA/Bテストがかなりやりやすくなった。以前は「設定変えたら全ユーザーに影響が出るので怖い」という状況だったけど、今はステージングトラフィックで先に検証できる。これが地味に助かってる。
キャッシュポリシー設計で失敗しがちなパターン
正直、ここが一番ハマった部分。デフォルトのキャッシュポリシーをそのまま使い続けて、「なんかキャッシュ効いてないな」という状態が半年以上続いていた。
問題の根本はVaryヘッダーの扱いだった。うちの構成ではAPIサーバーがVary: Accept-Encoding, Accept-Languageを返していて、これがCloudFrontのキャッシュキーに含まれてキャッシュヒット率が著しく低下していた。
# キャッシュヒット率の確認(CloudWatch Logs Insightsで実行)
fields @timestamp, @message
| filter @message like /x-cache/
| stats count(*) as total,
count_if(@message like /Hit/) as hit,
count_if(@message like /Miss/) as miss
by bin(1h)
| sort @timestamp desc
実際に診断したときのキャッシュヒット率の遷移がこれ:
xychart-beta
title "CloudFrontキャッシュヒット率の改善(月次)"
x-axis ["2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07"]
y-axis "キャッシュヒット率(%)" 0 --> 100
bar [23, 25, 61, 74, 82, 89]
line [23, 25, 61, 74, 82, 89]
4月から5月にかけて急改善しているのは、キャッシュポリシーを見直してVaryヘッダー問題を解消した時期。体感でも「あ、サイトが速くなった」と思えるくらいの変化があった。
実際に適用したキャッシュポリシーのTerraformコードはこう:
resource "aws_cloudfront_cache_policy" "optimized_v2" {
name = "OptimizedCachePolicy-2026"
comment = "Vary/Cookie除外・Accept-Encodingのみキャッシュキーに含める"
default_ttl = 86400
max_ttl = 31536000
min_ttl = 0
parameters_in_cache_key_and_forwarded_to_origin {
# Cookieはキャッシュキーから除外(認証はオリジンリクエストポリシーで制御)
cookies_config {
cookie_behavior = "none"
}
# ヘッダーはAccept-Encodingのみ
headers_config {
header_behavior = "whitelist"
headers {
items = ["Accept-Encoding"]
}
}
# クエリ文字列は正規化してキャッシュキーを統一
query_strings_config {
query_string_behavior = "whitelist"
query_strings {
items = ["v", "lang"]
}
}
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
}
}
ここで重要なのがenable_accept_encoding_brotli = trueの設定。2026年現在、ブラウザのBrotli対応率は97%を超えているので、GZIPだけでなくBrotliも有効にしておくべきだ。実測で転送サイズが追加で12〜18%削減できた。
ただし正直、Cookieをキャッシュキーから完全に除外するのは全員に合う設定ではない。認証状態によってコンテンツが変わるページはOriginリクエストポリシーで制御するか、Behaviorを分けて個別対応が必要になる。「とりあえずCookieなしで全部キャッシュ」は罠なので気をつけてほしい。
オリジンシールド導入で劇的に変わったオリジン負荷
オリジンシールドを導入する前、夕方のトラフィックピーク時にオリジン(ALB + ECS)のCPUが80%を超えてAuto Scalingが何度も発動していた。CloudFrontのエッジから来るリクエストが分散しすぎて、各エッジロケーションが独立してオリジンをキャッシュMISS叩く構造になっていたのが原因だった。
オリジンシールドの選択は地理的に重要で、うちの場合ユーザーが日本中心だったのでap-northeast-1(東京)をオリジンシールドに指定した。
resource "aws_cloudfront_distribution" "main" {
# ...
origin {
domain_name = aws_lb.main.dns_name
origin_id = "ALB-main"
# オリジンシールド設定(2026年推奨設定)
origin_shield {
enabled = true
origin_shield_region = "ap-northeast-1"
}
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
# オリジンシールドがある場合、接続タイムアウトを少し長めに
origin_read_timeout = 60
origin_keepalive_timeout = 60
}
}
}
導入後のオリジンリクエスト数の変化:
xychart-beta
title "オリジンシールド導入前後のオリジンリクエスト数(万req/h)"
x-axis ["月", "火", "水", "木(導入)", "金", "土", "日"]
y-axis "オリジンリクエスト数(万req/h)" 0 --> 200
bar [180, 175, 183, 91, 88, 72, 68]
line [180, 175, 183, 91, 88, 72, 68]
木曜日に導入して、その日からオリジンへのリクエストが約50%削減された。正直、「ほんとにこんな簡単に減るの?」と疑いたくなるレベルだったけど数値は嘘をつかなかった。ECSのDesired Countを下げてコスト削減にも繋がった。
Lambda@Edge vs CloudFront Functions、2026年時点での使い分け
これはLambda@EdgeとCloudFront Functions、本番で両方使い倒して見えた本当の使い分け2026でも詳しく書いたけど、自分なりの整理をここでも残しておく。
flowchart LR
subgraph 判断基準
A{処理内容は?}
A -->|URLリライト・リダイレクト・ヘッダー操作| B[CloudFront Functions]
A -->|外部API呼び出し・DB参照・認証検証| C[Lambda@Edge]
A -->|高度な画像変換・複雑なビジネスロジック| D[オリジンで処理]
end
subgraph 実行場所
B --> E["200+ エッジ(全PoP)"]
C --> F["4リージョンのみ"]
D --> G["オリジンサーバー"]
end
実際の性能比較がこちら。コスト差が6倍あるのに気づかず「とりあえずLambda@Edge」でいくと地味に痛い出費になるので注意:
| 項目 | CloudFront Functions | Lambda@Edge |
|---|---|---|
| 実行場所 | 全エッジPoP(200+) | 4リージョンのみ |
| 最大実行時間 | 2ms | 30秒(オリジン応答) |
| メモリ | 2MB | 最大10GB |
| コールドスタート | なし(V8エンジン常駐) | あり(数十ms〜) |
| コスト(100万回) | $0.10 | $0.60〜 |
| Node.jsバージョン | ES2022相当 | 22.x(2026年対応) |
| 外部API呼び出し | 不可 | 可能 |
2026年になってLambda@EdgeがNode.js 22.xに対応したのは地味に嬉しいアップデートだった。以前はランタイムが古くてモジュールの互換性で苦労していたので。
うちのチームではこういうルールで使い分けてる:
CloudFront Functionsで実装している処理
// viewer-request イベント:URLの正規化とリダイレクト
function handler(event) {
const request = event.request;
const uri = request.uri;
// トレイリングスラッシュの正規化
if (uri !== '/' && uri.endsWith('/')) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: uri.slice(0, -1) },
'cache-control': { value: 'max-age=3600' }
}
};
}
// SPAのフォールバック(拡張子なしパスをindex.htmlへ)
if (!uri.includes('.')) {
request.uri = '/index.html';
}
return request;
}
Lambda@Edgeで実装している処理(JWTの検証)
// Node.js 22.x対応
import { createVerifier } from 'fast-jwt';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
let cachedPublicKey = null;
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// 認証不要パスのスキップ
const publicPaths = ['/health', '/api/public'];
if (publicPaths.some(p => request.uri.startsWith(p))) {
return request;
}
const authHeader = headers.authorization?.[0]?.value;
if (!authHeader?.startsWith('Bearer ')) {
return {
status: '401',
statusDescription: 'Unauthorized',
body: JSON.stringify({ error: 'Missing token' })
};
}
try {
// Secrets Managerから公開鍵をキャッシュ(Lambdaインスタンス単位)
if (!cachedPublicKey) {
const client = new SecretsManagerClient({ region: 'ap-northeast-1' });
const response = await client.send(
new GetSecretValueCommand({ SecretId: 'jwt-public-key' })
);
cachedPublicKey = response.SecretString;
}
const verify = createVerifier({ key: cachedPublicKey, algorithms: ['RS256'] });
const payload = await verify(authHeader.slice(7));
// 検証済みユーザー情報をオリジンに転送
request.headers['x-user-id'] = [{ value: payload.sub }];
request.headers['x-user-role'] = [{ value: payload.role }];
return request;
} catch (err) {
return {
status: '403',
statusDescription: 'Forbidden',
body: JSON.stringify({ error: 'Invalid token' })
};
}
};
このLambda@EdgeのJWT検証、実はまだ検証中の部分もある。コールドスタートがゼロではないので、特に深夜の低トラフィック時間帯にどの程度レイテンシへ影響するかもう少し測定を続けたいところだ。
実際の構成図とコスト削減効果
うちのチームで最終的に落ち着いた本番構成:
graph TB
subgraph Internet["インターネット"]
User["ユーザー"]
Bot["クローラー・Bot"]
end
subgraph CloudFront["CloudFront Distribution"]
CF_WAF["WAF v2\nマネージドルール"]
CF_DIST["CloudFront Distribution"]
subgraph Behaviors["Cache Behaviors"]
B1["/api/* → No Cache\nOriginRequestPolicy付き"]
B2["/static/* → 1年Cache\nバージョニング必須"]
B3["/* → 24h Cache\nSPA用フォールバック"]
end
CF_FUNC["CloudFront Functions\nURLリライト・リダイレクト"]
L_EDGE["Lambda@Edge\nJWT検証"]
end
subgraph Shield["Origin Shield (ap-northeast-1)"]
OS_NODE["オリジンシールドノード"]
end
subgraph AWS_Region["ap-northeast-1"]
subgraph VPC["VPC"]
subgraph AZ1["AZ-a"]
ALB["ALB"]
ECS1["ECS Fargate"]
end
subgraph AZ2["AZ-c"]
ECS2["ECS Fargate"]
end
end
subgraph Storage["ストレージ"]
S3_STATIC["S3\n静的アセット"]
S3_ORIGIN["S3\nOAC経由"]
end
subgraph Cache["キャッシュ"]
ELASTICACHE["ElastiCache\nセッション管理"]
end
end
User --> CF_WAF
Bot --> CF_WAF
CF_WAF --> CF_DIST
CF_DIST --> CF_FUNC
CF_DIST --> L_EDGE
CF_DIST --> B1
CF_DIST --> B2
CF_DIST --> B3
B1 --> OS_NODE
B2 --> S3_ORIGIN
B3 --> OS_NODE
OS_NODE --> ALB
ALB --> ECS1
ALB --> ECS2
ECS1 --> ELASTICACHE
ECS2 --> ELASTICACHE
S3_ORIGIN --> S3_STATIC
この構成で3ヶ月後のコスト変化がこちら。個人的にはオリジンサーバーの削減幅が一番意外だった:
| 項目 | 最適化前(月) | 最適化後(月) | 削減率 |
|---|---|---|---|
| CloudFrontデータ転送 | ¥180,000 | ¥95,000 | 47% |
| オリジンサーバー(ECS) | ¥320,000 | ¥195,000 | 39% |
| Lambda@Edge実行費 | ¥45,000 | ¥28,000 | 38% |
| WAF処理費 | ¥22,000 | ¥18,000 | 18% |
| 合計 | ¥567,000 | ¥336,000 | 41% |
オリジンサーバーのコスト削減が意外と大きいのは、キャッシュヒット率が上がってECSのAuto Scalingが発動する頻度が激減したから。「CDN最適化はCDN費用が減るもの」と思いがちだけど、実はオリジン側のコスト削減効果の方が大きくなるケースがある。
Continuous Deploymentでキャッシュポリシーを安全に検証する
2026年に入ってから本格的に使い始めたのがCloudFrontのContinuous Deployment機能。これは本番と同じDistributionを使いながら、一部のトラフィックだけをステージング設定に流せる機能で、キャッシュポリシーの変更を実際のトラフィックで検証できる。
# Staging Distributionの作成
aws cloudfront create-continuous-deployment-policy \
--continuous-deployment-policy-config '{
"StagingDistributionDnsNames": {
"Quantity": 1,
"Items": ["d111111abcdef8.cloudfront.net"]
},
"Enabled": true,
"TrafficConfig": {
"Type": "SingleWeight",
"SingleWeightConfig": {
"Weight": 0.05
}
}
}'
# 5%のトラフィックをステージングに流してキャッシュヒット率を比較
# CloudWatch Metrics で x-cache: Hit from cloudfront の割合を確認
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=STAGING_DIST_ID \
--statistics Average \
--start-time 2026-07-25T00:00:00Z \
--end-time 2026-07-26T00:00:00Z \
--period 3600
以前はキャッシュポリシー変更のたびに「本番に出してみないとわからない」という状況で、チームの心理的安全性が低かった。Continuous Deploymentで5〜10%のトラフィックで先に検証できるようになってから、変更のリードタイムが体感で半分くらいになった。「あの設定、本番で試してみていい?」という会話の心理的ハードルが全然違う。
AWSのインシデント対応についてはインシデント対応の最新ベストプラクティス2026にも書いたけど、CDN設定変更のロールバック手順もちゃんと整備しておくと安心だ。またAWSのマルチリージョン戦略についてはマルチクラウド戦略2026の観点からもCloudFrontのオリジンフェイルオーバー設定を組み合わせると可用性が上がる。
まとめ
CDN最適化は「設定したら終わり」ではなく、継続的に改善していくものだと痛感した3ヶ月だった。やってみて思うのは、デフォルト設定の罪深さで、「動いてるからいいか」が一番コストと性能の両方を食いつぶす。
要点まとめ:
-
キャッシュヒット率の計測から始める — デフォルト設定のままだとVaryヘッダーでキャッシュキーが肥大化してヒット率が20〜30%台になりがち。まずCloudWatch Logs Insightsで現状把握を
-
オリジンシールドは日本ユーザー向けなら
ap-northeast-1一択 — 導入するだけでオリジンリクエストが40〜50%削減できる。コストとオリジン負荷の両方に効く -
CloudFront Functions vs Lambda@Edge は実行時間と外部依存で判断 — URLリライト・ヘッダー操作はFunctions、JWT検証や外部API呼び出しはEdge。コスト差は6倍あるので安易にEdgeを使わない
-
キャッシュポリシーの変更はContinuous Deploymentで5%検証してから — 2026年時点でGAになっているので積極活用を。本番全体に出す前に定量的な根拠が取れる
-
Brotli対応を有効化する —
enable_accept_encoding_brotli = trueを設定するだけで転送サイズが追加12〜18%削減できる。書くのに5秒、効果は永続的
次のアクション:
- CloudWatch Logs Insightsで今のキャッシュヒット率を確認する(Hitが50%未満なら要改善)
- Terraform/CDKでキャッシュポリシーをコード化してバージョン管理する
- オリジンシールドを有効化してオリジンのメトリクス変化を7日間観察する
皆さんのCloudFront設定、デフォルトのままになってるところありませんか?意外とそこが一番の改善余地だったりするので、一度設定を見直してみてほしい。