はじめに
以下の記事では 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 した際に任意の処理をフックしたいケースについて紹介しました。あまり一般的なケースではないですが、知っておいて損はないかと思います。