ECSとCodePipelineのブルー/グリーンデプロイ構成をCDKで実装してみた
こんにちは、つくぼし(tsukuboshi0755)です!
以前以下のブログで、CDK(TypeScript)でECSとECRのコンテナ構成を実装しました。
今回はこの構成に対して、CodeCommit及びCodeBuildを含むCodePipelineを追加し、ECSのブルー/グリーンデプロイ構成を実現するCDKコードを作ってみたいと思います!
前提条件
今回は以下の通り、CDKv2を使ってコードを書いていきます。
$ cdk version 2.83.1 (build 006b542)
またDockerクライアントとしては、Rancher Desktopを使用します。
$ rdctl version rdctl client version: 1.1.0, targeting server version: v1
全体構成
今回はVPC+ALB+ECS(Fargate)+ECRに加えて、CodeCommit+CodeBuild+CodeDeploy+CodePipelineのCI/CD構成を追加で作成し、nginxコンテナをパブリック公開します。
リポジトリ
コード全体については、以下のリポジトリに格納していますのでご参照ください。
tsukuboshi/cdk-microservices-bluegreendeployment-template
コード解説
CDKコードの中核となるlib/cdk-microservices-bluegreendeployment-template-stack.ts
について説明します。
なお以前のECSとECRのコンテナ構成をCDKで実装してみた | DevelopersIOで説明した箇所は省略します。
また今回ブルー/グリーンデプロイで必要なビルド仕様ファイル及びイメージ定義ファイルについて触れますが、該当ファイルの詳細は以下のブログをご参照ください。
ECSのデプロイ設定
// Create ALB and ECS Fargate Service const service = new ecs_patterns.ApplicationLoadBalancedFargateService( this, "FargateService", { loadBalancerName: `${resourceName}-lb`, publicLoadBalancer: true, cluster: cluster, serviceName: `${resourceName}-service`, cpu: 256, desiredCount: 2, memoryLimitMiB: 512, assignPublicIp: true, taskSubnets: { subnetType: ec2.SubnetType.PUBLIC }, taskImageOptions: { family: `${resourceName}-taskdef`, containerName: `${resourceName}-container`, image: ecs.ContainerImage.fromEcrRepository(ecrRepository, "latest"), logDriver: new ecs.AwsLogDriver({ streamPrefix: `container`, logGroup: logGroup, }), }, deploymentController: { type: ecs.DeploymentControllerType.CODE_DEPLOY, }, } );
deployController
のtypeにDeploymentControllerType.CODEDEPLOY
を設定し、CodeDeployがデプロイを制御するように設定します。
この設定により、ECSタスクがブルー/グリーンデプロイでデプロイされるようになります。
2つ目のALBリスナー/ターゲットグループの作成
// Create Second Target Group const targetGroup2 = new elbv2.ApplicationTargetGroup(this, "TargetGroup", { vpc: vpc, port: 80, protocol: elbv2.ApplicationProtocol.HTTP, targetType: elbv2.TargetType.IP, }); ... (中略) ... // Create Second Listener const listener2 = service.loadBalancer.addListener("Listener2", { port: 8080, open: true, defaultTargetGroups: [targetGroup2], });
色々調査してみたものの、残念ながら現状のecs_patterns.ApplicationLoadBalancedFargateService
には、ブルー/グリーンデプロイに必要な追加のALBリスナー及びターゲットグループを作成する機能が見当たりませんでした。
そのため個別でALBリスナー及びターゲットグループを作成し、作成したロードバランサーに追加しておきます。
(この辺について、もしより良い作成方法をご存知な方がいらっしゃればぜひお伺いしたいです...!)
CodeCommitリポジトリの作成
// Create CodeCommit Repository const codeCommitRepository = new codecommit.Repository( this, "CodeCommitRepo", { repositoryName: `${resourceName}-codecommit-repo`, } );
CodePipelineのソースステージに指定するCodeCommitリポジトリを作成します。
なお今回は記述していませんが、以下のようにcode
にディレクトリパスを指定する事で、CodeCommitリポジトリが作成された後に、特定のディレクトリに含まれる初期コードをすぐにプッシュできます。
{ repositoryName: `${resourceName}-codecommit-repo`, code: codecommit.Code.fromDirectory( path.join(__dirname, "..", "app"), "main" ), }
CodeBuildプロジェクトの作成
// Create CloudWatch Log Group const buildLogGroup = new logs.LogGroup(this, "BuildLogGroup", { logGroupName: `/aws/codebuild/${resourceName}`, removalPolicy: RemovalPolicy.DESTROY, }); // Create CodeBuild Project const codeBuildProject = new codebuild.Project(this, "CodeBuildProject", { projectName: `${resourceName}-codebuild-project`, source: codebuild.Source.codeCommit({ repository: codeCommitRepository, }), environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_5_0, computeType: codebuild.ComputeType.SMALL, privileged: true, environmentVariables: { AWS_ACCOUNT_ID: { value: accountId, }, REPOSITORY_URI: { value: ecrRepository.repositoryUri, }, CONTAINER_BUILD_PATH: { value: ".", }, CONTAINER_NAME: { value: service.taskDefinition.defaultContainer?.containerName, }, TASK_FAMILY: { value: service.taskDefinition.family, }, TASK_ROLE_ARN: { value: service.taskDefinition.taskRole?.roleArn, }, EXECUTION_ROLE_ARN: { value: service.taskDefinition.executionRole?.roleArn, }, CW_LOG_GROUP: { value: logGroup.logGroupName, }, CW_LOG_STREAM_PREFIX: { value: "container", }, }, }, logging: { cloudWatch: { logGroup: buildLogGroup, }, }, // buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec.yml'), buildSpec: codebuild.BuildSpec.fromObject({ version: "0.2", phases: { pre_build: { commands: [ "echo Logging in to Amazon ECR...", "aws --version", "aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com", "COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)", "IMAGE_TAG=${COMMIT_HASH:=latest}", ], }, build: { commands: [ "echo Build started on `date`", "echo Building the Docker image...", "docker build -t $REPOSITORY_URI:latest $CONTAINER_BUILD_PATH", "docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG", ], }, post_build: { commands: [ "echo Build completed on `date`", "echo Pushing the Docker images...", "docker push $REPOSITORY_URI:$IMAGE_TAG", "docker push $REPOSITORY_URI:latest", "echo Writing image detail file...", 'printf \'{"ImageURI":"%s"}\' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json', "echo Rewriting task definitions file...", 'sed -i -e "s##$TASK_FAMILY#" taskdef.json', 'sed -i -e "s##$TASK_ROLE_ARN#" taskdef.json', 'sed -i -e "s##$EXECUTION_ROLE_ARN#" taskdef.json', 'sed -i -e "s##$CONTAINER_NAME#" taskdef.json', 'sed -i -e "s##$CW_LOG_GROUP#" taskdef.json', 'sed -i -e "s##$AWS_DEFAULT_REGION#" taskdef.json', 'sed -i -e "s##$CW_LOG_STREAM_PREFIX#" taskdef.json', "echo Rewriting appspec file...", 'sed -i -e "s##$CONTAINER_NAME#" appspec.yml', ], }, }, artifacts: { files: ["imageDetail.json", "taskdef.json", "appspec.yml"], }, }), });
CodePipelineのビルドステージに指定するCodeBuildプロジェクトを作成します。
今回はbuildSpec
にBuildSpec.fromObject
を設定し、ビルド仕様をコマンド形式で書く形にしています。
この形にする事で、ビルド仕様をCDKコード内に記述でき、CDKの機能を利用しやすくなります。
参考:CodeBuildのbuildspecはCDKのBuildSpec.fromObjectで作成するようにした - mazyu36の日記
なおコマンド形式ではなく、ソースに存在するビルド仕様ファイル(buildspec.yml)を読み込む形にしたい場合は、fromSourceFilename
を用いる事で実現できます。
その場合はBuildSpec
の箇所を以下の内容に書き換えた上で、CodeCommitリポジトリにビルド仕様ファイルをプッシュしてください。
buildSpec: codebuild.BuildSpec.fromSourceFilename('./app/buildspec.yml'),
CodeBuildサービスロールへのECRアクセス権付与
// Create ECR Access Policy const ecrAccessPolicy = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "ecr:BatchCheckLayerAvailability", "ecr:CompleteLayerUpload", "ecr:GetAuthorizationToken", "ecr:InitiateLayerUpload", "ecr:PutImage", "ecr:UploadLayerPart", ], resources: ["*"], }); // Add ECR Access Policy to CodeBuild Project codeBuildProject.addToRolePolicy(ecrAccessPolicy);
先程のProject
ではCodeBuildサービスロールも自動的に作成されますが、デフォルトではECRへのPush権限がついていません。
そのため必要なECR権限を、CodeBuildサービスロールに付与します。
参考:CodeBuild のDocker サンプル - AWS CodeBuild
CodeDeployアプリケーション/デプロイグループの作成
// Create CodeDeploy Application const codeDeployapplication = new codedeploy.EcsApplication( this, "CodeDeployApplication", { applicationName: `${resourceName}-codedeploy-application`, } ); // Create CodeDeploy Group const codeDeployGroup = new codedeploy.EcsDeploymentGroup( this, "BlueGreenDG", { service: service.service, application: codeDeployapplication, deploymentGroupName: `${resourceName}-codedeploy-group`, blueGreenDeploymentConfig: { deploymentApprovalWaitTime: Duration.minutes(60), terminationWaitTime: Duration.minutes(5), blueTargetGroup: service.targetGroup, greenTargetGroup: targetGroup2, listener: service.listener, testListener: listener2, }, deploymentConfig: codedeploy.EcsDeploymentConfig.ALL_AT_ONCE, } );
CodePipelineのデプロイステージに指定するCodeDeployのデプロイグループを作成します。
まずEcsDeploymentGroup.blueGreenDeploymentConfig
にあるblueTargetGroup及びlistenerに対して、ecs_patterns.ApplicationLoadBalancedFargateService
で自動的に作成されるALBリスナー及びターゲットグループを指定します。
一方でgreenTargetTargetGroup及びtestlistnerに対して、先程追加で作成した2つ目のALBリスナー及びターゲットグループを指定します。
またdeploymentApprovalWaitTimeを設定する事で、CodeDeployが自動でトラフィックを再ルーティングするまでの待機時間を変更できます。
さらにterminationWaitTimeを変更する事で、CodeDeployが自動で元のリビジョンを終了するまでの待機時間を変更できます。
なおCodeDeployのサービスロールは、EcsDeploymentGroup
で自動的に作成され、デフォルトでAWSCodeDeployRoleForECS
マネージドポリシーが付与されます。
S3アーティファクトバケットの作成
// Create Artifact Bucket for CodePipeline const artifactBucket = new s3.Bucket(this, "ArtifactBucket", { bucketName: `${resourceName}-artifact-bucket-${accountId}`, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, encryption: s3.BucketEncryption.S3_MANAGED, removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, });
CodePipelineで使用するS3アーティファクトバケットを作成します。
今回のS3バケットは検証用のため、autoDeleteObjects
をtrueで設定し、バケットにオブジェクトが存在する場合でも削除できるようにしています。
CodePipelineの作成
// Create CodePipeline const pipeline = new codepipeline.Pipeline(this, "CodePipeline", { artifactBucket: artifactBucket, pipelineName: `${resourceName}-pipeline`, }); // Add Source Stage to Pipeline const sourceOutput = new codepipeline.Artifact(`${resourceName}-source`); const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ actionName: "Source", repository: codeCommitRepository, output: sourceOutput, branch: "main", trigger: codepipeline_actions.CodeCommitTrigger.EVENTS, }); pipeline.addStage({ stageName: "Source", actions: [sourceAction], }); // Add Build Stage to Pipeline const buildOutput = new codepipeline.Artifact(`${resourceName}-build`); const buildAction = new codepipeline_actions.CodeBuildAction({ actionName: "Build", project: codeBuildProject, input: sourceOutput, outputs: [buildOutput], }); pipeline.addStage({ stageName: "Build", actions: [buildAction], }); // Add Deploy Stage to Pipeline const deployAction = new codepipeline_actions.CodeDeployEcsDeployAction({ actionName: "Deploy", deploymentGroup: codeDeployGroup, taskDefinitionTemplateFile: buildOutput.atPath("taskdef.json"), appSpecTemplateFile: buildOutput.atPath("appspec.yml"), containerImageInputs: [ { input: buildOutput, taskDefinitionPlaceholder: "IMAGE1_NAME", }, ], }); pipeline.addStage({ stageName: "Deploy", actions: [deployAction], });
CodePipelineを作成し、ソースステージ/ビルドステージ/デプロイステージを各々設定します。
CodeCommitSourceAction
のtriggerにCodeCommitTrigger.EVENTS
を設定する事で、CodePipeline用のEventBridgeルールが合わせて作成され、CodeCommitリポジトリにプッシュされると自動的にパイプラインが起動するようになります。
またCodeDeployEcsDeployAction
のtaskDefinitionTemplateFile及びappSpecTemplateFileには、ビルドステージで作成したタスク定義ファイル(taskdef.json)及びアプリケーション仕様ファイル(appspec.yml)を指定します。
さらにcontainerImageInputs
にあるtaskDefinitionPlaceholderには、タスク定義ファイル(taskdef.json)のコンテナイメージを置き換えるためのプレースホルダとしてIMAGE1_NAME
を指定します。
動作確認
CDKコードのデプロイ、及びECSタスクの起動確認については、ECSとECRのコンテナ構成をCDKで実装してみた | DevelopersIOをご参照ください。
ここではCodePipelineによるブルー/グリーンデプロイの動作確認を実施します。
CDKコードをデプロイした後、パイプラインが以下の通り作成されている事を確認します。
続いてパイプラインを稼働させるため、コンソール上で作成されたCodeCommitリポジトリをクリックし、"ファイルのアップロード"をクリックします。
"Choose File"をクリックし、例としてGitHubリポジトリのapp/Dockerfile
を選択してください。
また今回は検証のため、"作成者名"及び"Eメールアドレス"は何でも構いません。
項目を入力し終えたら、"変更のコミット"をクリックします。
さらに今回はブルー/グリーンデプロイにapp/taskdef.json
及びapp/appspec.yml
が必要になるため、これらのファイルもCodeCommitリポジトリに繰り返しコミットしてください。
コミット完了後、正常に設定されていればパイプラインが稼働します。
数分後、Deployステージで停止するので、"詳細"をクリックしてください。
デプロイの内容に問題がなければ、"トラフィックの再ルーティング"をクリックし、新しいタスクセットへの再ルーティングを実施してください。
再ルーティング完了後、"元のタスクセットの終了"をクリックし、古いタスクセットを削除してください。
元のタスクセットが終了し、パイプラインのDeployステージまで成功していればブルー/グリーンデプロイの動作確認は完了です!
最後に
今回はCodeCommit及びCodeBuildを含むCodePipelineを追加し、ECSのブルー/グリーンデプロイ構成を実現するCDKコードを作ってみました。
ぜひCDKでECSとCodePipelineのブルー/グリーンデプロイ構成を実装する際に、参考にしてみてください。
なおブルー/グリーンデプロイ構成と対をなす、ローリングアップデート構成については、以下のブログで解説していますので、必要に応じてご参照ください。
以上、つくぼし(tsukuboshi0755)でした!