Posted at

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


はじめに

こんにちは。Wano株式会社エンジニアのnariと申します。

今週も記事を書いていきます。

今回はECSにおけるデプロイ方法を、デフォルトのrolling updateの仕様がいささか怖いので、Blue/Greenに変更した話を書こうと思います。

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


Blue/Greenデプロイ とは

稼働中のBlueに対しては何もせず、別で新しいリビジョンのGreenを作って、任意の戦略に沿って全体をGreenに切り替えていくデプロイのこと。

稼働中の環境に手を加え、変更を重ねる従来のin place方式と比べ、内部の一貫性が保ちやすく、rollbackが容易と言われている。



(BlueGreenDeploymentから引用)


ECSでのデプロイの特徴


  • デフォルトのECSのデプロイは、in place方式(rolling update)を採用している。

  • 去年対応したBlue/Green方式はAll at once(全てを一気に切り替える)なRed/Blackデプロイであり、これはBlue/Greenのやり方の一つにすぎないらしい。


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


何故rolling updateからBlue/Greenデプロイにしたいか


  • 新規リリースによってトラブルが発生しても迅速にロールバックできる


  • 新旧システムの混在の可能性がないため、整合性に気を配る必要がない


というのが、一般的に謳われる理由ですが、加えてcanarycage - AWS ECSへの堅牢なデプロイツール - Qiitaでも書かれているように、


ECSのデフォルトのrolling updateは、壊れたタスクを配置してしまった場合、サービスが落ちる可能性がある


stage環境で試しに運用している際、度々この件で肝を冷やしました、、、

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


システム全体像(before/after)


  • 上記のようなCD pipelineである

  • この構成は変えずに、deploy stageの仕組みだけ以下のように変える


どう変更していったか


1.まず、target group(blue)とそれに対応するlistner、listener ruleを追加する


  • albにport8080許可のsgを追加する必要はない(moduleはこちら)


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より)

  • 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


参考文献