Lambda@Edge のオリジンリクエストで、bodyの内容を書き換えてレスポンスを返す方法 [sync-request]

# はじめに

Lambda@Edge(以後、L@E)で、bodyの一部を書き換えてレスポンスを返す方法についてまとめました。

L@Eでbodyを書き換えるには、ビューアーリクエストもしくはオリジンリクエストで可能です。2つの違いは以下になります。

>ビューワーリクエスト
>- CloudFront がビューワーからリクエストを受け取ると、リクエストされたオブジェクトが CloudFront キャッシュにあるかどうかを確認する前に関数が実行されます。

>オリジンリクエスト
>- CloudFront がリクエストをオリジンに転送したときにのみ、関数が実行されます。リクエストされたオブジェクトが CloudFront キャッシュ内にある場合、関数は実行されません。
L@Eでbodyを書き換える際の制限は他にもあり、下記のドキュメントに記載されています。

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

L@Eのリクエストボディへのアクセスの例になります。

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

# やりたいこと
リクエスト時、L@Eによって、bodyタグ内の`<body>直下に<p>test</p>`を追加し、レスポンスをする。

特定のビヘイビアのみにL@Eを有効にすることで、特定のパスのみ、body内を修正した内容をレスポンスすることができます。(今回は、`<p>test</p>`を追加)

理想は、レスポンス時にL@Eでできれば良いのですが、前述の通り、bodyの読み書きは、リクエスト時にしかできません。

![スクリーンショット 2022-05-07 9.51.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/b042689c-2f34-bc0c-e80f-ede3b5e7b5f8.png)

# 実現方法
L@Eでは、オリジンレスポンスやビューアーレスポンスでは書き換えができないため、今回は`オリジンリクエストをトリガー`とし、`sync-request`を使用します。

https://www.npmjs.com/package/sync-request/v/6.1.0

`sync-request`モジュールは、同期処理でHTTP通信を行うことができ、同期処理というのがポイントになります。

L@Eの処理の流れについて説明します。


1. リクエストが来た際、CloudFrontのL@Eがトリガーされ、`sync-request`によって同じurlにリクエストします。その際、L@Eはオリジンへのリクエストはせず、`sync-request`の処理を待ちます。
1. `sync-request`で同じurlでリクエストする際、クエリ文字列で`任意の文字列を付与します（今回は、lambdaSkip=1）`
1. `sync-request`によって、① と同様にCloudFrontのL@Eがトリガーされますが、L@Eでは、クエリ文字列として`lambdaSkip=1`があった場合、`lambdaSkip=1`を外し、オリジンにリクエストする処理を行います。
1. `sync-request`によってリクエストしたbodyがL@Eにレスポンスされます。
1. L@Eは、受け取ったbodyを書き換え、オリジンにリクエストせず、レスポンスを返します。

この処理の流れについて、`sync-request`がリクエストした（同期処理した）箇所は、②〜⑤であり、L@Eは`sync-request`の②〜⑤の処理を待ち、⑥でレスポンスを返します。

図にすると以下の流れになります。

![スクリーンショット 2022-05-07 10.00.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/8efc0e77-99b8-a105-bed1-7e167dc4681d.png)
![スクリーンショット 2022-05-07 10.00.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/271b0cd1-3533-5819-f227-aaf0c727dd3d.png)

# 前提
- CloudFront,オリジン先を作成済み。今回オリジン先は、ALBです。

# L@E作成

L@Eは、バージニア州で作成します。
![スクリーンショット 2022-05-07 10.30.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/96798b3c-bf14-edb0-3758-9117b2ed338d.png)

L@Eの実行タイムアウトは、`3秒→30秒`に変更します。
![スクリーンショット 2022-05-07 19.07.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/591dae30-bed9-2d73-162f-bb090f02428f.png)

## sync-requestモジュールをL@Eにアップロード
ローカルでsync-requestモジュールをインストールし、L@Eにアップロードします。

```terminal
% mkdir nodejs

% cd nodejs 

nodejs % npm i sync-request

added 40 packages, and audited 41 packages in 7s

6 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

nodejs % ls
node_modules		package-lock.json	package.json

nodejs % touch index.js

nodejs % ls
index.js		node_modules		package-lock.json	package.json

nodejs % zip -r project.zip *
```
`project.zip`を作成したL@Eにアップロードします。


![スクリーンショット 2022-05-07 10.32.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/8a3ed89e-5743-3ae7-9fe0-4b22296f9004.png)
ディレクトリの配置は、以下のようになります。
![スクリーンショット 2022-05-07 19.09.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/d975537b-ed2e-c208-a7a1-b7cddb074d58.png)

## index.js
index.jsに以下のコードを貼り付けます。

L@Eのオリジンリクエストの構造 （json形式のコード） を見ると理解しやすいかと思います。

また、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)

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

