【AWS】CloudFront FunctionsとLambda@Edgeの比較と実践

# それぞれの概要と違い

CloudFront FunctionsとLambda@Edgeの概要について、以下に要点を表形式でまとめました。

※その他の違いや詳細については、AWSドキュメント「[CloudFront Functions と Lambda@Edge の違い](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/edge-functions-choosing.html)」をご参照ください。

| 項目	| CloudFront Functions	| Lambda@Edge|
| ----- | ------- | ------ |
| 概要	| 簡単な処理、低レイテンシ要求の処理| 複雑な処理、大規模なトラフィック、長時間実行の処理|
| プログラミング言語	| JavaScript (ECMAScript 5.1 準拠)	| Node.js と Python |
| 関数の持続時間 | サブミリ秒 | 最大 5 秒 (Viewer request、Viewer response)<br/>最大 30 秒 (Origin request、Origin response) 
| スケール量　| 毎秒 1000 万 件	| 1 リージョンあたり毎秒 1万 件まで |
| 対応するCloudFront イベント	| Viewer request<br/>Viewer response | Viewer request<br/>Viewer response<br/>Origin request<br/>Origin response |

### Viewer Request/ResponseとOrigin Request/Responseについて

- Viewer Request
    - クライアント（ユーザー）が CloudFront にリクエストを送信した直後に実行される
- Viewer Response
    - CloudFront がクライアントにレスポンスを返す直前に実行される
- Origin Request
    - CloudFront がオリジンサーバー（例: S3やEC2）にリクエストを送信する直前に実行される
- Origin Response
    - オリジンサーバーから CloudFront にレスポンスが戻ってきた直後に実行される

Viewer Request/Response と Origin Request/Response のトリガーを使い分けることで、CloudFront 関数や Lambda@Edge を適切に活用し、キャッシュの前後でコンテンツを効率的に操作できます。

<img src="https://d2908q01vomqb2.cloudfront.net/da4b9237bacccdf19c0760cab7aec4a8359010b0/2021/04/08/cloudfront-function-and-lambda-edge-2-1024x454.png">

CloudFront Functions で利用できない機能が必要な場合でも、Lambda@Edge を使ってキャッシュの前後でコンテンツを操作できます。

<img src="https://d2908q01vomqb2.cloudfront.net/da4b9237bacccdf19c0760cab7aec4a8359010b0/2021/04/08/cloudfront-functions-only-lambda-egde-1024x413.png">


