3
Help us understand the problem. What are the problem?

posted at

updated at

Lambda@Edge のリクエスト利用パターン集

はじめに

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形式のコードをコピペするとよいです。

スクリーンショット 2022-05-05 11.38.22.png

Lambda@Edgeの制限

イベントオブジェクトのフィールドの制限

L@Eで使用できるclientIp、path、Hostなどは、読み取り専用、もしくは書き込み可能(内容の修正)かどうか下記のリンクから確認できます。

例えば、pathの場合、読み取りと書き込みが可能だとわかります。

path (読み取り/書き込み) (カスタムおよび Amazon S3 オリジン)
リクエストがコンテンツを検索するサーバーのディレクトリパス。パスは、先頭をスラッシュ (/) にする必要があります。末尾をスラッシュ (/) にすることはできません (たとえば、末尾が example-path/ は不可です)。カスタムオリジンの場合のみ、パスは URL エンコードされ、最大長は 255 文字にする必要があります。

ヘッダーに対しての制限

また、ビューアーリクエスト、オリジンリクエストでもヘッダーに対して、読み取り専用、もしくは書き込みも可能かどうか下記リンクで確認できます。

例えば以下のヘッダーは、ビューワーリクエストでは読み取り専用になります。

  • Content-Length
  • Host
  • Transfer-Encoding
  • Via

オリジン/ビューアー リクエスト

ホスト名を書き換える

(オリジンリクエスト)

スクリーンショット 2022-05-05 11.22.32.png

パスが/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に変更する
スクリーンショット 2022-05-05 11.24.55.png

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に記載している期間の間だった場合、リクエストに対して、オリジンにリクエストを送らず、レスポンスを返す。

スクリーンショット 2022-05-05 11.41.18.png

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からリクエストされた場合のみリクエストを通す

(オリジンリクエストのみ。ビューアーリクエストは不可

スクリーンショット 2022-08-06 10.39.26.png

androidもしくはiosからのリクエストのみ、リクエストを通し、それ以外の端末からのリクエストは、特定のレスポンスを返します。

まず、オリジンリクエストポリシーのヘッダーに対して、全てのビューアーヘッダーと次のCloudFrontヘッダーを選択し、CloudFront-Is-Android-ViewerCloudFront-Is-IOS-Viewerのヘッダーを追加し、ビヘイビアのオリジンリクエストポリシーに設置します。
スクリーンショット 2022-05-07 23.51.47.png

CloudFront-Is-Android-Viewer
-- ビューワーが Android オペレーティングシステム搭載のデバイスであると CloudFront が判断すると、true に設定します。

CloudFront-Is-IOS-Viewer
-- ビューワーが iPhone、iPod touch、その他の iPad デバイスなど、Apple モバイルオペレーティングシステム搭載のデバイスであると CloudFront が判断すると、true に設定します。

スクリーンショット 2022-05-07 23.53.32.png

キャッシュしたい場合、キャッシュポリシーにもヘッダーでCloudFront-Is-Android-ViewerCloudFront-Is-IOS-Viewerを追加します。

他の追加できるHTTPヘッダーは、以下になります。

L@Eは以下のコードを貼り付けます。

index.js
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がレスポンスされたことが分かりました!
スクリーンショット 2022-05-07 23.58.59.png

eventの内容

CloudFront-Is-Android-Viewerのヘッダーを追加した場合のオリジンリクエスト時のL@Eが受け取ったevent内容は、以下のようになっていました。

オリジンリクエストで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からリクエストされた場合、オリジン先を変える

(オリジンリクエストのみ。ビューアーリクエストは不可
スクリーンショット 2022-08-06 10.38.42.png

オリジンリクエストポリシー、キャッシュポリシーは、先程と同じです。

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 フォームを変更する

参考

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
3
Help us understand the problem. What are the problem?