CodePipelineの最後にCloudFrontのキャッシュを自動で削除する

# これは何？
S3の静的サイトホスティングとCodePipelineを組み合わせてWebサイトを公開しているのですが、今まではCloudFrontのキャッシュ削除を手動で実施していました。

今回、そのキャッシュ削除を自動化したのでその際に実施したことをまとめた記事です。

# 構成
以下の構成でWebサイトを公開しています。
![20240317_165731.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/2718f394-2ef8-5f42-4cad-2a0535ab7ab4.jpeg)

- S3の静的サイトホスティングを利用
- CMSは導入しておらず、ソースコードはCodeCommitで管理
- CodeCommitでブランチをマージするとパイプライン処理が起動し、S3へのデプロイが実行される
- S3へのデプロイ完了後、LambdaがCloudFrontのキャッシュを削除しに行く
    - ↑赤枠の部分であり、今回の実装内容です
- パイプライン処理の開始/終了時にはAWS Chatbotを通してSlackへ通知される

# 実施内容
以下の順序で作業を進めていきます。
1. IAMポリシーの作成
1. IAMロールの作成
1. Lambdaの作成
1. CodePipelineの修正

## IAMポリシーの作成
まずはLambdaにアタッチするロールに設定するためのIAMポリシーを作成します。
IAMポリシーの作成画面に移動して、以下のように進めていきます。

![20240316_172749.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/7b16bc5f-9bad-2ff2-652e-f22b90a8d0b4.png)

![20240316_172905.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/436f5844-cd6b-4466-8cf8-523d0efc7950.png)

以下のjsonをペーストします。
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:GetDistribution",
                "cloudfront:GetDistributionConfig",
                "cloudfront:ListDistributions",
                "cloudfront:ListStreamingDistributions",
                "cloudfront:CreateInvalidation",
                "cloudfront:ListInvalidations",
                "cloudfront:GetInvalidation",
                "codepipeline:AcknowledgeJob",
                "codepipeline:GetJobDetails",
                "codepipeline:PollForJobs",
                "codepipeline:PutJobFailureResult",
                "codepipeline:PutJobSuccessResult",
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```
![20240316_173043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/cdd34af5-60cd-a660-c6c1-fef8613d3cac.png)

## IAMロールの作成
作成したIAMポリシーを利用してロールを作成します。
今度はIAMロールの作成画面に移動し、以下のように進めていきます。
![20240316_174530.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/dd9173cc-2589-b4a4-648e-f8b125c9dcec.png)
![20240316_174632.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/fe559851-7bcb-5c78-2bac-4c24ad4c62be.png)

以上でLambdaにアタッチするロールの作成が完了しました。

## Lambdaの作成
続いて、CloudFrontのキャッシュを削除するためのLambda関数を作成します。
このLambda関数はCodePipelineから呼び出され、処理が正常に完了すると、その情報をCodePipeline側に返却します。

Lambda関数の作成画面に移動し、以下のように進めます。
![20240316_205837.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/0aa97022-9598-0519-d15c-be49b546e38d.jpeg)
![20240316_205858.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/cb9ecd45-2f8f-b233-240e-7f5f165f5949.jpeg)
![20240316_210012.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/81c1fe95-b26d-d690-dcd2-ae96233f676c.jpeg)

コードは、以下のPythonコードを貼り付けます。

※コードの実装内容については[こちら](https://dev.classmethod.jp/articles/cloudfront-invalidation-using-codepipeline-custom-action/)の記事を参考にしました。
(参考記事内のコードでは、Lambda→SNSへ通知連携処理が入っていますが、今回はAWS ChatBotを利用して通知を実装しているため、その処理は削除しています。)
```python
import boto3
import json
import logging
import time
import traceback

logger = logging.getLogger()
logger.setLevel(logging.INFO)

cp = boto3.client('codepipeline')
cf = boto3.client('cloudfront')

def create_invalidation(distribution_id):
    logger.info('Creating invalidation')
    res = cf.create_invalidation(
        DistributionId=distribution_id,
        InvalidationBatch={
        'Paths': {
            'Quantity': 1,
            #削除するキャッシュのパスを指定
            'Items': ['/*'],
        },
        'CallerReference': str(time.time())
        }
    )

    invalidation_id = res['Invalidation']['Id']
    logger.info('InvalidationId is %s', invalidation_id)
    return invalidation_id

def monitor_invalidation_state(distribution_id, invalidation_id):
    res = cf.get_invalidation(
        DistributionId=distribution_id,
        Id=invalidation_id
    )

    return res['Invalidation']['Status']

def put_job_success(job_id):
    logger.info('Putting job success')
    cp.put_job_success_result(jobId=job_id)

