AWS ECSでのデプロイをrolling updateからBlue/Greenデプロイに変更する

## はじめに
こんにちは。Wano株式会社エンジニアのnariと申します。
今週も記事を書いていきます。
今回は**ECSにおけるデプロイ方法を、デフォルトのrolling updateの仕様がいささか怖いので、Blue/Greenに変更した話**を書こうと思います。

今回のプロジェクトでは、ほぼ大体のインフラリソースをTerraformで管理しているため、設定は全てtfファイルで行います、ご了承ください。　　

## Blue/Greenデプロイ　とは
**稼働中のBlueに対しては何もせず、別で新しいリビジョンのGreenを作って、任意の戦略に沿って全体をGreenに切り替えていくデプロイ**のこと。
稼働中の環境に手を加え、変更を重ねる従来のin place方式と比べ、内部の一貫性が保ちやすく、rollbackが容易と言われている。
<img width="614" alt="スクリーンショット 2019-08-18 19.06.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321850/091641d3-e507-0f31-a37e-d6e589a14bc4.png">
([BlueGreenDeployment](https://martinfowler.com/bliki/BlueGreenDeployment.html)から引用)
## ECSでのデプロイの特徴

- デフォルトのECSのデプロイは、**in place方式(rolling update)**を採用している。
- 去年対応したBlue/Green方式は**All at once(全てを一気に切り替える)なRed/Blackデプロイ**であり、これはBlue/Greenのやり方の一つにすぎないらしい。  

ちなみに**Canary(部分だけ新しいリビジョンをデプロイして、問題がないかを先に確認すること)なデプロイ設定は対応していないため**、行いたい場合は自身でスクリプトを書く必要があります。

## 何故rolling updateからBlue/Greenデプロイにしたいか
- **新規リリースによってトラブルが発生しても迅速にロールバックできる**
- **新旧システムの混在の可能性がないため、整合性に気を配る必要がない**  

というのが、一般的に謳われる理由ですが、加えて[canarycage - AWS ECSへの堅牢なデプロイツール - Qiita](https://qiita.com/keroxp/items/7ae472bf7344c1efa021)でも書かれているように、
>**ECSのデフォルトのrolling updateは、壊れたタスクを配置してしまった場合、サービスが落ちる可能性がある**

stage環境で試しに運用している際、度々この件で肝を冷やしました、、、
これでは本番運用にあたって、眠れない夜を過ごすことになりそうだったのでBlue/Greenデプロイに変更することとしました。(これによって**壊れたタスクであるかがデプロイ前にチェックできるし、デプロイ後に不備が確認されても気軽にrollbackできる**)

## システム全体像(before/after)
<img width="751" alt="スクリーンショット 2019-08-18 17.27.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321850/aaef4947-d8a7-cadd-cd96-f4aef9eef502.png">

- 上記のようなCD pipelineである
- この構成は変えずに、**deploy stageの仕組みだけ以下のように変える**

<img width="630" alt="スクリーンショット 2019-08-18 17.51.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321850/da836c8c-35d7-2380-02c9-0e5613996cd1.png">

## どう変更していったか
### 1.まず、target group(blue)とそれに対応するlistner、listener ruleを追加する
- albにport8080許可のsgを追加する必要はない(moduleは[こちら](https://github.com/takafk9/terraform-sample-aws-modules))

```main.tf

module "ad_blue_listener" {
  source  = "../../modules/common/alb/listener"
  alb_arn = module.laboon_ad_alb.alb.arn
  /* alb listener required */
  listener_port     = 8080
  listener_protocol = "HTTP"
}

/* alb target*/
module "ad_blue_target_group" {
  source            = "../../modules/common/alb/target_group"
  target_group_name = "laboon-stage-blue-target-group"
  alb               = module.laboon_ad_alb.alb
  vpc_id            = local.laboon_stage_vpc_id
  target_port       = 8080
}

/* alb listner_rule*/
module "ad_blue_listener_rule" {
  source = "../../modules/common/alb/listener_rule"
  /* alb listener_rule required */
  listener_arn     = module.ad_blue_listener.lister_arn
  target_group_arn = module.ad_blue_target_group.target_group_arn
}
```

### 2.CodeDeploy(Blue/Green対応)を作成する
- deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"がポイント

```main.tf
# CodeDeploy for blue green
resource "aws_codedeploy_app" "app" {
  compute_platform = "ECS"
  name             = var.name
}

resource "aws_codedeploy_deployment_group" "group" {
  app_name               = aws_codedeploy_app.app.name
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
  deployment_group_name  = var.deployment_group_name
  service_role_arn       = var.service_role_arn

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }

    terminate_blue_instances_on_deployment_success {
      action                           = "TERMINATE"
      termination_wait_time_in_minutes = var.termination_wait_time_in_minutes
    }
  }

  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }

  ecs_service {
    cluster_name = var.cluster_name
    service_name = var.service_name
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = var.listener_arns
      }

      target_group {
        name = var.blue_target_name
      }

      target_group {
        name = var.green_target_name
      }
    }
  }
}

```
### 3.Codepipelineのコードのdeploy stageのactionを変更する
- blue/greenでもrolling updateでも対応できるmoduleに変更する

```main.tf
...
  stage {
    name = var.deploy_stage_name

    dynamic "action" {
      for_each = var.has_bg_service ? { dummy : "hoge" } : {}

      content {
        name     = var.ActionName
        category = "Deploy"
        owner    = "AWS"
        provider = "CodeDeployToECS"
        version  = 1
        input_artifacts = [
        "Build"]
        configuration = {
          ApplicationName                = var.ApplicationName
          DeploymentGroupName            = var.DeploymentGroupName
          TaskDefinitionTemplateArtifact = "Build"
          TaskDefinitionTemplatePath     = "taskdef.json"
          AppSpecTemplateArtifact        = "Build"
          AppSpecTemplatePath            = "appspec.yml"
        }
      }
    }

    dynamic "action" {
      for_each = var.has_another_service ? { dummy : "hoge" } : {}

      content {
        name            = var.AnothoerActionName
        category        = "Deploy"
        owner           = "AWS"
        provider        = "ECS"
        version         = 1
        input_artifacts = ["Build"]
        configuration = {
          ClusterName = var.AnothoerClusterName
          ServiceName = var.AnothoerServiceName
          FileName    = var.AnothoerFileName
        }
      }
    }

...
```
### 4.CodeDeployの設定ファイル(appspec.yml)を作成する
- projectの直下に生成する(別ディレクトリで管理し、build時に移動させてもOK)
- `TaskDefinition: "<TASK_DEFINITION>"`　はいじらずそのままに、ContainerNameとPortだけ変えてください  

```appspec.yml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "<TASK_DEFINITION>"　
        LoadBalancerInfo:
          ContainerName: "hogehoge"    　　　
          ContainerPort: "80"
```
### 5.CodeBuildの設定ファイル（buildspec.yml)を修正する
- 一つのサービスの中で、複数のイメージを変更したい場合は、imageDetail.jsonは使わず、taskdef.jsonをsedコマンドで書き換えて渡す。([CodeDeployでECSに複数のImageをBlue/Green Deploymentする方法 - Qiita](https://qiita.com/hareku/items/93b18335ee253fc1ba2c)より）
- taskdef-production-template.jsonは自分で作成したタスク定義の中身をそのままコピーし、image部分だけ`"image": "<IMAGE_RP_NAME>"`のように書き換えておく

```buildspec.yml
  post_build:
    commands:
      # 複数のイメージを更新したい場合こうするしかないっぽい
      - cat taskdef-production-template.json | sed -e s@\<IMAGE_APP_NAME\>@$APP_REPOSITORY_URL:latest@ -e s@\<IMAGE_WEB_NAME\>@$WEB_REPOSITORY_URL:latest@ > taskdef.json
artifacts:
  files:
    - taskdef.json
    - appspec.yml
```
## 参考文献
- [CodeDeployでECSに複数のImageをBlue/Green Deploymentする方法 - Qiita](https://qiita.com/hareku/items/93b18335ee253fc1ba2c)
- [CodePipelineからECSにBlue/Greenデプロイする ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/codepipeline-ecs-codedeploy/)
- [Blue/Greenデプロイとは？](https://aws.typepad.com/sajp/2015/12/what-is-blue-green-deployment.html)
- [canarycage - AWS ECSへの堅牢なデプロイツール - Qiita](https://qiita.com/keroxp/items/7ae472bf7344c1efa021)
- [AmazonECS / Fargate 本番運用のための構築とデプロイ方法まとめ - Qiita](https://qiita.com/HiromuMasuda0228/items/2170032142ae1c33adcc)