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 キャッシュ内にある場合、関数は実行されません。

https://aws.amazon.com/jp/blogs/news/lambdaedge-design-best-practices/

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html

# テスト方法
Lambdaでテストする際のイベント構造は、以下のjson形式のコードをコピペするとよいです。



https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request

![スクリーンショット 2022-05-05 11.38.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/993efced-95c5-41fe-fbbc-d12439086932.png)

# Lambda@Edgeの制限
## イベントオブジェクトのフィールドの制限
L@Eで使用できるclientIp、path、Hostなどは、読み取り専用、もしくは書き込み可能（内容の修正）かどうか下記のリンクから確認できます。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request

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

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

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/edge-functions-restrictions.html#lambda-at-edge-function-restrictions

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

# オリジン/ビューアー リクエスト
## ホスト名を書き換える
（オリジンリクエスト）

![スクリーンショット 2022-05-05 11.22.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/9c7d9079-b9db-2ea9-8cea-986a391e540d.png)


パスが`/test/`で始まる場合に限り、ホストを書き換える（オリジン先は変わらない）
```js
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/7a9717be-01b5-c25f-39c4-0adb695fb6fb.png)
```diff_javascript
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`をリクエスト → 成功画面をレスポンス
```js
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/5162ce48-d6a0-5675-6fda-60ba70312f53.png)

```js
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);
};
```
日付の比較は下記を参考にしました。

https://qiita.com/holdout0521/items/c9ad545d15cfed8328d0

## パスが`/`で終わる場合、末尾に`index.html`を付与
（オリジンリクエスト、ビューアーリクエスト）

パスが`/`で終わる場合、`index.html`を付与してリクエストします。
```js
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です。
```js
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/2777b564-1c95-9d6d-a8a9-b847d0693ffe.png)

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

まず、オリジンリクエストポリシーのヘッダーに対して、`全てのビューアーヘッダーと次のCloudFrontヘッダー`を選択し、`CloudFront-Is-Android-Viewer`と`CloudFront-Is-IOS-Viewer`のヘッダーを追加し、ビヘイビアのオリジンリクエストポリシーに設置します。
![スクリーンショット 2022-05-07 23.51.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/39904570-2b13-3543-0b45-734617339627.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/c61ea9a0-3789-9b28-dc0e-c4ae10b9d7bb.png)

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



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

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/using-cloudfront-headers.html

L@Eは以下のコードを貼り付けます。
```js: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/6f23e431-a505-f0e6-2a44-36d6925385c5.png)

### eventの内容
`CloudFront-Is-Android-Viewer`のヘッダーを追加した場合のオリジンリクエスト時のL@Eが受け取ったevent内容は、以下のようになっていました。
```json:オリジンリクエストで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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/98b4b3ea-ebc1-7e6d-2f69-a16562ffb04e.png)

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


L@Eは以下のコードは、以下の通りにします。
```js
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 フォームを変更する

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html



# 参考

https://wellknowledge.org/lambda-edge-redirect/

https://qiita.com/kooohei/items/7fcbc3602f7b7026b5a8#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4


https://qiita.com/hatappo/items/110746fdbfb3918b54cc

https://bokyung.dev/ja/2021/05/14/lambda-edge-resize/