CloudFrontのLambda＠EdgeでCORS設定

# はじめに
下記のような`CloudFront`=>`ALB`=>`EC2`な構成で、CloudFrontのLambda＠EdgeにてCORS対応をする方法について説明します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68254/4f48631d-b53b-a804-98db-9c25b61c4092.png)

CORSについては、[こちら](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)をご覧ください。
Lambda@Edgeについては[こちら](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-edge.html)をご覧ください。

# 事前準備
まずEC2インスタンスを作ってREST APIのサンプルを入れます。
私は、Tonyさんの素晴らしい[Laravel製のRest API Sample](https://github.com/Tony133/LaravelApiRest)を使わせてもらいました。
こちらのソースはCORS対応されているため、下記の方法でCORS対応をOFFにします。

```bash
mv config/cors.php{,.bk}
```

そして、ALBを作成して、EC2インスタンスを紐づけます。

# 手順
## CloudFront Distributionを作成する。
- `CloudFront Distributions`画面で、`Create Distribution`ボタンをクリック
- `Select a delivery method for your content.`画面で、`Web`の`Get Started`ボタンをクリック

- `Create Distribution`で下記のように設定

| 項目名           |       設定値 | 
|:-----------------|------------------|
| Origin Domain Name|        ALBのDNS 名 |
| Allowed HTTP Methods|  GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE |
| Cache Based on Selected Request Headers|  Whitelist|
| Whitelist Headers [^1]  |                Access-Control-Request-Headers<br>Access-Control-Request-Method<br>Authorization<br>Origin|
| Query String Forwarding and Caching| Forward all,cache based on all|

[^1]: 指定するヘッダーについては[こちら](https://aws.amazon.com/jp/premiumsupport/knowledge-center/no-access-control-allow-origin-error/)に詳しく書かれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68254/6ef48297-0ca9-d6f6-2866-32b6885b396c.png)

- `CloudFront Distributions`画面で、作成された`Distribution`のIDをクリックする。
- `General`タブの`ARN`（arn:aws:cloudfront::0000000...）をコピーしておく。
   (後程使います)

## us-east-1でlambdaを作成する。
- Lambdaサービスを選択し、リージョンを`バージニア北部(us-east-1)`に変更し、`関数の作成`ボタンをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68254/fd8f5748-ead7-acc8-28f6-c76872ed3086.png)

### origin request用関数作成
- まずは、origin requestの関数の作成を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68254/9eded22f-aa57-e7d8-3334-79c17c8109f4.png)

- 関数コードの部分に下記を入力して、`保存`ボタンをクリックします。
- originでの制限は入れてますが、メソッドとヘッダーはザルなので、必要な場合はソースを直してください。

```javascript
'use strict';

const allowMethods = "GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE";
// const originRegex = /^.+/; //すべてのoriginを許可する場合はこちら
const originRegex = /aaaaaaaa\.cloudfront\.net/; //originで制限する場合はこちら

exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    if (request.headers['origin'] && request.method == 'OPTIONS') {
        const origin = request.headers['origin'][0]['value'];
        if (origin.match(originRegex)) {
            const allowHeaders =
                request.headers.hasOwnProperty('access-control-request-headers') ?
                    request.headers['access-control-request-headers'][0]['value'] : "*";
            const response = {
                status: '200',
                headers: {
                    'access-control-allow-origin': [{
                        key: 'access-control-allow-origin',
                        value: origin
                    }],
                    'access-control-allow-methods': [{
                        key: 'access-control-allow-methods',
                        value: allowMethods
                    }],
                    'access-control-allow-headers': [{
                        key: 'access-control-allow-headers',
                        value: allowHeaders
                    }],
                },
                body: '',
            };
            callback(null, response);
            return;
        }
    }
    callback(null, request);
}
```
- `テストイベントの選択`で`テストイベントの設定をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68254/d1c3d614-d9e2-2885-1d26-b5899d00ba0e.png)
- イベント名にテキトウな名前と下記のJSONを入力し、`作成`ボタンをクリック。

```json
{
  "Records": [
    {
      "cf": {
        "config": {
          "distributionId": "EXAMPLE"
        },
        "request": {
          "headers": {
            "host": [
              {
                "key": "Host",
                "value": "d123.cf.net"
              }
            ],
            "user-name": [
              {
                "key": "User-Name",
                "value": "CloudFront"
              }
            ],
            "origin": [
              {
                "key": "origin",
                "value": "https://aaaaaaaa.cloudfront.net"
              }
            ],
            "access-control-request-methods": [
              {
                "key": "access-control-request-methods",
                "value": "POST"
              }
            ],
            "access-control-request-headers": [
              {
                "key": "access-control-request-headers",
                "value": "Authentication"
              }
            ]
          },
          "clientIp": "0000:aaaa::0000:0000",
          "uri": "/test",
          "method": "OPTIONS"
        },
        "response": {
          "status": "200",
          "statusDescription": "OK",
          "headers": {
            "x-cache": [
              {
                "key": "X-Cache",
                "value": "Hello from Cloudfront"
              }
            ]
          }
        }
      }
    }
  ]
}
```

- 作成したテストイベントを選択し、`テスト`ボタンをクリック。
- 実行結果が`成功`になって、`Execution Result`に`access-control-allow-*`が出力されていることを確認。
- `アクション` -> `Lambda@Edgeへのデプロイ`をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68254/fae277f5-98ca-ea99-3663-ebdab7184d2d.png)

- `DistributionのARN`と`キャッシュ動作`と`CloudFrontイベント`を選択し、デプロイをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68254/e007e70f-d928-dbfd-ffae-90d7fef7b665.png)


### origin response用関数作成
- つぎにorigin responseの関数の作成とテストとデプロイをorigin requestを参考に行います。
- コード

```javascript
'use strict';

