CloudFrontの作成、設定変更、キャッシュクリアが完了したら通知して欲しかったので作ってみた
AWS事業本部の梶原@福岡オフィスです。
CloudFrontの作成や、設定変更後のデプロイって、ちょっぴり時間かかりますよね(個人の意見です) やらかしちゃった後のキャッシュクリアとかも、完了するまではよはよ!もう急いで!ってなりますよね(個人の意見です) とはいえ、ずっとコンソールに張り付くのも、なんだかな~なので、状態が変化したら通知してくれるのイベントないかなーと探したんですがちょっと見つからなかったので、作りました。
構成はこんな感じです。
状態を監視するlambdaのリトライについてはSQSを使用しています。 詳しくはこちら
ざっくり動きを説明すると
1. CloudWatch EventRuleで、CloudTrailに出力されるCloudFrontのイベントを補足 2. 補足したらSQSのキューにメッセージを投入 3. SQSからLambdaを起動 3.1 完了していなければ、エラーを返すことによりリトライ 3.2 完了していれば、SNS通知を行い、正常終了
と言うことで、それでは各種リソースを作成していきます!というのも、結構手間なので CloudFormation一撃シリーズ化しました。 これで、メールアドレスだけあれば、通知がきますよ!(検証はちゃんとしてくださいね)
いるもの(前提条件)
- AWSアカウント(各種権限)
- メールアカウント(SNS通知用)
- CluodTrailが有効になっていること
各種設定
CloudWatch Rule 設定
イベントソース:CloudFront
イベント名:
- CreateDistribution(CloudFrontディストリビューションの作成),
- CreateDistributionWithTags(CloudFrontディストリビューションの作成タグ付き),
- UpdateDistribution(CloudFrontディストリビューションの設定更新)
- CreateInvalidation(キャッシュクリア)
※補足 監視対象のイベント名はAPIリファレンスなどを参照してください。 https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_Operations.html
SQS 設定
- デフォルトの可視性タイムアウト: 1 分
- メッセージ保持期間: 4 日
- メッセージ受信待機時間: 20 秒
1分間隔で、リトライして,Lambdaを実行するようにしています。取得(リトライ間隔)は調整ください、そんなに要求が厳しくなければ5分でも (CloudFormationのパラメータ化しています)
lambda に割り当てたRole権限
- 基本的なLambdaの実行権限(マネージドポリシー)
- SQSへのアクセス(マネージドポリシー)
- CloudFrontへの読み取り専用アクセス(マネージドポリシー)
- SNSへのPublish権限
リージョンについて
CloudFrontのイベントを補足するために、米国東部 (バージニア北部)でCloudFormationを実行する必要があります。 CloudWatchのルール以外の各種リソースはとくにリージョンの縛りはありませんが、動作確認は米国東部 (バージニア北部)のリソースで実施しています。 また、lambdaのソースを置くバケットなどもCloudFormationを実行するバケットと同じところに置く必要があります。
リソースの作成
CloudFormationテンプレートのポイント
長いので、全体はブログの最後に記載します。
- CloudWatch EventRuleで、CloudTrailに出力されるCloudFrontのイベントを補足
- 補足したらSQSのキューにメッセージを投入
の部分のテンプレートは下記になります。
CloudFrontEventsRule: Type: AWS::Events::Rule Properties: Description: "CloudFrontEventsRule" EventPattern: source: - aws.cloudfront detail-type: - AWS API Call via CloudTrail detail: eventSource: - cloudfront.amazonaws.com eventName: - CreateDistribution - CreateDistributionWithTags - UpdateDistribution - CreateInvalidation State: "ENABLED" Targets: - Id: "CFEventSqsQueue" Arn: !GetAtt CFEventSqsQueue.Arn
イベントパターンを変更することで、キャッシュクリアだけのイベントを補足、また特定のディストリビューションだけ補足することもできますので ぜひカスタマイズしてみてください。 CloudTrailのイベントソースの書き方などは結構、試行錯誤しました。判れば簡単ですが他のイベントを補足する際などにも参考になるかと
lambda関数は特に変な処理はさせていないと思うで、適時カスタマイズしてください。 むしろエラー処理とかすっとばしてるので、カスタマイズ前提でお願いします。
CloudFormationの実行
テンプレートと、lambdaのソースはS3バケットに置いてます。 ログイン後、リージョンを米国東部 (バージニア北部)に変更して、下記'ここをクリック'を押下してCloudFormationを実行し、パラメータに通知先のメールアドレスを入れてください。
正常にリソースが作成されたら、下記のような感じになるかと思います。
下記件名でSNS通知の確認がメールアドレス宛に来ていると思うので、承認(Confirmation)してください。
AWS Notification - Subscription Confirmation
動作確認
CloudFrontの作成または設定変更を行うとSQSにイベントが通知され、lambdaが動きます。
CloudFrontのStatus
がDeployed
になるまで、1分間隔でリトライを行います。
キャッシュのクリアの場合はInvalidation
のStatus
がCompleted
になるまで1分間隔でリトライします。
リトライを続けても4日間でメッセージは消えるようになっているので、万が一エラーが出続けても4日経過すると消えます。
まとめ
実装自体は、そうでもなかったのですが、やっぱり動作確認に結構時間がとられました。(あくまで個人の意見です) 実際にこのまま使うことは想定していないですが、CloudTrailのイベントの補足の仕方、CloudFrontに限らずステータスの完了を待つなどの 時に使えるテクニックではないでしょうか?
また、SNS通知でなく、再度別のSQSキューに入れて、後続処理を流していくなどの使い方などもできるかもしれません。 結構ニッチなニーズな気はしますが、どなたかの役にたつと幸いです。
テンプレート
AWSTemplateFormatVersion: '2010-09-09' Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "SQS Settings" Parameters: - ReceiveMessageWaitTimeSeconds - VisibilityTimeout - MessageRetentionPeriod - Label: default: "SNS Settings" Parameters: - EMailAddress ParameterLabels: VisibilityTimeout: default: "VisibilityTimeout" ReceiveMessageWaitTimeSeconds: default: "ReceiveMessageWaitTimeSeconds" MessageRetentionPeriod: default: "MessageRetentionPeriod" EMailAddress: default: "E-Mail Address" Parameters: VisibilityTimeout: Type: Number Default: 60 ReceiveMessageWaitTimeSeconds: Type: Number Default: 20 MessageRetentionPeriod: Type: Number Default: 345600 EMailAddress: Type: String Default: [email protected] Resources: CFEventSqsQueue: Type: AWS::SQS::Queue Properties: ReceiveMessageWaitTimeSeconds: !Ref ReceiveMessageWaitTimeSeconds VisibilityTimeout: !Ref VisibilityTimeout MessageRetentionPeriod: !Ref MessageRetentionPeriod CloudFrontEventsRule: Type: AWS::Events::Rule Properties: Description: "CloudFrontEventsRule" EventPattern: source: - aws.cloudfront detail-type: - AWS API Call via CloudTrail detail: eventSource: - cloudfront.amazonaws.com eventName: - CreateDistribution - CreateDistributionWithTags - UpdateDistribution - CreateInvalidation State: "ENABLED" Targets: - Id: "CFEventSqsQueue" Arn: !GetAtt CFEventSqsQueue.Arn CFEventSqsQueuePolicy: Type: AWS::SQS::QueuePolicy Properties: PolicyDocument: Version: '2012-10-17' Statement: - Sid: AWSEvents_CloudFrontEventsRule_Id1 Effect: Allow Principal: Service: events.amazonaws.com Action: sqs:SendMessage Resource: !GetAtt CFEventSqsQueue.Arn Condition: ArnEquals: aws:SourceArn: !GetAtt CloudFrontEventsRule.Arn Queues: - !Ref CFEventSqsQueue EMailSNSTopic: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: !Ref EMailAddress Protocol: email CFGetStatusLambdaFunc: Type: AWS::Lambda::Function Properties: Handler: index.handler Role: !GetAtt LambdaExecutionRole.Arn Environment: Variables: SNS_TOPIC_ARN: !Ref EMailSNSTopic Code: S3Bucket: pub-devio-blog-vtuisp2o S3Key: lambda/lambda-cf-getevent.zip Runtime: nodejs8.10 Timeout: 30 EventSourceMapping: Type: AWS::Lambda::EventSourceMapping Properties: BatchSize: 1 EventSourceArn: !GetAtt CFEventSqsQueue.Arn FunctionName: !GetAtt CFGetStatusLambdaFunc.Arn LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole - arn:aws:iam::aws:policy/CloudFrontReadOnlyAccess Path: "/" Policies: - PolicyName: LambdaExecutionRole-SnsPublishPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sns:Publish Resource: arn:aws:sns:*:*:*
lambda関数
var AWS = require('aws-sdk'); var SNS_TOPIC_ARN = process.env.SNS_TOPIC_ARN; exports.handler = async(event) => { // SQSで1メッセージのみ取得 var body = JSON.parse(event.Records[0].body); var response; switch (body.detail.eventName) { case 'CreateDistribution': case 'CreateDistributionWithTags': case 'UpdateDistribution': console.info(body.detail.eventName + ' Event'); response = getDistributionStatus(body); break; case 'CreateInvalidation': console.info(body.detail.eventName + ' Event'); response = getInvalidationStatus(body); break; default: console.warn('Not Support Event ' + body.detail.eventName); return; } return response; }; async function getDistributionStatus(body) { console.log(JSON.stringify(body, null, 4)); // CoudFront getDistribution var cloudfront = new AWS.CloudFront(); var params; switch (body.detail.eventName) { case 'CreateDistribution': case 'CreateDistributionWithTags': params = { Id: body.detail.responseElements.distribution.id }; break; case 'UpdateDistribution': params = { Id: body.detail.requestParameters.id }; break; } var distribution = await cloudfront.getDistribution(params).promise(); console.log(JSON.stringify(distribution, null, 4)); if (distribution.Status != 'Deployed') { console.info('DistributionId:' + params.Id + ' Distribution Status ' + distribution.Status + ' to retry'); // throw Error const error = new Error("Retry getDistribution"); throw error; } console.info('DistributionId:' + params.Id + ' Distribution Status ' + distribution.Status); // SNS publish var message = JSON.stringify(distribution, null, 4); var result = snsPublish( message, 'distribution Status ' + distribution.Status, SNS_TOPIC_ARN ); console.log(JSON.stringify(result, null, 4)); return distribution; } async function getInvalidationStatus(body) { // CoudFront getInvalidation var cloudfront = new AWS.CloudFront(); var params = { DistributionId: body.detail.requestParameters.distributionId, Id: body.detail.responseElements.invalidation.id }; var invalidation = await cloudfront.getInvalidation(params).promise(); console.log(JSON.stringify(invalidation, null, 4)); if (invalidation.Status != 'Completed') { console.info('DistributionId:' + params.DistributionId + ' Invalidation Status ' + invalidation.Status + ' to retry'); // throw Error const error = new Error("Retry getInvalidation"); throw error; } console.info('DistributionId:' + params.DistributionId + ' Invalidation Status ' + invalidation.Status); // SNS publish var message = JSON.stringify(invalidation, null, 4); var result = snsPublish( message, 'Invalidation Completed', SNS_TOPIC_ARN ); console.log(JSON.stringify(result, null, 4)); return invalidation; } async function snsPublish(message, subject, topicArn) { var sns = new AWS.SNS(); var params = { Message: message, Subject: subject, TopicArn: topicArn }; var result = await sns.publish(params).promise(); return result; }