```js
const host = "www.fuga.com";

const syncRequest = require("sync-request");
const { URLSearchParams } = require("url");

exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  console.log("Received event:", JSON.stringify(request, null, 2));

  const params = new URLSearchParams(request.querystring);
  const lambdaSkip = params.get("lambdaSkip");

  //クエリ文字列lambdaSkipがある場合、クエリ文字列を外して、オリジンにリクエストを出す
  if (lambdaSkip) {
    request.querystring = "";
    callback(null, request);
  } else {
    // sync-requestがクエリ文字列lambdaSkip付きでリクエストを出す
    const uri = request.uri;
    const url = "http://" + host + uri + "?lambdaSkip=1"; // httpsでも可
    const syncResponse = syncRequest("GET", url, { timeout: 28000 });

    // sync-requestのレスポンスを受け取る
    const syncResponseBody = syncResponse.getBody("utf8");

    // 受け取ったレスポンスのbodyを書き換える
    const bodyAddScript = "<body><p>test</p>";
    const syncResponseBodyRewrite = syncResponseBody.replace(
      "<body>",
      bodyAddScript
    );
    console.log(syncResponseBodyRewrite);

    const BodyRewriteResponse = {
      status: 200,
      statusDescription: "OK",
      headers: {
        "content-type": [
          {
            key: "Content-Type",
            value: "text/html; charset=UTF-8",
          },
        ],
      },
      body: syncResponseBodyRewrite,
    };
    // bodyを書き換えたものをレスポンスする
    callback(null, BodyRewriteResponse);
  }
};
```
### index.jsの一部コード解説
#### const { URLSearchParams } = require("url");
`const { } =` は分割代入と言われています。
この{}はオブジェクトを参照していて分割代入は配列でも使うことができます。
例えば、以下のコードがあったとします。
``` js
const { apple } = { orange: 1, apple: 2, grape: 3 }
console.log(apple) 
//2
```
これを実行すると、appleの値である`2`が表示されます。

また、`require("url")`は、以下の構造になっております。
```js
console.log(require("url")) ;

//結果
{
  Url: [Function: Url],
  parse: [Function: urlParse],
  resolve: [Function: urlResolve],
  resolveObject: [Function: urlResolveObject],
  format: [Function: urlFormat],
  URL: [class URL],
  URLSearchParams: [class URLSearchParams],
  domainToASCII: [Function: domainToASCII],
  domainToUnicode: [Function: domainToUnicode],
  pathToFileURL: [Function: pathToFileURL],
  fileURLToPath: [Function: fileURLToPath],
  urlToHttpOptions: [Function: urlToHttpOptions]
}
```

つまり、`URLSearchParams`の値が変数として定義されたということになります。

#### new URLSearchParams(string)
`new URLSearchParams(string)`は、stringをクエリ文字列として 解析し、それを使用して新しいURLSearchParamsオブジェクトをインスタンス化します。
今回の場合、`string`は、`lambdaSkip=1`になります。

https://nodejs.org/api/url.html#new-urlsearchparams

https://developer.mozilla.org/ja/docs/Web/API/URLSearchParams

#### params.get("string")

`params.get("lambdaSkip")`は、`lambdaSkip`があれば、`1`を返し、ない場合、nullを返します。

https://nodejs.org/api/url.html#urlsearchparamsgetname


下記の記事では、クエリ文字列の様々な処理方法が記載されており参考になります。

https://beyondjapan.com/blog/2017/06/nodejs8-whatwg-url-api/

## L@Eデプロイ

後は、L@Eを特定のCloudFrontのビヘイビアにデプロイします。

`ボディを含める`にチェックしてください。


![スクリーンショット 2022-05-07 11.18.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/019cb7e8-5f95-badb-053e-1c5845c053dc.png)



# CloudFrontの設定

## キャッシュポリシー
L@EにをデプロイしたCloudFrontのビヘイビアのキャッシュポリシーを修正します。
キャッシュポリシーで、クエリ文字列の`lambdaSkip`を含めるようにします。

これがない場合、クエリ文字列をつけて`sync-request`でリクエストした際、`lambdaSkip`が付与した状態でキャッシュされます。
続いて、キャッシュされた状態でリクエストした場合、`lambdaSkip`が付与した状態のurlがレスポンスされてしまい、503が返されます。

そこで、キャッシュポリシーにクエリ文字列の`lambdaSkip`を含めることで、`lambdaSkip`があるurlとないurlがそれぞれキャッシュされ、レスポンス時、`lambdaSkip`がない正常なものがレスポンスされます。


![スクリーンショット 2022-05-07 12.12.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/111867c5-929e-c0f9-cc13-740d95490d45.png)

キャッシュの時間は何でも構いません。キャッシュ時間0でもよいです。

作成したポリシーをビヘイビアに反映させます。

![スクリーンショット 2022-05-07 12.25.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/91ec292b-53d6-51e1-6ab7-e603ac86fb2c.png)

## オリジンリクエストポリシー
オリジンリクエストポリシーは、あってもなくてもよいです。

# アクセスしてみる

では、ブラウザにurlを記載し、にアクセスしてみましょう。
ビヘイビアで指定したパスでアクセスすると、`<body>直下に<p>test</p>`が付きました！
![スクリーンショット 2022-05-07 11.30.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/a13238db-e9e2-3658-1a9a-6ace306e9975.png)

# logの確認
ログは、エッジサーバーのリージョンでCloudWatchLogsで出力されます。
日本でリクエストした場合、東京リージョンにログが出力されるでしょう。

ブラウザで503エラーが出た場合、logを手がかりにでエラーの原因を特定しましょう。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudwatch-metrics-logging.html

# Basic認証している場合
CloudFrontやALBでBasic認証もしくは、Bearer認証の設定をしている場合、sync-requestのヘッダーに`Authorization`と値を付与する必要があります。
```diff_javascript
-   const syncResponse = syncRequest("GET", url, { timeout: 28000 });
    const syncResponse = syncRequest("GET", url, {
      timeout: 28000,
      headers: {
        authorization:
          "Basic <token値>",
      },
    });
    const syncResponseBody = syncResponse.getBody("utf8");
```

# レスポンスまでの時間がかかる場合
L@Eの処理で`sync-request`を使うと、レスポンスまでの時間が遅いと感じる場合があるかと思います。
その場合の対処法は、2つが挙げられます。

- L@Eのメモリのスペックをデフォルトの128MBから上げる。
- L@Eのコード内の`console.log()`を削除する

# 参考

https://www.npmjs.com/package/sync-request/v/6.1.0

https://code-examples.net/ja/q/5c4af8