Home

Published

- 7 min read

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

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

これは何?

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

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

構成

以下の構成でWebサイトを公開しています。 20240317_165731.jpg

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

実施内容

以下の順序で作業を進めていきます。

  1. IAMポリシーの作成
  2. IAMロールの作成
  3. Lambdaの作成
  4. CodePipelineの修正

IAMポリシーの作成

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

20240316_172749.png

20240316_172905.png

以下の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

IAMロールの作成

作成したIAMポリシーを利用してロールを作成します。 今度はIAMロールの作成画面に移動し、以下のように進めていきます。 20240316_174530.png 20240316_174632.png

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

Lambdaの作成

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

Lambda関数の作成画面に移動し、以下のように進めます。 20240316_205837.jpg 20240316_205858.jpg 20240316_210012.jpg

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

※コードの実装内容についてはこちらの記事を参考にしました。 (参考記事内のコードでは、Lambda→SNSへ通知連携処理が入っていますが、今回はAWS ChatBotを利用して通知を実装しているため、その処理は削除しています。)

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

CodePipelineの修正

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

パイプラインの編集画面を開き、以下のように進めていきます。 20240316_175708.png 20240316_175755.png 20240316_175846.png

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

ここで、呼び出すLambda関数とそのLambda関数に引数として渡す入力パラメーターを定義します。 20240316_180617.png

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

{
	"PipelineName": "ここにパイプライン名を入力",
	"DistributionId": "ここにディストリビューションIDを入力"
}

設定する内容は以上です。

動作確認

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

まずはCodeCommitでプルリクエストを作成し、ブランチをマージします。 (テストファイルを作成してマージします。) 20240316_202115.png

マージに成功すると、パイプラインが起動し、処理が走り始めます。 20240316_203858.jpg 20240316_203912.jpg 20240316_203027.jpg 最後のステップが2024/3/16 PM8:26(JTC)に完了していることがわかりますね。

CloudFront側のキャッシュ削除の履歴も確認します。 20240316_202750.jpg こちらも2024/3/16 PM8:25(JTC)にキャッシュが削除されており、パイプラインの終了とほぼ同じ時刻ですので、正常に実行されていることが確認できました!

まとめ

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

参考リンク

CodePipelineからAWS Lambdaを呼び出してCloudFrontのキャッシュを削除(Invalidation)してみた