はじめに
Lambda@Edge(以後、L@E)を様々なパターンで使用したため、まとめます。
CF2とL@Eの比較
CloudFront Functions に比べて、オリジンリクエスト、オリジンレスポンスで使用できる点や、リクエスト本文を修正できる点において、L@Eは優れているかと思います。
1 | 2 | 3 |
---|---|---|
CloudFront Functions | Lambda@Edge | |
ランタイムサポート | JavaScript(ECMAScript 5.1準拠) | Node.js、Python |
実行場所 | 218 以上の CloudFrontエッジロケーション | 13 の CloudFrontリージョンのエッジキャッシュ |
CloudFront トリガー | ビューアリクエスト / ビューアレスポンス | ビューアリクエスト / ビューアレスポンス / オリジンリクエスト / オリジンレスポンス |
最大実行時間 | 1 ミリ秒未満 | 5 秒 (ビューアトリガー) / 30 秒 (オリジントリガー) |
最大メモリ | 2 MB | 128 MB (ビューアトリガー) / 10 GB (オリジントリガー) |
合計パッケージサイズ | 10 KB | 1 MB (ビューアトリガー) / 50 MB (オリジントリガー) |
ネットワークアクセス | なし | あり |
ファイルシステムアクセス | なし | あり |
リクエスト本文へのアクセス | なし | あり |
料金 | 無料利用枠あり。リクエストごとに課金。 | 無料利用枠なし。リクエストと関数の実行時間ごとに課金。 |
デプロイ反映時間 | 10秒以下 | 5分程度 |
デプロイ方法 | 複数を一度にデプロイ可能 | 1つずつデプロイ可能 |
ビューワーリクエストとオリジンリクエストの違いは以下になります。
ビューワーリクエスト
- CloudFront がビューワーからリクエストを受け取ると、リクエストされたオブジェクトが CloudFront キャッシュにあるかどうかを確認する前に関数が実行されます。
オリジンリクエスト
- CloudFront がリクエストをオリジンに転送したときにのみ、関数が実行されます。リクエストされたオブジェクトが CloudFront キャッシュ内にある場合、関数は実行されません。
テスト方法
Lambdaでテストする際のイベント構造は、以下のjson形式のコードをコピペするとよいです。
Lambda@Edgeの制限
イベントオブジェクトのフィールドの制限
L@Eで使用できるclientIp、path、Hostなどは、読み取り専用、もしくは書き込み可能(内容の修正)かどうか下記のリンクから確認できます。
例えば、pathの場合、読み取りと書き込みが可能だとわかります。
path (読み取り/書き込み) (カスタムおよび Amazon S3 オリジン)
リクエストがコンテンツを検索するサーバーのディレクトリパス。パスは、先頭をスラッシュ (/) にする必要があります。末尾をスラッシュ (/) にすることはできません (たとえば、末尾が example-path/ は不可です)。カスタムオリジンの場合のみ、パスは URL エンコードされ、最大長は 255 文字にする必要があります。
ヘッダーに対しての制限
また、ビューアーリクエスト、オリジンリクエストでもヘッダーに対して、読み取り専用、もしくは書き込みも可能かどうか下記リンクで確認できます。
例えば以下のヘッダーは、ビューワーリクエストでは読み取り専用になります。
- Content-Length
- Host
- Transfer-Encoding
- Via
オリジン/ビューアー リクエスト
ホスト名を書き換える
(オリジンリクエスト)
パスが/test/
で始まる場合に限り、ホストを書き換える(オリジン先は変わらない)
exports.handler = async (event, context, callback) => {
const request = event.Records[0].cf.request;
console.log("Received event:", JSON.stringify(request, null, 2));
const uri_app_path = request.uri.startsWith("/test/");
if (uri_app_path) {
if (request.headers["host"]) {
const origin = "www.huga.com"; // 変更先のホスト名
request.headers["host"] = [{ key: "Host", value: origin }];
}
}
return callback(null, request);
};
下記によって、CloudWatchLogsにevent
の内容がログとして記録されます
console.log("Received event:", JSON.stringify(request, null, 2));
オリジン先を書き換える
(オリジンリクエスト)
パスが/test/
で始まる場合に限り、オリジン先をs3に変更する
exports.handler = async (event, context, callback) => {
const request = event.Records[0].cf.request;
console.log("Received event:", JSON.stringify(request, null, 2));
const uri_app_path = request.uri.startsWith("/test/");
if (uri_app_path) {
const origin = "test-s3-xxxxxxxxxx.s3.ap-northeast-1.amazonaws.com"; // 変更先のオリジン先
+ request.origin.custom.domainName = origin;
}
return callback(null, request);
};
パスに応じて、静的ページの内容を変える
(オリジンリクエスト、ビューアーリクエスト)
リクエストのパス配下にerror
が含まれている場合とsuccess
が含まれている場合で、静的ページの表示を変える。
どちらも含んでいない場合、オリジンにリクエストされる
https://test.com/hoge/error
をリクエスト → エラー画面をレスポンス
https://test.com/hoge/success
をリクエスト → 成功画面をレスポンス
const success_content = `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
成功しました!
</body>
</html>
`;
const error_content = `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
失敗しました
</body>
</html>
`;
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
const uri_success = '\/success\/';
const uri_error = '\/error\/';
if(uri.match(uri_success)){
var display_content = success_content;
} else if(uri.match(uri_error)){
var display_content = error_content;
}
try {
const response = {
status: '200',
statusDescription: 'OK',
headers: {
'content-type': [{
key: 'Content-Type',
value: 'text/html'
}]
},
body: display_content
};
callback(null, response);
return;
} catch (ex) { }
//requestを変更せずcloudfrontに返す
callback(null, request);
return request;
};
特定の日時の期間のみ、メンテナンスの情報をjsonで返す
(オリジンリクエスト、ビューアーリクエスト)
enabled
がbool型でtrue
、かつ、現在時刻がmaintenanceContent
に記載している期間の間だった場合、リクエストに対して、オリジンにリクエストを送らず、レスポンスを返す。
const maintenanceContent = {
enabled: true,
from: "2022-05-05T12:00+09:00",
to: "2022-05-05T12:08+09:00",
};
const now = Date.now();
const maintenance_from = new Date(maintenanceContent.from).getTime();
const maintenance_to = new Date(maintenanceContent.to).getTime();
const maintenance_enabled = maintenanceContent.enabled;
const maintenanceResponce = {
status: "200",
statusDescription: "OK",
headers: {
"content-type": [
{
key: "Content-Type",
value: "application/json",
},
],
},
body: JSON.stringify(maintenanceContent, null, 2),
};
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
if (maintenance_enabled && maintenance_from <= now && now <= maintenance_to) {
callback(null, maintenanceResponce);
return;
}
callback(null, request);
};
日付の比較は下記を参考にしました。
パスが/
で終わる場合、末尾にindex.html
を付与
(オリジンリクエスト、ビューアーリクエスト)
パスが/
で終わる場合、index.html
を付与してリクエストします。
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const oldUri = request.uri;
const newUri = oldUri.replace(/\/$/, "/index.html");
request.uri = newUri;
return callback(null, request);
};
Basic認証
(ビューワーリクエスト)
ユーザー名:user、パスワード:passです。
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// Configure authentication
const authUser = "user";
const authPass = "pass";
const authString =
"Basic " + new Buffer(authUser + ":" + authPass).toString("base64");
if (
typeof headers.authorization == "undefined" ||
headers.authorization[0].value != authString
) {
const body = "Unauthorized";
const response = {
status: "401",
statusDescription: "Unauthorized",
body: body,
headers: {
"www-authenticate": [{ key: "WWW-Authenticate", value: "Basic" }],
},
};
callback(null, response);
}
callback(null, request);
};
androidもしくはiosからリクエストされた場合のみリクエストを通す
(オリジンリクエストのみ。ビューアーリクエストは不可 )
androidもしくはiosからのリクエストのみ、リクエストを通し、それ以外の端末からのリクエストは、特定のレスポンスを返します。
まず、オリジンリクエストポリシーのヘッダーに対して、全てのビューアーヘッダーと次のCloudFrontヘッダー
を選択し、CloudFront-Is-Android-Viewer
とCloudFront-Is-IOS-Viewer
のヘッダーを追加し、ビヘイビアのオリジンリクエストポリシーに設置します。
CloudFront-Is-Android-Viewer
-- ビューワーが Android オペレーティングシステム搭載のデバイスであると CloudFront が判断すると、true に設定します。
CloudFront-Is-IOS-Viewer
-- ビューワーが iPhone、iPod touch、その他の iPad デバイスなど、Apple モバイルオペレーティングシステム搭載のデバイスであると CloudFront が判断すると、true に設定します。
キャッシュしたい場合、キャッシュポリシーにもヘッダーでCloudFront-Is-Android-Viewer
とCloudFront-Is-IOS-Viewer
を追加します。
他の追加できるHTTPヘッダーは、以下になります。
L@Eは以下のコードを貼り付けます。
const content = `
<\!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
</head>
<body>
<p>not android or ios</p>
</body>
</html>
`;
const response = {
status: "200",
statusDescription: "OK",
headers: {
"content-type": [
{
key: "Content-Type",
value: "application/json",
},
],
},
body: JSON.stringify(content, null, 2),
};
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
console.log("Received event:", JSON.stringify(event, null, 2));
const headers = request.headers;
if (
headers["cloudfront-is-ios-viewer"] &&
headers["cloudfront-is-ios-viewer"][0].value === "true"
) {
console.log("ios");
callback(null, request);
return;
} else if (
headers["cloudfront-is-android-viewer"] &&
headers["cloudfront-is-android-viewer"][0].value === "true"
) {
console.log("android");
callback(null, request);
return;
}
console.log("request");
callback(null, response);
};
デプロイすると、PCからのアクセスは、L@E内で定義したcontentがレスポンスされたことが分かりました!
eventの内容
CloudFront-Is-Android-Viewer
のヘッダーを追加した場合のオリジンリクエスト時のL@Eが受け取ったevent内容は、以下のようになっていました。
{
"Records": [
{
"cf": {
"config": {
"distributionDomainName": "xxxxxxxx.cloudfront.net",
"distributionId": "xxxxxxxxxx",
"eventType": "origin-request",
"requestId": "6NqWJZsnNWlYcZqoXyjvRpuhbloPUwPDVtHKUT3Lfyse0v3SqR8wMg=="
},
"request": {
"body": {
"action": "read-only",
"data": "",
"encoding": "base64",
"inputTruncated": false
},
"clientIp": "xxx.xxx.xxx.xxx",
"headers": {
"host": [
{
"key": "Host",
"value": "www.fuga.com"
}
],
"cloudfront-is-ios-viewer": [
{
"key": "CloudFront-Is-IOS-Viewer",
"value": "false"
}
],
"cloudfront-is-android-viewer": [
{
"key": "CloudFront-Is-Android-Viewer",
"value": "true"
}
],
"accept-language": [
{
"key": "Accept-Language",
"value": "ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7"
}
],
"accept": [
{
"key": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
}
],
"x-forwarded-for": [
{
"key": "X-Forwarded-For",
"value": "xxx.xxx.xxx.xxx"
}
],
"user-agent": [
{
"key": "User-Agent",
"value": "Mozilla/5.0 (Linux; Android 11; M2101K9R) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Mobile Safari/537.36"
}
],
"via": [
{
"key": "Via",
"value": "1.1 ee0deed2be76715690fbb6f80352497a.cloudfront.net (CloudFront)"
}
],
"cache-control": [
{
"key": "Cache-Control",
"value": "max-age=0"
}
],
"upgrade-insecure-requests": [
{
"key": "Upgrade-Insecure-Requests",
"value": "1"
}
],
"accept-encoding": [
{
"key": "Accept-Encoding",
"value": "gzip, deflate"
}
]
},
"method": "GET",
"origin": {
"custom": {
"customHeaders": {},
"domainName": "test.ap-northeast-1.elb.amazonaws.com",
"keepaliveTimeout": 5,
"path": "",
"port": 80,
"protocol": "http",
"readTimeout": 30,
"sslProtocols": ["TLSv1", "TLSv1.1", "TLSv1.2"]
}
},
"querystring": "",
"uri": "/"
}
}
}
]
}
androidもしくはiosからリクエストされた場合、オリジン先を変える
オリジンリクエストポリシー、キャッシュポリシーは、先程と同じです。
L@Eは以下のコードは、以下の通りにします。
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
console.log('Received request:', JSON.stringify(request, null, 2));
const headers = request.headers;
if (
headers['cloudfront-is-ios-viewer'] &&
headers['cloudfront-is-ios-viewer'][0].value === 'true'
) {
request.origin.s3.domainName = 'xxxxx.s3.ap-northeast-1.amazonaws.com';
request.headers.host[0].value = 'xxxxx.s3.ap-northeast-1.amazonaws.com';
console.log('Received ios request:', JSON.stringify(request, null, 2));
} else if (
headers['cloudfront-is-android-viewer'] &&
headers['cloudfront-is-android-viewer'][0].value === 'true'
) {
request.origin.s3.domainName = 'xxxxx.s3.ap-northeast-1.amazonaws.com';
request.headers.host[0].value = 'xxxxx.s3.ap-northeast-1.amazonaws.com';
console.log('Received android request:', JSON.stringify(request, null, 2));
}
callback(null, request);
};
'xxxxx.s3.ap-northeast-1.amazonaws.com'
は、オリジン先です。
他のL@Eの使用パターン
ドキュメントでは、使用例が豊富でした。
一般的な例
-A/B テスト
-レスポンスヘッダーのオーバーライドレスポンスの生成
-静的コンテンツの提供 (生成されたレスポンス)
-HTTP リダイレクトの生成 (生成されたレスポンス)クエリ文字列の操作
-クエリ文字列パラメータに基づくヘッダーを追加する
-キャッシュヒット率を向上させるためのクエリ文字列パラメータの標準化
-認証されていないユーザーをサインインページにリダイレクトする国またはデバイスタイプヘッダー別のコンテンツのパーソナライズ
-ビューワーリクエストを国に固有の URL にリダイレクトする
-デバイスに基づいて異なるバージョンのオブジェクトを供給するコンテンツベースの動的オリジンの選択
-オリジンリクエストトリガーを使用してカスタムオリジンを Amazon S3 オリジンに変更する
-オリジンリクエストトリガーを使用して Amazon S3 オリジンのリージョンを変更する
-オリジンリクエストトリガーを使用して Amazon S3 オリジンからカスタムオリジンに変更する
-オリジンリクエストトリガーを使用して Amazon S3 バケットから別のバケットにトラフィックを徐々に転送する
-オリジンリクエストトリガーを使用して Country ヘッダーに基づいてオリジンのドメイン名を変更するエラーステータスの更新
-オリジンレスポンストリガーを使用してエラーステータスコードを 200 に更新する
-オリジンレスポンストリガーを使用してエラーステータスコードを 302 に更新するリクエストボディへのアクセス
-リクエストトリガーを使用して HTML フォームを読み込む
-リクエストトリガーを使用して HTML フォームを変更する
参考