def continue_job_later(job_id, invalidation_id):
    continuation_token = json.dumps({'InvalidationId':invalidation_id})
    logger.info('Putting job continuation')

    cp.put_job_success_result(
        jobId=job_id,
        continuationToken=continuation_token
    )

def put_job_failure(job_id, err):
    logger.error('Putting job failed')
    message = 'Function exception: ' + str(err)
    cp.put_job_failure_result(
        jobId=job_id,
        failureDetails={
            'type': 'JobFailed',
            'message': message
        }
    )

def lambda_handler(event, context):
    try:
        job_id = event['CodePipeline.job']['id']
        job_data = event['CodePipeline.job']['data']

        user_parameters = json.loads(
            job_data['actionConfiguration']['configuration']['UserParameters']
        )

        pipeline_name = user_parameters['PipelineName']
        distribution_id = user_parameters['DistributionId']

        if 'continuationToken' in job_data:
            continuation_token = json.loads(job_data['continuationToken'])
            invalidation_id = continuation_token['InvalidationId']
            logger.info('InvalidationId is %s', invalidation_id)
            status = monitor_invalidation_state(distribution_id, invalidation_id)
            logger.info('Invalidation status is %s', status)
            if not status == 'Completed':
                continue_job_later(job_id, invalidation_id)
            else:
                put_job_success(job_id)
        else:
            invalidation_id = create_invalidation(distribution_id)
            continue_job_later(job_id, invalidation_id)
    except Exception as err:
        logger.error('Function exception: %s', err)
        traceback.print_exc()
        put_job_failure(job_id, err)

    logger.info('Function complete')
    return "Complete."
```


:::note warn
コードを入力した後は必ず`デプロイ`してください。変更が反映されません。
:::

デフォルト値では短すぎるので、Lambda関数のタイムアウト値を5分に変更しておきます。
(参考までに、私の環境ではだいたい2~3分で関数の実行は完了しています。)
![20240317_160508.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/cb4766e4-01fd-524b-877c-cc4978c456d6.jpeg)


## CodePipelineの修正
今回はすでに動作しているCodePipelineの設定を変更します。
既存のDeployステージの後に、キャッシュ削除用のステージを作成します。

パイプラインの編集画面を開き、以下のように進めていきます。
![20240316_175708.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/03187fcc-68cd-6f57-a9a4-6e137ba69db9.png)
![20240316_175755.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/9a6eae87-d9f6-70f7-0fd5-1c766e3ddf2b.png)
![20240316_175846.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/69aa2304-5b57-3f9e-bdcc-8631289d072f.png)

実行するアクションの詳細を設定します。

ここで、呼び出すLambda関数とそのLambda関数に引数として渡す入力パラメーターを定義します。
![20240316_180617.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/369cf345-ff6e-43f6-693f-a0df4579ac2e.png)

入力パラメータには、`CodePipelineのパイプライン名`と`キャッシュを削除するCloudFrontのディストリビューションID`をjson形式で渡します。

```json
{"PipelineName": "ここにパイプライン名を入力","DistributionId": "ここにディストリビューションIDを入力"}
```
設定する内容は以上です。

# 動作確認
今回は、動作確認として実際に適当なコミットを作成し、ブランチをマージしてパイプラインの処理を実行させます。
パイプライン実行時に、CloudFrontのキャッシュが削除されていれば処理成功です。

まずはCodeCommitでプルリクエストを作成し、ブランチをマージします。
(テストファイルを作成してマージします。)
![20240316_202115.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/fe4fd7a7-3db4-d22e-9026-28976035110a.png)

マージに成功すると、パイプラインが起動し、処理が走り始めます。
![20240316_203858.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/2ef71239-6ced-f2da-a339-0a1ae9768bf4.jpeg)
![20240316_203912.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/11ed1ba5-d830-2535-63f8-2b0a85cb8d77.jpeg)
![20240316_203027.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/8b22791a-6e61-0de0-ab65-90997c10d9ee.jpeg)
最後のステップが2024/3/16 PM8:26(JTC)に完了していることがわかりますね。

CloudFront側のキャッシュ削除の履歴も確認します。
![20240316_202750.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601535/0b6b0411-42a8-fcfa-5cc0-2206b45ddd46.jpeg)
こちらも2024/3/16 PM8:25(JTC)にキャッシュが削除されており、パイプラインの終了とほぼ同じ時刻ですので、正常に実行されていることが確認できました！

# まとめ
無事にCodePipelineの最後にCloudFrontのキャッシュを自動で削除できるようになりました。
デプロイ作業が更にラクになって嬉しいです！

※もっといい方法や内容の誤りがあった場合はコメントいただけると幸いです！

# 参考リンク
[CodePipelineからAWS Lambdaを呼び出してCloudFrontのキャッシュを削除(Invalidation)してみた](https://dev.classmethod.jp/articles/cloudfront-invalidation-using-codepipeline-custom-action/)