この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、佐伯です。
CodePipelineからAWS Lambdaを呼び出してCloudFrontのキャッシュ削除(Invalidation)、キャッシュ削除のステータス確認、SNSへ通知までを行うLambda Functionを作ってみました。
CodePipelineからAWS Lambdaを呼び出すときどういう感じで作ればいいか?というのが本エントリの主旨です。
やってみた
書くこと
CodePipelineのアクション設定、CodePipelineから呼び出すLambda Functionのコードについて書きます。その他リソースの作成・設定手順やSlack通知のコードもググればいっぱい出てくると思うので省略させて頂きます!
構成
以下のようなS3にファイルを配置しCloudFrontで静的サイトを配信しているケースを想定してみました。
パイプライン処理は、CodeCommitにPushしたソースをCodePipelineでS3にデプロイ後、AWS Lambdaを呼び出しLambda FunctionでInvalidatioinとInvalidationのステータスチェックを行い、Invalidationが完了したらSNSへPublishする感じです。
CodePipelineからAWS Lambda呼び出し時の動作
CodePipelineからAWS Lambdaを呼び出す場合、CodePipelineはAWS Lambdaへジョブリクエストを送信します。Lambda Functionはジョブを実行し、実行結果をCodePipelineへ送信してジョブのステータスを知らせます。実行結果に継続トークンを含めることでジョブが完了していないことをCodePipelineへ知らせることができ、未完了のジョブはCodePipelineによって再実行される、といった仕組みのようです。
参考リンク: CodePipeline でのカスタムアクションの作成と追加 - CodePipeline
AWS Lambda の呼び出しアクションの制限事項
継続トークンを実行結果に含めることでCodePipelineがLambda Functionを再実行してくれますが、AWS Lambdaの呼び出しアクションのタイムアウトは1時間となっていますので、1時間以上かかる処理はAWS Lambda以外の方法で実現する必要があります。
参考リンク: AWS CodePipeline 制限 - CodePipeline
CodePipelineのアクション設定
CodePipelineのDeployステージの後にInvalidationステージを追加し、アクションを作成します。設定は以下の通りです。
ユーザーパラメーターにパイプライン名、CloudFrontディストリビューションID、SNSトピックARNの情報をJSONで設定しています。このパラメーターはCodePipelineからLambda Functionが呼び出される際にイベントとして渡され、Lambda Functionで参照することができます。以下のJSONは見やすいように改行していますが、実際の設定は改行を含めずに設定しています。
{
"PipelineName": "example-pipeline",
"DistributionId": "E1EVDTCEXAMPLE",
"SnsTopicArn": "arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:example-topic"
}
Lambda Functionのサンプルコード
本題のCodePipelineから呼び出すLambda Functionのコードを書いてみました。ランタイムはPython3.7で動作することを確認しています。
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')
sns = boto3.client('sns')
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 sns_publish(sns_topic_arn, pipeline_name, job_id, job_status):
logger.info('Publish to SNS topic')
message = 'PipelineName: ' + pipeline_name + '\n'
message += 'JobId: ' + job_id + '\n'
message += 'Status: ' + job_status + '\n'
res = sns.publish(
TopicArn=sns_topic_arn,
Message=message
)
messaeg_id = res['MessageId']
logger.info('SNS Messaeg ID is %s', messaeg_id)
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']
sns_topic_arn = user_parameters['SnsTopicArn']
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:
sns_publish(sns_topic_arn, pipeline_name, job_id, job_status='success')
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()
sns_publish(sns_topic_arn, pipeline_name, job_id, job_status='failed')
put_job_failure(job_id, err)
logger.info('Function complete')
return "Complete."
以下AWSドキュメントにもサンプルコードがあり、こちらも参考になると思います。
参考リンク: CodePipeline で パイプラインに AWS Lambda 関数を呼び出す - CodePipeline
サンプルコードの補足
かなりざっくりですがLambda Functionの処理は以下のような流れになります。また、サンプルコードではInvalidationのPathは/*
に固定し、全キャッシュを削除する形にしていますので、参考にされる際はご注意ください。
- イベントからパイプライン名、CloudFrontディストリビューションID、SNSトピックARNを取得
- CloudFrontのキャッシュを削除を実行
- CodePipelineへ継続トークンを含めたジョブ実行結果を送信
- CodePipelineがLambda Functionを再実行
- イベントにcontinuationTokenが含まれている場合はCloudFrontのキャッシュ削除のステータスを確認
- キャッシュ削除が完了していない場合: CodePipelineへ継続トークンを含めた実行結果を送信
- キャッシュ削除が完了している場合: SNSトピックへメッセージをPublish
- キャッシュ削除が完了している場合: CodePipelineへ継続トークンなしで実行結果を送信
実行
適当にファイルを更新してパイプラインを実行した際のLambda Functionのログが以下となります。(各種IDなどは一部変更しています)
START RequestId: ab7c19c4-0552-4d9d-993c-f5deeexample Version: $LATEST
[INFO] 2019-03-06T05:25:08.878Z ab7c19c4-0552-4d9d-993c-f5deeexample Creating invalidation
[INFO] 2019-03-06T05:25:09.908Z ab7c19c4-0552-4d9d-993c-f5deeexample InvalidationId is I22E4C6EXAMPLE
[INFO] 2019-03-06T05:25:09.909Z ab7c19c4-0552-4d9d-993c-f5deeexample Putting job continuation
[INFO] 2019-03-06T05:25:10.171Z ab7c19c4-0552-4d9d-993c-f5deeexample Function complete
END RequestId: ab7c19c4-0552-4d9d-993c-f5deeexample
REPORT RequestId: ab7c19c4-0552-4d9d-993c-f5deeexample Duration: 1330.52 ms Billed Duration: 1400 ms Memory Size: 128 MB Max Memory Used: 85 MB
START RequestId: 5ff1fc7d-6b2d-444a-ba06-b9788example Version: $LATEST
[INFO] 2019-03-06T05:25:40.610Z 5ff1fc7d-6b2d-444a-ba06-b9788example InvalidationId is I22E4C6EXAMPLE
[INFO] 2019-03-06T05:25:41.593Z 5ff1fc7d-6b2d-444a-ba06-b9788example Invalidation status is InProgress
[INFO] 2019-03-06T05:25:41.593Z 5ff1fc7d-6b2d-444a-ba06-b9788example Putting job continuation
[INFO] 2019-03-06T05:25:41.721Z 5ff1fc7d-6b2d-444a-ba06-b9788example Function complete
END RequestId: 5ff1fc7d-6b2d-444a-ba06-b9788example
REPORT RequestId: 5ff1fc7d-6b2d-444a-ba06-b9788example Duration: 1118.73 ms Billed Duration: 1200 ms Memory Size: 128 MB Max Memory Used: 86 MB
START RequestId: 91bfb2a8-ae58-42eb-ab0c-4bc16example Version: $LATEST
[INFO] 2019-03-06T05:26:13.941Z 91bfb2a8-ae58-42eb-ab0c-4bc16example InvalidationId is I22E4C6EXAMPLE
[INFO] 2019-03-06T05:26:14.708Z 91bfb2a8-ae58-42eb-ab0c-4bc16example Invalidation status is InProgress
[INFO] 2019-03-06T05:26:14.708Z 91bfb2a8-ae58-42eb-ab0c-4bc16example Putting job continuation
[INFO] 2019-03-06T05:26:14.807Z 91bfb2a8-ae58-42eb-ab0c-4bc16example Function complete
END RequestId: 91bfb2a8-ae58-42eb-ab0c-4bc16example
REPORT RequestId: 91bfb2a8-ae58-42eb-ab0c-4bc16example Duration: 887.00 ms Billed Duration: 900 ms Memory Size: 128 MB Max Memory Used: 86 MB
START RequestId: 9ae598c1-66b7-4df9-a14d-4c623example Version: $LATEST
[INFO] 2019-03-06T05:26:45.168Z 9ae598c1-66b7-4df9-a14d-4c623example InvalidationId is I22E4C6EXAMPLE
[INFO] 2019-03-06T05:26:46.92Z 9ae598c1-66b7-4df9-a14d-4c623example Invalidation status is Completed
[INFO] 2019-03-06T05:26:46.92Z 9ae598c1-66b7-4df9-a14d-4c623example Publish to SNS topic
[INFO] 2019-03-06T05:26:46.301Z 9ae598c1-66b7-4df9-a14d-4c623example SNS Messaeg ID is d43d48e4-5d66-54ac-9e1c-1c1d0example
[INFO] 2019-03-06T05:26:46.301Z 9ae598c1-66b7-4df9-a14d-4c623example Putting job success
[INFO] 2019-03-06T05:26:46.428Z 9ae598c1-66b7-4df9-a14d-4c623example Function complete
END RequestId: 9ae598c1-66b7-4df9-a14d-4c623example
REPORT RequestId: 9ae598c1-66b7-4df9-a14d-4c623example Duration: 1280.82 ms Billed Duration: 1300 ms Memory Size: 128 MB Max Memory Used: 86 MB
AWSマネジメントコンソール上もCompleted
になっていました!
まとめ
CodePipelineからAWS Lambdaを呼び出す場合、アクションで設定したユーザーパラメーターをLambda Functionから参照することができます。それによって複数のパイプラインから同じLambda Functionを呼び出しても同様の処理を行うことができそうです。また、継続トークンを使用することで再実行までしてくれるCodePipeline最高!という気持ちになりました。
コーディングが必要でLambda Functionのメンテはしなくてはなりませんが、CodePipelineからAWS Lambdaを呼び出すことでCI/CDの幅が広がるのではないかと考えています。例えば、デプロイ後にAWS LambdaからのHTTPリクエストのテストを行ったり、Route 53のCNAME入れ替えを自動化したりなどのユースケースがあるかと思います。
本エントリの内容が少しでもお役にたてれば幸いです。