S3更新時、CloudFrontのキャッシュを削除するLambdaをイベント通知で実行

# はじめに
S3とCloudFrontで静的サイトを構築した状態で、オリジンのS3が更新されたらCloudFrontのキャッシュを削除するLambdaを実装します。
キャッシュの削除は、`全てのキャッシュを削除する`パターンと、`更新したファイルのキャッシュのみを削除`するパターンの２パターンのコードを作成します。

![スクリーンショット 2022-10-02 17.04.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/b670f6b6-e98e-5a72-3bec-a7c11b558dee.png)

# S3を作成
S3を作成します。
設定は以下の通りです。
![スクリーンショット 2022-10-02 16.02.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/df16f6d5-4f44-9714-b59b-c189243884ac.png)

# Cloudfront作成
設定は、以下以外は全てデフォルトです。
- オリジンドメインは、先程作成したS3を選択
- S3 バケットアクセスは、`Origin access control settings (recommended)`を選択
    - コントロール設定を作成する（設定はデフォルトでよい）
- キャッシュポリシーは、開発中は、`CachingDisabled`にしておく
- ビューワープロトコルポリシーは、`Redirect HTTP to HTTPS`
![スクリーンショット 2022-10-02 16.10.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/cc55dcdc-7ec1-63d9-c23f-0a3b339dc815.png)
![スクリーンショット 2022-10-02 16.10.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/8db6b1bf-3136-7daf-569e-000a6b00465d.png)
ディストリビューション作成が成功すると、ポリシーをコピーし、S3のバケット権限に反映させましょう。
![スクリーンショット 2022-10-02 16.13.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/691618e7-5883-373f-8b79-8f3eef7d8485.png)

そして、s3に適当な画像をアップロードし、表示されるか確認しましょう。
`aws-icon.png`
![スクリーンショット 2022-10-02 16.20.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/b309e9c4-4d0c-47cc-807e-394a69bda308.png)
表示されることを確認しました。
![スクリーンショット 2022-10-02 16.18.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/4f202dc7-7aee-3f0c-c530-088fa9ffc93b.png)

# Lambdaを作成
Lambdaを作成します。Python3.9を指定し、他はデフォルトのままにします。
![スクリーンショット 2022-10-02 17.11.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/494e8877-30fb-450e-ca47-80688a875da2.png)

## IAMロール
LambdaのIAMロールに`cloudfront:CreateInvalidation`の権限をリソースを`"arn:aws:cloudfront::xxxxxxxxxxx:distribution/*"`に絞り、権限付与します。

`xxxxxxxxxxx`は、アカウントIDです。
```diff_json:修正前
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-1:xxxxxxxxxxx:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-1:xxxxxxxxxxx:log-group:/aws/lambda/delete-cache-of-cloud_front-in-S3:*"
            ]
        }
    ]
}
```

```diff_json:修正後
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "cloudfront:CreateInvalidation",
                "logs:CreateLogGroup"
            ],
            "Resource": [
                "arn:aws:cloudfront::xxxxxxxxxxx:distribution/*",
                "arn:aws:logs:ap-northeast-1:xxxxxxxxxxx:*"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:ap-northeast-1:xxxxxxxxxxx:log-group:/aws/lambda/delete-cache-of-cloud_front-in-S3:*"
        }
    ]
}
```

## コード
コードの内容ですが、S3の更新頻度によって２パターンに分かれると思います。
1. `S3の更新頻度が多い、または更新時のファイル数が多い`場合
    - →１回の更新で、全キャッシュ削除します。
2. `S3の更新頻度が低い、または更新時のファイル数が少ない`場合
    - → 更新ファイルごとに、そのパスのみのキャッシュを削除します

1.のパターンでは、パスごとに削除すると、Lambdaの実行回数が多く、同時実行数の上限に達したり、料金が高くなることがあります。
2.のパターンでは、パスごとに削除しても1.のデメリットがありません。また、削除する必要のないキャッシュは残るメリットがあります。

1.のパターンのコードはこちらです。
環境変数には、CloudFrontのディストリビューションIDを`DISTRIBUTION_ID`として設定しています。
```python
from __future__ import print_function
import boto3
import time
import os
import json

DISTRIBUTION_ID = os.environ['DISTRIBUTION_ID']

def decimal_to_int(obj):
    if isinstance(obj, Decimal):
        return int(obj)

def lambda_handler(event, context):
    print("Received event:" + json.dumps(event, default=decimal_to_int, ensure_ascii=False))
    
    client = boto3.client('cloudfront')
    invalidation = client.create_invalidation(DistributionId=DISTRIBUTION_ID,
        InvalidationBatch={
            'Paths': {
                'Quantity': 1,
                'Items': ['/*']
            },
        'CallerReference': str(time.time())
    })
```
開発中は、ログを残すため、`print`を使っていますが、本番時は削除してかまいません。

2.のパターンのコードはこちらです。
```python
from __future__ import print_function
import boto3
import time
import os
import json

DISTRIBUTION_ID = os.environ['DISTRIBUTION_ID']

def decimal_to_int(obj):
    if isinstance(obj, Decimal):
        return int(obj)

def lambda_handler(event, context):
    print("Received event:" + json.dumps(event, default=decimal_to_int, ensure_ascii=False))
    
    for items in event["Records"]:
        path = "/" + items["s3"]["object"]["key"]
        print("path:" + path)
        client = boto3.client('cloudfront')
        invalidation = client.create_invalidation(DistributionId=DISTRIBUTION_ID,
            InvalidationBatch={
                'Paths': {
                    'Quantity': 1,
                    'Items': [path]
            },
            'CallerReference': str(time.time())
        })
```


# S3のイベント通知でLambdaを指定
あとは、Lambdaのコンソール画面から`トリガーを追加`をクリックし、S3のイベント通知を設定します。
オプションの`Suffix`は、イベント通知する拡張子を指定できます。
![スクリーンショット 2022-10-02 17.43.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/084a8ac9-bead-9f6e-e803-0455b6da0cc1.png)

これで、S3にファイルをアップロードしたり、削除すると、Lambdaが起動し、cloudfrontのキャッシュを削除します。