// const originRegex = /^.+/; //すべてのoriginを許可する場合はこちら
const originRegex = /aaaaaaaa\.cloudfront\.net/; //originで制限する場合はこちら

exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const response = event.Records[0].cf.response;
    if (response.status.match(/^20/) && request.headers['origin']) {
        const origin = request.headers['origin'][0]['value'];
        if (origin.match(originRegex)) {
            response.headers['access-control-allow-origin'] = [{
                key: 'access-control-allow-origin',
                value: origin
            }];
            response.headers['access-control-allow-credentials'] = [{
                key: 'access-control-allow-credentials',
                value: 'true'
            }];
        }
    } 
    callback(null, response);
}
```
- テストイベントのJSON

```json
{
  "Records": [
    {
      "cf": {
        "config": {
          "distributionId": "EXAMPLE"
        },
        "request": {
          "headers": {
            "host": [
              {
                "key": "Host",
                "value": "d123.cf.net"
              }
            ],
            "user-name": [
              {
                "key": "User-Name",
                "value": "CloudFront"
              }
            ],
            "origin": [
              {
                "key": "origin",
                "value": "https://aaaaaaaaa.cloudfront.net/"
              }
            ]
          },
          "clientIp": "0000:aaaa::0000:0000",
          "uri": "/test",
          "method": "GET"
        },
        "response": {
          "status": "200",
          "statusDescription": "OK",
          "headers": {
            "x-cache": [
              {
                "key": "X-Cache",
                "value": "Hello from Cloudfront"
              }
            ]
          }
        }
      }
    }
  ]
}
```

## 動作確認
- CORSの動作確認をします。

### curlで確認
```bash
# preflight request
curl -s -D - 'https://bbbbbbb.cloudfront.net/lambda/api/books/1' \
  -H 'authorization: Basic '`echo -n 'tony_admin@laravel.com:admin'|base64` \
  -H 'origin: https://aaaaaaa.cloudfront.net' \
  -X OPTIONS

HTTP/2 200 
content-length: 0
server: CloudFront
date: Sun, 26 Apr 2020 14:48:16 GMT
access-control-allow-origin: https://aaaaaaa.cloudfront.net
access-control-allow-methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
access-control-allow-headers: *

# actual request
curl -s -D - 'https://bbbbbbb.cloudfront.net/lambda/api/books/1' \
  -H 'authorization: Basic '`echo -n 'tony_admin@laravel.com:admin'|base64` \
  -H 'origin: https://aaaaaaa.cloudfront.net' 

HTTP/2 200 
content-type: application/json
content-length: 198
date: Sun, 26 Apr 2020 14:48:01 GMT
server: Apache/2.4.41 () PHP/7.2.24
x-powered-by: PHP/7.2.24
cache-control: no-cache, private
x-ratelimit-limit: 60
x-ratelimit-remaining: 59
access-control-allow-origin: https://aaaaaaa.cloudfront.net
access-control-allow-credentials: true
vary: Authorization

{"book":{"id":1,"title":"test 2020-04-26 18:02:22","price":"02.22","author":"Foo author","editor":"Foo editor","created_at":"2020-04-23T14:35:32.000000Z","updated_at":"2020-04-26T09:02:23.000000Z"}}
```

### javascriptで確認

```javascript
$.ajax({
    url: "https://bbbbbb.cloudfront.net/lambda/api/books/1",
    dataType: 'json',
    headers: { "Authorization": "Basic " + btoa("tony_admin@laravel.com:admin") },
    success: function (data) {
        console.log('[ok]');
        console.log(data);
    },
    error: function (xhr, status, error) {
        console.log(`[ng] ${error}`);
    }
});
```
 
# 注意点
- Lambda@Edgeを複数のBehaviorに割り当てた場合、デプロイ時に一つのBehaviorしか指定できないため、もう一方のBehaviorは手動でCloudFrontのBehaviorタブからバージョンを上げる必要があり、とても面倒です。
 
```
arn:aws:lambda:us-east-1:000000000000:function:lambda-no-onamae:2   <-- この2を最新の番号に変える
```
# おわりに
こんなことするくらいなら、素直にEC2側でCORS対応したほうがいいです。Frameworkを使ってるならCORS用のライブラリは通常あります。
もしくはAPI Gatewayを加えて、そこのCORS設定を使いましょう。