はじめに
以下の記事では CloudFront + S3 でホストされたアプリケーションを Blue/Green デプロイする方法について解説しました。
CloudFront の継続的デプロイをパイプライン化してみた
今回はそれに関連して、CodePipeline の手動承認ステージで Reject した場合に任意の処理を走らせる方法を紹介します。サンプルリポジトリはこちらです。
概要
図の下側に生えている網掛け部分が対象です。クリーンアップ処理で使うスクリプトは、パイプラインの Cleanup ステージで使っているものを流用するために CodeBuild で実装しています。
実装
GitHub へのリンクとコードの紹介です。
コンテキストで設定した cloudfrontConfig.stagingDistributionCleanupEnabled
が true
の場合にクリーンアップ用のリソースが synthesize されます。
ロールの作成
CodeBuild プロジェクトにアタッチするロールを作ります。ポイントとしては ListContinuousDeploymentPolicies
の許可が必要でした。
// Create codebuild project role when approval failed const frontendPurgeProjectRole = new iam.Role(this, "FrontendPurgeProjectRole", { roleName: `${serviceName}-frontend-purge-project-role`, assumedBy: new iam.ServicePrincipal("codebuild.amazonaws.com"), inlinePolicies: { ["FrontendPurgeProjectRoleAdditionalPolicy"]: new iam.PolicyDocument({ statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["ssm:GetParameter", "ssm:GetParameters", "ssm:PutParameter"], resources: [`arn:aws:ssm:${this.region}:${this.account}:*`], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["s3:GetBucketAcl", "s3:PutBucketAcl"], resources: [cloudfrontLogBucket.bucketArn, cloudfrontLogBucket.bucketArn + "/*"], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "cloudfront:GetDistribution", "cloudfront:GetDistributionConfig", "cloudfront:DeleteDistribution", "cloudfront:UpdateDistribution", "cloudfront:GetInvalidation", "cloudfront:CreateInvalidation", ], resources: [`arn:aws:cloudfront::${this.account}:distribution/*`], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "cloudfront:GetContinuousDeploymentPolicy", "cloudfront:DeleteContinuousDeploymentPolicy", "cloudfront:ListContinuousDeploymentPolicies", // <- ココ ], resources: [`arn:aws:cloudfront::${this.account}:continuous-deployment-policy/*`], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["wafv2:GetWebACL"], resources: [webAclArn], }), ], }), }, });
CodeBuild プロジェクトの作成
CodeBuild ではやはり環境変数が重要です。SSM パラメータストアから CloudFront のディストリビューション ID を取得します。
// Create codebuild project when approval failed const frontendPurgeProject = new codebuild.Project(this, "FrontendPurgeProject", { projectName: `${serviceName}-frontend-purge-project`, source: codebuild.Source.codeCommit({ repository: codeCommitRepository, branchOrRef: branch, cloneDepth: 1, }), buildSpec: codebuild.BuildSpec.fromSourceFilename(`${buildspecDir}/buildspec.frontend.cleanup.yml`), environment: { buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_4, }, environmentVariables: { SERVICE: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: serviceName, }, PRODUCTION_DISTRIBUTION_ID: { type: codebuild.BuildEnvironmentVariableType.PARAMETER_STORE, value: `/${serviceName}/cloudfront/cfcd-production`, }, STAGING_DISTRIBUTION_ID: { type: codebuild.BuildEnvironmentVariableType.PARAMETER_STORE, value: `/${serviceName}/cloudfront/cfcd-staging`, }, }, badge: false, role: frontendPurgeProjectRole, logging: { cloudWatch: { logGroup: new logs.LogGroup(this, "FrontendPurgeProjectLogGroup", { logGroupName: `/${serviceName}/codebuild/frontend-purge-project`, removalPolicy: RemovalPolicy.DESTROY, retention: logs.RetentionDays.THREE_DAYS, }), }, }, });
EventBridge ルールのターゲットにアタッチするロール
これは作らなくても裏でいい感じに作ってくれるのですが、命名をちゃんとしたいので作成しています。ポリシーは何も定義しなければ CDK が判断したデフォルトのものだけが適用されます。
// Create event role for frontend cleanup project when apploval failed const frontendPurgeEventRole = new iam.Role(this, "frontendPurgeEventRole", { roleName: `${serviceName}-frontend-purge-event-role`, assumedBy: new iam.ServicePrincipal("events.amazonaws.com"), });
EventBridge ルール
今回の肝ですが、パイプラインの手動承認ステージで Reject した時に任意の処理をフックするためのイベントパターンを作ります。
こういうイベントパターンが必要なので、
{ "detail-type": ["CodePipeline Action Execution State Change"], "resources": ["arn:aws:codepipeline:\:\:\"], "source": ["aws.codepipeline"], "detail": { "stage": ["Approve"], "action": ["Approve"], "state": ["FAILED"] // <- Reject すると状態が FAILED になるのを利用 } }
こんな感じで書きます。
// Create eventbridge rule when approval failed const frontendPurgeEventRule = new events.Rule(this, "FrontendPurgeEventRule", { enabled: true, ruleName: `${serviceName}-frontend-purge-rule`, eventPattern: { source: ["aws.codepipeline"], detailType: ["CodePipeline Action Execution State Change"], resources: [frontendPipeline.pipelineArn], detail: { stage: [approveStageName], action: [approveStageName], state: ["FAILED"], }, }, });
Event ターゲット
最後にイベントのターゲットに CodeBuild プロジェクトを紐付け、イベントが発生した時に発火するようにします。
frontendPurgeEventRule.addTarget( new events_targets.CodeBuildProject(frontendPurgeProject, { eventRole: frontendPurgeEventRole, }) );
クリーンアップ用スクリプト
スクリプトの作成もまぁ大変でしたが、使い回しが効くので作っておくと便利です。
共通関数を以下で定義しています。
CloudFront ディストリビューションが InProgress
である場合は Deployed
になるまで待機します。
echo "PROCESS: Waiting for CloudFront distribution changes to propagate to edge locations." wait_distribution_deploy "$PRODUCTION_DISTRIBUTION_ID" wait_distribution_deploy "$STAGING_DISTRIBUTION_ID"
本番ディストリビューションから Continuous deployment policy をデタッチします。
echo "PROCESS: Detaching continuous deployment policy from CloudFront production distribution." prod_distribution_config=$(get_distribution_config "$PRODUCTION_DISTRIBUTION_ID") continuous_deployment_policy_id=$(jq -r ".DistributionConfig.ContinuousDeploymentPolicyId" <<<"$prod_distribution_config") prod_distribution_etag=$(jq -r ".ETag" <<<"$prod_distribution_config") updated_prod_distribution_config=$(jq ".DistributionConfig.ContinuousDeploymentPolicyId = \"\" | .DistributionConfig" <<<"$prod_distribution_config") update_distribution "$PRODUCTION_DISTRIBUTION_ID" "$updated_prod_distribution_config" "$prod_distribution_etag" wait_distribution_deploy "$PRODUCTION_DISTRIBUTION_ID"
Continuous deployment policy を削除します。
echo "PROCESS: Deleting continuous deployment policy." continuous_deployment_policy_etag=$(get_continuous_deployment_policy_etag "$continuous_deployment_policy_id") delete_continuous_deployment_policy "$continuous_deployment_policy_id" "$continuous_deployment_policy_etag"
ステージングディストリビューションを無効化します。
echo "PROCESS: Disabling CloudFront staging distribution." stg_distribution_config=$(get_distribution_config "$STAGING_DISTRIBUTION_ID") stg_distribution_etag=$(jq -r ".ETag" <<<"$stg_distribution_config") updated_stg_distribution_config=$(jq ".DistributionConfig.Enabled = false | .DistributionConfig" <<<"$stg_distribution_config") stg_distribution=$(update_distribution "$STAGING_DISTRIBUTION_ID" "$updated_stg_distribution_config" "$stg_distribution_etag") wait_distribution_deploy "$STAGING_DISTRIBUTION_ID"
ステージングディストリビューションを削除します。
echo "PROCESS: Deleting CloudFront staging distribution." stg_distribution_etag=$(jq -r ".ETag" <<<"$stg_distribution") delete_distribution "$STAGING_DISTRIBUTION_ID" "$stg_distribution_etag"
最後に、ステージングディストリビューションが削除されたことを SSM パラメータストアに記録します。
echo "PROCESS: Putting string literal 'deleted' to SSM parameter store." put_ssm_parameter "/$SERVICE/cloudfront/cfcd-staging" "deleted"
おわりに
CodePipeline の手動承認ステージで Reject した際に任意の処理をフックしたいケースについて紹介しました。あまり一般的なケースではないですが、知っておいて損はないかと思います。