CloudFrontでLambda@Edgeを使ってHTTPリダイレクトを設定する。

# はじめに
CloudFrontでLambda@Edgeを使ってHTTPリダイレクトする方法についてまとめてみました。
他にも方法はあると思いますのであくまで参考にしていただければと思います。

# 前提条件
- CloudFrontが用意されていること。
- Lambdaの初期作成が終わっていること。(プログラムは空でOK)
- 動作確認言語：Node.js 12.x

# ここでは行わないこと
- CloudFrontについての説明。
- Lambdaの初期作成。
- Lambda@Edgeについての説明。

#事前準備
##IAMロールの修正
Lambda@Edgeを利用するためにはIAMロールの信頼関係を以下のように修正する必要があります。

```json:信頼されたエンティティ
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

Lambda@Edgeとして利用するために信頼されたエンティティに`edgelambda.amazonaws.com`を追加しています。


また、CloudWatchログへのアクセス権限も修正します。

```json:IAMポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*"
            ]
        }
    ]
}
```
なぜなら、Lambda@Edgeのログは**「関数が実行される場所に最も近い AWS リージョンで CloudWatch Logs ログストリームを作成する」**からです。
また、その際のロググループ名は`/aws/lambda/us-east-1.function-name`になります。

# Lambda関数
## ① 全てのアクセスを特定URLへリダイレクトさせる。
ここでは、CFtへのアクセス全てを http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html へリダイレクトさせる例です。

サービスページのドメイン変更などで利用できます。

```js:全てリダイレクト
'use strict';

exports.handler = (event, context, callback) => {
    /*
     * Generate HTTP redirect response with 302 status code and Location header.
     */
    const response = {
        status: '302',
        statusDescription: 'Found',
        headers: {
            location: [{
                key: 'Location',
                value: 'http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html',
            }],
        },
    };
    callback(null, response);
};
```

`value`の部分でリダイレクト先を指定しています。この値を変更することでリダイレクト先を変更できます。

実際に挙動について確認してみます。
まず、*Lambda@Edge*デプロイ前のステータスコードです。

```sh:デプロイ前
% curl -I https://d20glirsrxu021.cloudfront.net/
HTTP/2 200 
content-type: text/html
content-length: 4
...
```

デプロイ後に改めてアクセスしてみます。

```sh:デプロイ後
% curl -I https://d20glirsrxu021.cloudfront.net/
HTTP/2 302 
content-length: 0
location: http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html
server: CloudFront
...
```

ステータスコードが302になり、locationもLambda@Edgeで指定した値になっているのが確認できます。
ブラウザでアクセスしてみると実際の挙動も確認できます。


## ② 特定パス配下のアクセスを特定URLへリダイレクトさせる。
次は特定パスを含むURLに対してリダイレクト設定を行います。
今回は https://d20glirsrxu021.cloudfront.net/test/ 配下のアクセスに対してリダイレクトを行います。

```js:特定パスのみリダイレクト
'use strict';

exports.handler = (event, context, callback) => {
    /*
     * Generate HTTP redirect response with 302 status code and Location header.
     */
    var request = event.Records[0].cf.request;
    // Extract the URI from the request
    var uri = request.uri;  //追加①：URIの取得

    // Match any '/' that occurs at the end of a URI. Replace it with a default index
    var pattern = '^\/test\/'; //追加②：検索文字列の設定
    if(uri.match(pattern)){ //追加③：文字列のマッチング
        request = {
            status: '302',
            statusDescription: 'Found',
            headers: {
                location: [{
                    key: 'Location',
                    value: 'http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html',
                }],
            },
        };
    }  //追加④：追加③の閉じカッコ
    callback(null, request);
};
```
ここでは、matchメソッドを利用することで文字列チェックを行なっています。

それではまた、デプロイ前と後で確認してみます。

```zsh:デプロイ前
% curl -I https://d20glirsrxu021.cloudfront.net/test/index.html
HTTP/2 200 
content-type: text/html
content-length: 4
・・・
```

```zsh:デプロイ後
% curl -I https://d20glirsrxu021.cloudfront.net/test/index.html
HTTP/2 302 
content-length: 0
location: http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html
server: CloudFront
・・・
```

ちゃんとリダイレクトされていることが確認できました。

## ③ 特定パス配下のアクセスを特定URLへリダイレクトさせる。(複数パスの場合)

特定パス配下が複数ある場合はpatternの部分に`|`を使って記載します。

```javascript:変更前
    var pattern = '^\/test\/';
```

```javascript:変更後
    var pattern = '^\/test\/|^\/test2\/';
```


## ④ 特定パス配下へのアクセス以外を特定URLへリダイレクトする。
通常は特定パス配下に対してが多いと思いますが、逆のパターン(特定パス配下以外)の場合、matchメソッドの先頭に`!`を記載します。

```javascript:変更前
    if(uri.match(pattern)){
```

```javascript:変更後
    if(!uri.match(pattern)){
```


## ④ 特定パス配下へのアクセスを同じURLの異なるパスへリダイレクトする。
②のソースをベースに修正します。

```js:パスのリダイレクト
'use strict';

exports.handler = (event, context, callback) => {
    /*
     * Generate HTTP redirect response with 302 status code and Location header.
     */
    var request = event.Records[0].cf.request;
    // Extract the URI from the request
    var uri = request.uri;
    var host = event.Records[0].cf.request.headers["host"][0]; //追加①：ヘッダーのHost情報を取得
    var redirect = 'https://' + host.value + '/test/index.html'; //追加②：リダイレクト先を整形

    // Match any '/' that occurs at the end of a URI. Replace it with a default index
    var pattern = '\/test2\/'; //変更①：test2配下に適用するための設定
    if(uri.match(pattern)){
        request = {
            status: '302',
            statusDescription: 'Found',
            headers: {
                location: [{
                    key: 'Location',
                    value: redirect, //変更②：リダイレクト先の設定
                }],
            },
        };
    }
    callback(null, request);
};
```
リクエストイベントからHost情報を取得することで、クライアントのリクエスト先へ柔軟に対応しています。


# デプロイ
アクションのプルダウンから **Lambda@Edgeへデプロイ** をクリックします。
![スクリーンショット 2020-12-10 23.47.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/84de184c-d813-79b2-0951-4be1b8b24a38.png)
**Lambda@Edgeへのデプロイ** が表示されるので、**新しいCloudFrontトリガーの設定** を選択します。
ディストリビューションには関連付けたい CloudFront の **ディストリビューションID** を入力します。
また、 **CloudFrontイベント** は **ビューアーリクエスト** を選択します。


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

![スクリーンショット 2020-12-10 23.41.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82090/961a5c6f-9250-a904-1cf4-95664c90855f.png)
あとは **Lambda@Edgeへのデプロイを確認** にチェックを入れて **デプロイ** をクリックしたら完了です。


# おわりに
S3がOriginの場合で有効なのはもちろんですが、EC2の場合もCloudFrontで処理できれば負荷の軽減につながるので良いのではと思います。


# 参考URL
[Lambda@Edge 関数の例(レスポンスの生成 - 例)](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html#lambda-examples-generated-response-examples)
[Lambda@Edge イベント構造(リクエストイベント)](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request)
[Lambda 関数の CloudWatch メトリクスと CloudWatch Logs([CloudWatch Logs])](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudwatch-metrics-logging.html#lambda-cloudwatch-logs)
[Lambda 関数をトリガーできる CloudFront イベント](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-cloudfront-trigger-events.html
)