Edited at

CodeDeployでECSに複数のImageをBlue/Green Deploymentする方法

CodePipelineとCodeDeployを使って、ECSへBlue/Green Deploymentをする。ただし2つ以上のDockerイメージを持ったタスク定義の場合、少しドキュメントとは異なった手法を取らなければならない。今回はTerraformのサンプルも交えつつ紹介する。

結論から言うと、imageDetail.jsonは使わず、taskdef.jsonをsedコマンドで書き換えて渡す。


CodeBuildで行う処理

buildspec.ymlは以下のようにする。


buildspec.yml

version: 0.2

phases:
install:
commands:
- pip3 install awscli --upgrade --user
pre_build:
commands:
- $(aws ecr get-login --no-include-email --region ap-northeast-1)
- IMAGE_TAG=$CODEBUILD_RESOLVED_SOURCE_VERSION
- RP_IMAGE_URI=123456.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-rp
- WEB_IMAGE_URI=123456.dkr.ecr.ap-northeast-1.amazonaws.com/my-app-web
build:
commands:
- docker build -t $WEB_IMAGE_URI:$IMAGE_TAG -f Dockerfile_web .
- docker build -t $RP_IMAGE_URI:$IMAGE_TAG -f Dockerfile_rp .
post_build:
commands:
- docker push $RP_IMAGE_URI:$IMAGE_TAG
- docker push $WEB_IMAGE_URI:$IMAGE_TAG
- cat taskdef-production-template.json | sed -e s@\<IMAGE_RP_NAME\>@$RP_IMAGE_URI:$IMAGE_TAG@ -e s@\<IMAGE_WEB_NAME\>@$WEB_IMAGE_URI:$IMAGE_TAG@ > taskdef.json

artifacts:
files:
- appspec.yml
- taskdef.json


特別なことをやっているのはpost_buildの最後の行ぐらい。ここは用意したtaskdef-production-template.jsonの中にある<IMAGE_RP_NAME><IMAGE_WEB_NAM>をsedコマンドによって新しいイメージURIに書き換えている。そしてそのまま新しくtaskdef.jsonとしてartifactsで出力し、CodeDeployへ渡している。

taskdef-production-template.jsonは自分で作成したタスク定義の中身をそのままコピーし、image部分だけ"image": "<IMAGE_RP_NAME>"のように書き換えておく。

appspec.ymlは以下のようにロードバランサからのリクエストを受け取るイメージ名とポートを指定しておく。


appspec.yml

version: 0.0

Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "<TASK_DEFINITION>"
LoadBalancerInfo:
ContainerName: "rp"
ContainerPort: "80"

これを毎回CodeBuildのartifactsを渡している理由は、taskdef.json を含むアーティファクトのサイズを 3MB 未満にする必要があるからである。

CodePipelineのTerraformも参考程度に載せておく。


codepipeline.tf

resource "aws_codebuild_project" "production" {

count = "${local.is_prod ? 1 : 0}"
name = "airconkingdom-com-application-production"
build_timeout = 10
service_role = "${data.terraform_remote_state.samurai_iam_user.codebuild_role_arn}"

artifacts {
type = "CODEPIPELINE"
}

environment {
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/golang:1.11"
type = "LINUX_CONTAINER"
privileged_mode = true

environment_variable {
"name" = "DOCKER_BUILDKIT"
"value" = "1"
}
}

source {
type = "CODEPIPELINE"
buildspec = "buildspec-production.yml"
}
}

#####################################
# CodePipeline
#####################################
resource "aws_codepipeline" "example" {
stage {
name = "Source"

action {
name = "Source"
category = "Source"
owner = "AWS"
provider = "CodeCommit"
version = "1"
output_artifacts = ["app-source"]

configuration {
RepositoryName = "my-app"
BranchName = "master"
}
}
}

stage {
name = "ProductionBuild"

action {
name = "Build"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = "1"
input_artifacts = ["app-source"]
output_artifacts = ["production-artifacts"]

configuration {
ProjectName = "my-app-production-build"
}
}
}

stage {
name = "ProductionDeploy"

action {
name = "Deploy"
category = "Deploy"
owner = "AWS"
provider = "CodeDeployToECS"
input_artifacts = ["production-artifacts"]
version = "1"

configuration {
ApplicationName = "my-app-production"
DeploymentGroupName = "my-app-production"
TaskDefinitionTemplateArtifact = "production-artifacts"
TaskDefinitionTemplatePath = "taskdef.json"
AppSpecTemplateArtifact = "production-artifacts"
AppSpecTemplatePath = "appspec.yml"
}
}
}
}