はじめに
CloudFront でエッジ処理を入れたいと思ったとき、選択肢は 2 つあります。
- CloudFront Functions
- Lambda@Edge
どちらも CloudFront のリクエストやレスポンスに介入できるのですが、考え方や使いどころは変わってきます。
最初に「どっちにしようか」で迷うと、あとからコストが高くなったり、実装できない制約にぶつかったりすることがあります。そこで今回は、両者の違いを整理して、どういうケースでどちらを選ぶべきかを、まとめてみます。
実行タイミングの違い
まず大きな違いは、処理が走るタイミングです。
クライアント → [Viewer Request] → キャッシュ参照 → [Origin Request] → オリジン
オリジン → [Origin Response] → キャッシュ格納 → [Viewer Response] → クライアント
| イベント | CloudFront Functions | Lambda@Edge |
|---|---|---|
| Viewer Request | ◯ | ◯ |
| Viewer Response | ◯ | ◯ |
| Origin Request(キャッシュミス後) | ✗ | ◯ |
| Origin Response(キャッシュミス後) | ✗ | ◯ |
ここが一番重要なポイントです。CloudFront Functions は、基本的に Viewer のイベントでしか動きません。
キャッシュミス後にだけ処理を挟みたいなら、Lambda@Edge を使うのが基本です。逆に、全リクエストに対してキャッシュ参照前に軽い処理を入れたいなら、CloudFront Functions のほうが向いています。
スペック比較
| 項目 | CloudFront Functions | Lambda@Edge |
|---|---|---|
| 言語 | JavaScript (ES 5.1) | Node.js / Python |
| 実行場所 | 全エッジロケーション | リージョナルエッジキャッシュ |
| 実行時間上限 | 1 ミリ秒 | Viewer: 5 秒 / Origin: 30 秒 |
| メモリ | 2 MB | 128 MB〜10 GB |
| コードサイズ | 最大 10 KB | 最大 50 MB |
| ネットワークアクセス | 不可 | 可能 |
| 外部ライブラリ | 不可 | 可能 |
| リクエストボディ | 触れない | 触れる |
| 料金 | 約 $0.10 / 100万リクエスト (リージョンによって異なる) | リクエスト + 実行時間課金 |
| スケーラビリティ | 毎秒数千万リクエスト | 高いが規模を選ぶ |
ざっくり言うと、
- CloudFront Functions = 軽くて速いが、制約がある
- Lambda@Edge = 高機能で柔軟だけど、コストと設計の難しさがある
CloudFront Functions が向いているケース
CloudFront Functions は、次のような「軽い前処理・後処理」に向いています。
- キャッシュキーの正規化(URL やクエリの小文字化・ソート)
- リクエストヘッダーの追加・削除・書き換え
- 簡単なリダイレクト
共通点は、外部通信が不要で、ほぼ文字列操作だけで完結することです。処理時間の上限があるので、重いロジックには向きませんが、こういう用途ならほとんど問題になりません。
Lambda@Edge が向いているケース
一方で Lambda@Edge は、次のような「複雑な処理や外部連携が必要なケース」に向いています。
- JWT 検証・認証・認可
- 外部データソース参照によるルーティング(DynamoDB、S3 など)
- 動的なオリジン選択
- リクエストボディの検査・変換
- キャッシュミス時だけ実行したい処理
- 画像のリサイズ・変換
選定の目安
- Origin Request / Response で実行
▶Lambda@Edge - 外部 API や DB、S3 を参照する
▶ Lambda@Edge - リクエストボディ見るか
▶Lambda@Edge - 1 ミリ秒以内で終わる軽い処理
▶CloudFront Functions
基本的には、CloudFront Functions でできるかを最初に見て、制約に引っかかった際Lambda@Edgeという選び方が無難かなと思います。
キャッシュキー正規化
ここからは、具体的なユースケースとして「キャッシュキーの正規化」を紹介します。
問題
CloudFront は、リクエストの URL・クエリ文字列・ヘッダーなどを組み合わせてキャッシュキーを作ります。なので、たとえば次のような URL の揺れがあると、すべて別キャッシュとして扱われます。
/images/Logo.png?size=large&format=webp
/images/logo.png?format=webp&size=large
/images/LOGO.png?SIZE=large&FORMAT=webp
見た目は同じ画像に見えても、CloudFront から見ると別リクエストです。
その結果として、次のようなことが起こります。
- キャッシュヒット率が下がる
- オリジンへのアクセスが増える
- レスポンスが遅くなる
- コストが上がる
前提として、CloudFront のキャッシュポリシーでクエリ文字列をキャッシュキーに含める設定にしている場合の話です。デフォルトでは、クエリ文字列はキャッシュキーに含まれないので、ここは構成次第です。
解決策 Viewer Request で正規化する
キャッシュ参照の前に、URL とクエリ文字列を正規化してしまえば、同じリソースへのアクセスは同じキャッシュキーにまとまります。
CloudFront Functions での実装例
function handler(event) {
var request = event.request;
// URL を小文字に統一
if (request.uri) {
request.uri = request.uri.toLowerCase();
}
// クエリを小文字化してソート
var query = request.querystring || {};
var normalized = {};
var keys = Object.keys(query).sort();
for (var i = 0; i < keys.length; i++) {
var key = keys[i].toLowerCase();
var value = query[keys[i]];
normalized[key] = {
value: value && value.value ? value.value.toLowerCase() : ""
};
}
request.querystring = normalized;
return request;
}
なお、URI を小文字に統一する場合は注意が必要です。S3 のオブジェクトキーは大文字と小文字を区別することがあり、
Logo.pngとlogo.pngが別ファイルになることがあります。既存のオブジェクト名が大文字小文字混在していると、小文字化で参照できなくなることがあるので注意してください。ALB やアプリケーション側でルーティングを case-insensitive にしている構成なら、安全です。
Lambda@Edge で同じことをやる場合
参考までに、Viewer Request で Lambda@Edge で同じ処理を書くと、こんな感じになります。
from urllib.parse import parse_qs, urlencode
def lambda_handler(event, context):
request = event['Records'][0]['cf']['request']
request['uri'] = request['uri'].lower()
params = {k: v[0] for k, v in parse_qs(request['querystring'].lower()).items()}
sorted_params = sorted(params.items(), key=lambda x: x[0])
request['querystring'] = urlencode(sorted_params)
return request
動かすだけなら同じようにできます。ただし、Lambda@Edge は Viewer Request でも実行コストや時間がかかります。今回のような単純な正規化だけであれば、CloudFront Functions のほうが合理的です。
ここは大事なポイントですが、Lambda@Edge の Origin Request はキャッシュ参照後に走るので、ここでクエリ文字列を正規化してもキャッシュキーの改善にはつながりません。キャッシュキーを統一したいなら、必ず Viewer Request で処理する必要があります。
まとめ
結論としては、まず CloudFront Functions でできないかを考えるのがいいのではないかと思いました。
制約に引っかかるようなケースだけ、Lambda@Edge に切り替えるのがいちばんいいのではないでしょうか。