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 未満にする必要があるから](https://qiita.com/hareku/items/a4575de31571071d879e)である。

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"
      }
    }
  }
}
```