参考）[Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale](https://aws.amazon.com/jp/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/)




# 料金比較

| 項目	| CloudFront Functions	| Lambda@Edge|
| ----- | ------- | ------ |
| リクエスト料金 | 100 万回あたり 0.1 USD | 100万件あたり USD 0.60 |
| コンピューティング料金 | なし | あり（128 MB-秒の使用につき 0.00000625125 USD） |
| 無料枠	| あり（1 か月あたり 200 万件の CloudFront 関数呼び出し） | なし |

※AWSドキュメント「[Amazon CloudFront の料金](https://aws.amazon.com/jp/cloudfront/pricing/)」
※AWSドキュメント[「AWS Lambda 料金」](https://aws.amazon.com/jp/lambda/pricing/)

### 料金の例

 1か月に100万回リクエストされ、1か月のコンピューティング時間が合計100万 秒（毎回の実行時間が 100 ミリ秒）であった場合、発生する料金は以下のように計算されます。

| 項目	| CloudFront Functions	| Lambda@Edge|
| ----- | ------- | ------ |
| 1 か月のコンピューティング料金 | なし | 1000,000 × 0.00000625125 USD <br/>= 6.3 USD |
| 1 か月のリクエスト料金 | 100 万回あたり 0.1 USD | 100万件あたり USD 0.60 |
| 無料枠	| あり（1 か月あたり 200 万件） | なし |
| 合計	| 0 USD（※無料枠が適用された場合）  | 6.9 USD |


# CloudFront FunctionでBasic認証＋IP制限の実装

私がCloudFront Functionを触ったことがなく、CloudFront Functionでどの程度の処理なら出来るかのイメージが分からないので、とりあえずCloudFront Functionを使ってBasic認証＋IP制限の実装をやってみようと思います！
- 全体の流れ
    - ステップ１： S3 作成
    - ステップ２： CloudFront 作成
    - ステップ３： S3 バケットポリシー編集
    - ステップ４： （練習）リダイレクト用のCloudFront Functions作成)
    - ステップ５： CloudFront FunctionでBasic認証＋IP制限を実装



## ステップ１：　S3作成


- 今回オリジンをS3にするため、S3を作成します。

作成後、以下のフォルダ構成でフォルダ・HTMLファイルを作成し、S3バケットへのアップロードをします。

フォルダ構成はこちらになります。
```
/
  └── index.html
  └── management/
      └── index.html
```



```index.html
<!DOCTYPE html>
<html lang="ja">
<h1>Welcome</h1>
</html>
```

```management/index.html
<!DOCTYPE html>
<html lang="ja">
<h1>Welcome manager</h1>
</html>
```




## ステップ２： 　CloudFront 作成

- 続いて、CloudFrontを作成します。
- 作成する際にいくつかの項目を設定します。
    - Origin domain：ステップ１で作成したS3を選択
    - オリジンへのアクセス制御方法：OACを使用
        - Create new OACをクリックして、任意の名前で作成可能
    - 今回は検証のため、ウェブアプリケーションファイアウォール (WAF)は有効にせず作成


![スクリーンショット 2024-11-18 9.35.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/ad963ed1-a05e-0911-a802-e8573bc829e7.png)


![スクリーンショット 2024-11-18 9.35.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/5039f240-d51f-6d95-2e5f-36cf206d3b19.png)

- CloudFront 作成後にAWSマネジメントコンソール上部に以下の通知が来るので、「ポリシーをコピー」をクリックお願いします。ステップ３の「S3バケットポリシー編集」で使用します。


![スクリーンショット 2024-11-18 9.40.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/0efd32e4-e3f5-8340-122f-04cc6278d229.png)

## ステップ３：　S3バケットポリシー編集

- ステップ２の最後にコピーしたポリシーをステップ１で作成したS3のバケットポリシーに設定します。

例：
```json
{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{S3リソース名}/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "{CloudFrontのArn}"
                }
            }
        }
    ]
}
```


## 一度アクセスできるか確認

CloudFrontのコンソールにて、ステップ２で作成したCloudFrontのディストリビューションドメイン名をコピーしてアクセスしてみます。
そのドメインに`/index.html`を足してアクセスし、「Welcome」と表示されればOKです！

例：https://xxxxxxxxxxx.cloudfront.net/index.html

![スクリーンショット 2024-11-18 10.02.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/4ae9ce73-8f88-7d59-cd39-ead051e440ab.png)

![スクリーンショット 2024-11-19 8.53.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/d5747e45-841d-2690-7f90-974b697e2a9f.png)



## 練習：リダイレクト用のCloudFront Functions作成

ここで一度練習を入れました！必要ない方は飛ばしていただいても大丈夫です！

`https://xxxxxxxxxxx.cloudfront.net`にアクセスした際に、`https://xxxxxxxxxxx.cloudfront.net/index.html`にリダイレクトされるように設定します。

- CloudFrontのコンソールにて、ビヘイビアを作成します。
    - パスパターンを`/`
    - オリジンに作成したS3を設定

![スクリーンショット 2024-11-18 10.16.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/e433f9c3-b4d8-699d-5673-8ea1bf054450.png)

- 作成後の設定画面がこちらになります。

![スクリーンショット 2024-11-18 10.18.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/e4356799-d13a-3be7-88b7-660a8b0668f1.png)

- 続きまして、CloudFrontのコンソールのサイドメニューから関数をクリックし、CloudFront Functionsを作成します。

![スクリーンショット 2024-11-18 10.20.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/12d312cb-868c-96e5-54b1-1814d62730b2.png)

- 続いて実行するコードを記載し保存します。

```python
function handler(event) {
    var request = event.request;
    var uri = request.uri;

    // リクエストパスが `/` の場合に `index.html` を追加
    if (uri === "/") {
        request.uri += "index.html";
    }

    return request;
}
```

![スクリーンショット 2024-11-18 10.23.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/f422b723-3483-f48f-7dfe-269ae3746f9c.png)

- 続いて実行するコードをテストをするために、テスト画面にて項目のURLパスを`/`にして関数をテストします。

![スクリーンショット 2024-11-18 10.25.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/2a58b4c4-d236-2757-1b99-70f3b97ebd41.png)

- このように成功すればOKです！

![スクリーンショット 2024-11-18 10.27.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/b08a2286-4bdf-3944-98d4-5276b858dd19.png)


実行結果に記載されているコンピューティング使用率が50以上であれば、関数コードの最適化するか、Lambda@Edgeへの移行が必要になります。

:::note info
- コンピューティング使用率の値:
  - 1 ～ 50 — この機能は最大許容時間を快適に下回っており、スロットリングなしで実行されます。
  - 51 ～ 70 — 関数は最大許容時間に近づいています。関数コードを最適化することを検討してください。
  - 71 ～ 100 — 関数が最大許容時間に非常に近いか、それを超えています。ディストリビューションに関連付けた場合、CloudFront は、この関数をスロットリングする可能性があります。

参考）AWSコンソール画面の説明文より抜粋
:::


- テストを実行した後、「関数を発行」をクリックすると、ディストリビューションに関連付けの設定画面が表示されるので、作成したディストリビューションのキャッシュビヘイビア`/`を関連付けることができます。

![スクリーンショット 2024-11-18 10.36.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/1d86a27f-1105-2ed4-c9d8-2d68d45eedd6.png)

- クライアント（ユーザー）が CloudFront にリクエストを送信した直後に実行されるようにしたいので、イベントタイプでViewer Requestを選択しています。

![スクリーンショット 2024-11-20 15.53.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/c37fa5d6-c387-fbba-7ff2-1da3ba880855.png)

- 関連付けが完了したのち、`https://xxxxxxxxxxx.cloudfront.net`にアクセスし、`/index.html`の内容が表示されていればOKです！

![スクリーンショット 2024-11-18 10.43.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/f3600a89-70e7-56be-855e-478bffe19802.png)


## ステップ４： CloudFront FunctionでBasic認証＋IP制限を実装

それでは、先ほど実施していただいた手順と同じように、CloudFront FunctionでBasic認証＋IP制限を実装していきます。
`https://xxxxxxxxxxx.cloudfront.net/management/index.html`へアクセスする際に、指定したIPアドレスからのアクセスのみを許可し、さらにBasic認証を要求する実装になります。

- CloudFrontのコンソールにて、ビヘイビアを作成します。
    - パスパターンを`/management/*`
    - オリジンに作成したS3を設定


![スクリーンショット 2024-11-19 16.48.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/5883ff13-2b6c-ae0b-0934-608809ae85bf.png)

- 作成後の設定画面がこちらになります。

![スクリーンショット 2024-11-19 16.49.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/aa23f7c1-84fa-6259-b5b7-cdf28232aae4.png)

- 再度新規のCloudFunctionsを作成し、以下のコードを記載し保存します。

- 以下の三つの部分を変更してコードを使用してください。
    - XX.XX.XX.XX　 ：あなたの利用しているIPアドレス
    - yourUsername　：任意のユーザー名
    - yourPassword　：任意のパスワード


```python
function handler(event) {
    var request = event.request;
    var headers = request.headers;
    var clientIP = event.viewer.ip;

    console.log('Client IP:'+clientIP);

    // アクセス許可するIPリスト
    var IP_WHITE_LIST = {
        'XX.XX.XX.XX': true, // 要変更
    };

    // Basic認証のユーザー名とパスワード
    var credentials = {
        username: "yourUsername", // 要変更
        password: "yourPassword", // 要変更
    };

    // Basic認証のためにユーザー名とパスワードを Base64 でエンコード
    var encodedAuth = Buffer.from(credentials.username + ':' + credentials.password).toString('base64');
    var expectedAuthHeader = 'Basic ' + encodedAuth;

    // クライアントIPが許可されているかを確認
    var isPermittedIp = IP_WHITE_LIST[clientIP] || false;
    console.log(isPermittedIp);

    if (isPermittedIp) {
        // Authorizationヘッダーの有無と認証情報をチェック
        if (headers.authorization && headers.authorization.value === expectedAuthHeader) {
            console.log("Authorization successful for IP:"+clientIP);
            return request;  // 認証成功
        } else {
            console.log("Authorization failed for IP:"+clientIP);
            // 認証失敗時に401レスポンスを返す
            return {
                statusCode: 401,
                statusDescription: 'Unauthorized',
                headers: {
                    'www-authenticate': { value: 'Basic realm="Enter credentials"' },
                },
            };
        }
    } else {
        // 許可されていないIPの場合403レスポンスを返す
        console.log("Forbidden access for IP:"+clientIP);
        return {
            statusCode: 403,
            statusDescription: 'Forbidden',
        };
    }
}

```

- 想定外のIPアドレスからのアクセスが禁止されていることを確認するために、テスト画面にて以下の設定を行い、実行します。
    - URLパス：`/management/index.html`
    - IPアドレス：1.2.3.4

![スクリーンショット 2024-11-20 15.18.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/ab1b27ef-0d99-cf85-2fe4-003afc9f6c19.png)

- テストを実行した結果、ステータスが`403 Forbidden`となっていればOKです。この結果により、IPアドレス`1.2.3.4`からのアクセスが適切に禁止されていることが確認できます。

![スクリーンショット 2024-11-20 15.19.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/711ec933-e724-95f8-57ef-21a80783e43c.png)

- 次に、CloudFront Functionsに登録されているIPアドレスからのアクセスが許可されているかを確認するため、テスト画面にて以下の設定を行い、実行します。
    - URLパス：`/management/index.html`
    - IPアドレス：あなたの利用しているIPアドレス

![スクリーンショット 2024-11-20 15.19.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/2f5d1024-c37e-e570-22a6-7205dfd0dadd.png)

- テストを実行した結果、ステータスが`401 Unauthorized`となっていればOKです！`401 Unauthorized`エラーは、IPアドレスからのアクセスが許可された後に、Basic認証においてエラーとなっているだけです。つまり、このテスト結果から、ご利用のIPアドレスからのアクセスが許可されていることが確認できます。

![スクリーンショット 2024-11-20 15.21.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/e63a7d79-33ff-b157-7c51-9146e2125a05.png)


- テストを実行した後、「関数を発行」をクリックすると、ディストリビューションに関連付けの設定画面が表示されるので、作成したディストリビューションのキャッシュビヘイビア`/manager/*`を関連付けることができます。

![スクリーンショット 2024-11-20 15.30.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/ebf4271f-7ded-d400-bd10-b99eafd7bd5f.png)


- 関連付けたあと、アクセスするとBasic認証を求められます。

![スクリーンショット 2024-11-21 17.18.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/b6411f86-877d-d0a0-8e87-7484f5dd68ca.png)

-  ユーザー名とパスワードを入力し、「Welcome manager」と表示されればOKです！

![スクリーンショット 2024-11-21 9.57.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/b4423902-f084-26e6-cf56-9b8c3becacb2.png)


# まとめ

CloudFront FunctionsとLambda@Edgeの比較と実践を行いました！
これまでLambda@Edgeを利用して実装していたものをCloudFront Functionsに移行することも検討してみると良いかもしれません。
CloudFront Functionsの導入を検討されている方々にとって、少しでも参考になれば幸いです！