Lambda@EdgeとCloudFront Functions、本番で両方使い倒して見えた本当の使い分け2026
「軽い処理はCF Functions、重い処理はLambda@Edge」でいけると思ってたのに、最初の2ヶ月で設計を2回やり直した話。本番運用で踏んだ判断ポイントを正直に書きます。
最初から正直に言う:「どっちを使えばいい?」への答えは状況依存だった
去年の夏ごろ、うちのチームが大規模なECサイトのCDN層をリアーキテクチャするプロジェクトに入った。その際、Lambda@EdgeとCloudFront Functionsを「ちゃんと比較して使い分けよう」という方針になって、結果的に両方を本番で同時運用する構成になった。
当初は「軽い処理はCloudFront Functions、重い処理はLambda@Edge」というシンプルな切り分けで行けると思ってたんですよね。実際にやってみたら、その線引きが想像よりずっと繊細で、最初の2ヶ月は設計を2回やり直した。
この記事はその経験から、2026年時点での実務的な選定基準を整理したもの。教科書的なドキュメントは公式にいくらでもあるので、「実際に本番で踏んだ判断ポイント」を軸に書く。
2026年時点のCloudFront FunctionsとLambda@Edgeのスペック差
まずは基本スペックを整理しておく。2026年時点でのアップデートを含めたテーブルがこちら。
| 項目 | CloudFront Functions | Lambda@Edge |
|---|---|---|
| 実行場所 | 250+のエッジロケーション | リージョンエッジ(約13拠点) |
| 最大実行時間 | 1ms(CPU時間) | 30秒(Viewer側)/ 30秒(Origin側) |
| メモリ | 2MB | 128MB〜10GB |
| パッケージサイズ | 10KB(圧縮後) | 50MB(圧縮後) |
| ランタイム | JavaScript (ES5.5+) | Node.js / Python / Ruby / Go等 |
| ネットワークアクセス | 不可 | 可(外部APIコール等) |
| AWS SDKアクセス | 不可 | 可 |
| 実行フェーズ | Viewer Request / Response | Viewer Request / Response / Origin Request / Response |
| 料金(per 1M invocations) | $0.10 | $0.60(Viewer側)/ $3.00(Origin側) |
| 料金(コンピュート) | $0.0000001/ms | $0.00005001/GB-s |
| コールドスタート | なし(JITコンパイル済み) | あり(ただしSnapStartで改善) |
コールドスタートについては、Lambda@Edgeは通常のLambdaよりも頻繁に初期化が走るのが悩みどころだった。Lambda SnapStartの本番導入で冷却時間を60%削減した記事でも触れてるけど、Lambda@EdgeはSnapStartが使えないので、2026年現在もここだけは不利なまま。
実際に測定したレイテンシ追加のデータがこちら。
xychart-beta
title "エッジ関数のレイテンシオーバーヘッド実測値(p50/p99)"
x-axis ["CF Functions p50", "CF Functions p99", "L@E Viewer p50", "L@E Viewer p99", "L@E Origin p50", "L@E Origin p99"]
y-axis "追加レイテンシ (ms)" 0 --> 80
bar [0.3, 1.2, 3.5, 18.0, 8.0, 45.0]
この数字、実際に計測するまで甘く見てた。CloudFront Functionsのp99が1.2msに収まってるのは本当に優秀で、ユーザー体感では差が出ないレベル。一方Lambda@EdgeのViewer側p99が18msというのは、薄いリクエスト処理だとちょっと気になる数値だった。Origin側p99の45msはさすがに「あ、これは設計で吸収できないと痛いな」と感じた。
実際に構築したアーキテクチャ
うちのプロジェクトで最終的に落ち着いた構成がこれ。CloudFront Functionsで「高頻度・軽量・低レイテンシ必須」な処理を受け持ち、Lambda@Edgeで「ロジックが必要・外部参照あり・Origin側での処理」を担当する役割分担にした。
graph TB
subgraph Internet["Internet"]
User["👤 End User"]
end
subgraph CloudFront["CloudFront Distribution"]
direction TB
subgraph ViewerRequest["Viewer Request"]
CFF_VReq["CloudFront Functions\n・URLリライト\n・A/Bテスト振り分け\n・Basic認証チェック"]
end
subgraph ViewerResponse["Viewer Response"]
CFF_VRes["CloudFront Functions\n・セキュリティヘッダー付与\n・キャッシュヘッダー調整"]
end
subgraph OriginRequest["Origin Request"]
LAE_OReq["Lambda@Edge\n・認証トークン検証\n・DynamoDB参照\n・Origin選択ロジック"]
end
subgraph OriginResponse["Origin Response"]
LAE_ORes["Lambda@Edge\n・SSR結果キャッシュ制御\n・エラーハンドリング"]
end
end
subgraph Origins["Origins"]
subgraph VPC_Tokyo["VPC ap-northeast-1"]
ALB1["ALB\n(Primary)"]
ECS1["ECS Fargate\n(App Server)"]
end
subgraph VPC_Osaka["VPC ap-northeast-3"]
ALB2["ALB\n(Failover)"]
ECS2["ECS Fargate\n(App Server)"]
end
S3["S3\n(Static Assets)"]
end
subgraph DataStore["Data Stores"]
DDB["DynamoDB Global Tables\n(Session / Config)"]
SSM["Parameter Store\n(Feature Flags)"]
end
User --> CloudFront
CFF_VReq --> LAE_OReq
LAE_OReq --> ALB1
LAE_OReq --> ALB2
LAE_OReq --> S3
LAE_OReq --> DDB
LAE_OReq --> SSM
ALB1 --> ECS1
ALB2 --> ECS2
LAE_ORes --> CFF_VRes
この構成にたどり着くまでに「Lambda@EdgeでURLリライトも全部やろう」「いや全部CloudFront Functionsで行こう」という迷走があった。どちらかに統一しようとすると必ずどこかで詰まるんですよね。両方使うことへの抵抗感はあったけど、「混在させること自体は悪くない」と割り切ってから設計が安定した。
実際のコード比較:同じ処理をそれぞれで書いてみた
CloudFront FunctionsでのURLリライト
実際に本番で動いてるURLリライト処理がこれ。SPAの履歴APIルーティングと、古いURL体系からの301リダイレクトを組み合わせてる。
// CloudFront Functions (Viewer Request)
// ランタイム: JavaScript ES5.5+
function handler(event) {
var request = event.request;
var uri = request.uri;
// レガシーURLのリダイレクト(旧 /product/ → /items/)
var legacyPattern = /^\/product\/([^\/]+)\/?(.*)?$/;
var match = uri.match(legacyPattern);
if (match) {
var newUri = '/items/' + match[1] + (match[2] ? '/' + match[2] : '');
return {
statusCode: 301,
headers: {
location: { value: newUri },
'cache-control': { value: 'max-age=31536000' }
}
};
}
// SPA用:拡張子なしのパスはindex.htmlに
if (!uri.includes('.') && !uri.startsWith('/api/')) {
request.uri = '/index.html';
}
return request;
}
ここで重要なのは外部ネットワークアクセスができないこと。DynamoDBやParameter Storeを参照したいと思ったら即アウト。最初「Feature Flagの参照をここでやれないか」と試みたけど諦めた。Feature Flag管理についてはFeature Flag導入で本番バグ対応が1時間から5分に短縮した話も参考になると思う。
Lambda@EdgeでのOrigin選択ロジック
DynamoDBのグローバルテーブルを参照してユーザーのセッションに基づきOriginを動的に選択する処理。これがCloudFront Functionsでは絶対に書けない処理の典型例で、「こういうのがあるからLambda@Edgeをゼロにはできないんだよな」とチーム内でも話してた。
// Lambda@Edge (Origin Request)
// Node.js 20.x
const { DynamoDBClient, GetItemCommand } = require('@aws-sdk/client-dynamodb');
// グローバルテーブルなのでus-east-1のエンドポイントを使う
// ※Lambda@Edgeはus-east-1にデプロイ必須
const ddbClient = new DynamoDBClient({
region: 'us-east-1',
// 接続数を制限:Lambda@Edgeは並列数が多い
maxAttempts: 2,
});
const ORIGINS = {
primary: 'primary-alb.ap-northeast-1.elb.amazonaws.com',
failover: 'failover-alb.ap-northeast-3.elb.amazonaws.com',
};
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// セッションCookieからユーザーIDを取得
const cookieHeader = headers.cookie?.[0]?.value || '';
const sessionMatch = cookieHeader.match(/session_id=([^;]+)/);
if (!sessionMatch) {
// 未認証はprimaryに流す
return request;
}
const sessionId = sessionMatch[1];
try {
// DynamoDBからセッション情報を取得
const result = await ddbClient.send(new GetItemCommand({
TableName: 'user-sessions',
Key: { session_id: { S: sessionId } },
// 必要なattributeだけ取得(転送量削減)
ProjectionExpression: 'user_segment, preferred_region',
}));
const item = result.Item;
if (item?.preferred_region?.S === 'osaka') {
request.origin = {
custom: {
domainName: ORIGINS.failover,
port: 443,
protocol: 'https',
sslProtocols: ['TLSv1.2'],
readTimeout: 30,
keepaliveTimeout: 5,
customHeaders: {},
path: '',
}
};
// ヘッダーも書き換え
request.headers['host'] = [{ key: 'Host', value: ORIGINS.failover }];
}
} catch (err) {
// エラー時はsilent fallback(primary継続)
console.error('DynamoDB lookup failed:', err.message);
}
return request;
};
このコードで一番ハマったのがコールドスタートとDynamoDB接続の組み合わせだった。Lambda@Edgeはコールドスタート時にTCPコネクションの確立コストがそのまま乗っかってくる。DynamoDB接続をハンドラー外でグローバルスコープに出してコネクション再利用するようにしたら、ウォームアップ後のp99が45ms→18msに改善した。地味に効果が大きかったので、同じ構成を組む人は最初からそうしておくといい。
実際に運用してわかった選定基準
3ヶ月本番運用した結果、うちのチームで使ってる判断フロー。
flowchart TD
A["エッジで処理が必要"] --> B{"外部API・\nAWS SDKの\nアクセスが必要?"}
B -- Yes --> C["Lambda@Edge一択"]
B -- No --> D{"実行時間が\n1ms以内に\n収まる?"}
D -- No --> E["Lambda@Edge"]
D -- Yes --> F{"処理するフェーズは?"}
F -- "Viewer Request/Response" --> G["CloudFront Functions 推奨"]
F -- "Origin Request/Response" --> H{"ロジックが\nシンプル?"}
H -- Yes --> I["Lambda@Edge\n(Origin側)"]
H -- No --> J["Lambda@Edge\n+ 必要に応じてVPC統合"]
G --> K{"コスト・\nレイテンシ\n最優先?"}
K -- Yes --> L["✅ CloudFront Functions"]
K -- No --> M["どちらでもOK"]
正直このフローでも「グレーゾーン」が存在する。たとえばA/Bテストでのトラフィック振り分け。シンプルなCookie設定だけならCloudFront Functionsで十分だけど、「機械学習モデルのスコアに基づいて振り分けたい」みたいな要件が出てきた瞬間にLambda@Edge一択になる。最初から要件を詰めておかないと、後からアーキテクチャごと作り直すはめになるので注意が必要だった。
コスト面の実測
うちの構成での実際の月次コストを記録しておく(月間リクエスト数:約2億回)。
xychart-beta
title "月次エッジ関数コスト実測(USD)"
x-axis ["CF Functions (URLリライト)", "CF Functions (セキュリティヘッダー)", "L@E Viewer Request", "L@E Origin Request"]
y-axis "月次コスト (USD)" 0 --> 300
bar [22, 18, 145, 265]
Lambda@EdgeのOrigin Request側が高いのは想定内だったけど、Viewer Request側も思ったよりコストが乗ってきた。月次で$145というのはそこまで大きな数字には見えないかもしれないけど、CloudFront Functionsに置き換えれば$20前後に下がる処理が混ざってたりする。CloudFront Functionsに移行できる処理は早めに動かしたほうがいい。
2026年時点の新しい選択肢:CloudFront KeyValueStore
2024年末にGA(一般提供)されたCloudFront KeyValueStoreが、2026年現在うちのチームで地味に役立っている。CloudFront Functionsの最大の弱点だった「外部データ参照ができない」問題を、読み取り専用のKey-Valueストアで部分的に解決してくれる。
// CloudFront Functions + KeyValueStore
import cf from 'cloudfront';
const kvsHandle = cf.kvs('YOUR_KVS_ID');
async function handler(event) {
const request = event.request;
const country = event.viewer.ip; // 実際はCloudFrontのヘッダーから取得
// Feature Flagをキャッシュなしで取得(TTL管理はKVS側)
let featureEnabled = false;
try {
const value = await kvsHandle.get('feature_new_checkout');
featureEnabled = value === 'true';
} catch (e) {
// KVSアクセス失敗時は安全側に倒す
featureEnabled = false;
}
if (featureEnabled && request.uri.startsWith('/checkout')) {
request.uri = '/checkout-v2' + request.uri.slice('/checkout'.length);
}
return request;
}
最初は「DynamoDBの代替になるか」と期待しすぎてたけど、容量制限(5MB)と読み取り専用という制約を理解してから使い方が定まった。Feature FlagやA/Bテストの設定値を入れておくくらいが丁度いい用途で、それ以上を求めると設計が歪む。
運用で困ったこと、正直に全部書く
Lambda@Edgeのデプロイが遅い問題は2026年現在も未解決だった。CloudFrontのエッジロケーション全体にデプロイが伝播するまで最大で20〜30分かかる。バグ修正を緊急デプロイしたいときに、この待ち時間が本当につらい。CloudFront Functionsは2〜3分で終わるのでここも差がある。インシデント対応中の30分待ちは精神的にかなりきつかった。
Lambda@Edgeのログがus-east-1に集まらない問題も当初ハマった。Lambda@EdgeはリクエストのエッジロケーションのAWSリージョンにCloudWatch Logsが散らばる。東京から来たリクエストのログはap-northeast-1に、シンガポールはap-southeast-1に……という感じで、デバッグのたびに各リージョンを確認する羽目になる。これはCloudWatch vs Datadog 2026の比較記事でも触れたけど、マルチリージョンのログ集約は外部ツールが正直楽。うちは結局CloudWatch Logs Insightsのクロスアカウントクエリ機能で一箇所から検索できるようにした。
CloudFront Functionsのデバッグ環境も整備に時間がかかった。ローカルでのテスト実行環境が整備されておらず、公式のエミュレーターが2026年になってようやくまともになってきた印象。それまではAWS SAMを使った擬似的なローカルテストでしのいでた。個人的にはここが一番「もう少し早く整備してほしかった」と思ってる部分だった。
インシデント対応の観点では、エッジ関数のバグはリージョン単位で対処できないため影響範囲が広い。バグが出たときの緊急回避手段(Feature Flagでエッジ処理をバイパスする仕組み)を事前に用意しておくのは必須だと思う。インシデント対応の最新ベストプラクティスも合わせて参考にしてほしい。
セキュリティ面では、エッジで動く関数にJWTの検証ロジックを持たせる場合、シークレットの管理に注意が必要。Lambda@EdgeはSecrets Manager・Parameter Storeへのアクセスができるが、その都度ネットワーク呼び出しが発生するので、適切にキャッシュする設計が必要だった。CloudFront FunctionsはKeyValueStoreに公開鍵だけ置いてJWT署名検証する方法もある。OWASP Top 10の対策実装との絡みで、エッジでの認証・認可設計は別記事でも書きたいと思ってる。
まとめ
3ヶ月本番運用して見えてきた結論を整理するとこんな感じ。
-
デフォルトはCloudFront Functions: URLリライト・セキュリティヘッダー・A/Bテストの振り分けはほぼこれで完結する。レイテンシとコストの両面で優位
-
Lambda@Edgeが必要なのは「データ参照」か「複雑なビジネスロジック」があるとき: DynamoDB・外部API・Secrets Managerへのアクセスが必要な処理は迷わずLambda@Edge。ただしコールドスタートとデプロイ遅延は覚悟する
-
CloudFront KeyValueStoreが第三の選択肢に育ってきた: Feature Flagや設定値程度なら、Lambda@Edgeを使わずCloudFront Functions + KVSで解決できるケースが増えた
-
両方を混在させるなら役割境界を明確に: 「Viewer側はCF Functions、Origin側はLambda@Edge」くらいの大雑把な境界線でもよい。グレーゾーンに悩みすぎず動かしながら見直すのが現実的
-
ログ設計とデプロイ戦略は最初から考える: 後から整備しようとするとかなりしんどい。特にLambda@Edgeのマルチリージョンログ集約は最初の設計フェーズで決めておくことを強くすすめる
次のアクションとしては、まずCloudFront Functionsで始めてみて、「外部データ参照が必要」「1ms超える処理が必要」という壁に当たったらLambda@Edgeに切り替える、という順序が一番スムーズだと思う。最初からLambda@Edgeに全部突っ込むのは、運用コストが想像以上にかかるのでおすすめしない。
皆さんのチームではどっちメインで運用してますか?特にCloudFront KeyValueStoreの使い方、もっと面白いユースケースがあれば聞いてみたい。