とある Blue/Green Deployment 構築・運用案(ecspresso + CodeDeploy)
先週(2021/4/12 - 17 間)だいぶ AWS CodeDeploy による AWS Fargate への Blue/Green Deployment を動かしてみてわかってきたように思うので整理してみる記事です。
実習コードはこちらに置いてます。参考になるようであれば自由にご利用ください。
簡単な HTML を置いた nginx コンテナ2つを切り替えるような感じの環境です。
Blue/Green HTML
blue
green
とりあえずの簡単な造りなので、
- デフォルトブランチの最新版がデプロイ対象
- 世代を戻せるのも1世代前だけ
という仕組みになっています。
任意の世代に戻せるようにする・現在のリビジョンを明確に識別するには、適切に Docker Image のタグと ECS タスク定義との整合性を取れるように考慮したバージョニング構造が必要です。
概要
- CodeDeploy の使いどころ
- フロントエンドの裏にバックエンドがあるような構成
- 構築における特記事項
- Load Balancer・Target Group
- ECS Service Discovery
- ECS Service Definition
- CodeDeploy と IAM
- 運用(案)
- 最新のコードベースで ECR を push
- バックエンドの ecspresso deploy
- フロントエンドの ecspresso register
- フロントエンドの ecspresso deploy
- CodeDeploy でトラフィックを制御
- 補足
- ロールバックに備えた ecspresso appspec
- 実習環境の片付け
- 簡素化に向けて
各論
CodeDeploy の使いどころ
複数のアプリケーションを一体として切り替えてアップデートする必要がある場合に Blue/Green Deployment を採用する意義があると思います。
例えばフロントエンドの裏にバックエンドがあるような構成です。
単体というか1タスクで完結しているようなケースでは、デフォルトの ローリングデプロイ の方が妥当だと思います。[1]
構築における特記事項
お試し構築は 実習コードの README に沿って行えば可能なはずです。
この節では特に大事なポイントについてのみ記述します。
Load Balancer・Target Group
公式ドキュメントから抜粋するのですが、当然ながら blue・green に相当する 2つのターゲットグループが必要 です。
- 最初に CodeDeploy アプリケーションおよびデプロイグループを作成する際に、
以下を指定する必要があります。
- ロードバランサーに対して 2 つのターゲットグループを定義する必要があります。
1 つのターゲットグループは、Amazon ECS サービスの作成時に、ロードバランサーに対して
定義された最初のターゲットグループです。
ロードバランサーに関しては必須なリスナーは少なくとも1つ、オプショナルでテストリスナーを設定可能です。[2]
- 本番稼働用リスナーをロードバランサーに追加する必要があります。
これは本番トラフィックをルーティングするために使用されます。
- オプションテストリスナーをロードバランサーに追加することができます。
これはテストトラフィックをルーティングするために使用されます。テストリスナーを指定する場合、
CodeDeploy はデプロイメント中にテストトラフィックを置換タスクセットにルーティングします。
- 本番稼働用とテストリスナーの両方が同じロードバランサーに属している必要があります。
- ロードバランサーに対してターゲットグループを定義する必要があります。
ターゲットグループは本番稼働用リスナーを通じてトラフィックをサービスの元のタスクセットにルーティングします。
実習コードでは 8080 ポートで受けるテストリスナーを用意しています。 8080 ポートへのアクセスを接続元IPで絞り込む等でテストリスナーの公開をコントロールすることが可能です。[3]
ECS Service Discovery
実習コードには記述を入れていないのですが、バックエンド等の通信先がある場合には Service Discovery 機能を利用すると楽に運用できると思います。
通信イメージ
Service Discovery の機能説明は↓の 23 - 31 ページが図解もありわかりやすいです。
公式のガイドは↓こちら。
せっかくなので構築用の Terraform コードサンプルを記載しておこうと思います。
バックエンドの ECS サービス等を適宜実装に加えてご利用ください。
modules/ecs-fargate/main.tf
resource "aws_service_discovery_private_dns_namespace" "sd_private_dns_namespace_module" {
name = var.private_dns_namespace_name
description = var.private_dns_namespace_description
vpc = var.vpc_id
}
resource "aws_service_discovery_service" "sd_service_front_bg_module" {
name = var.service_discovery_front_bg_name
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.sd_private_dns_namespace_module.id
dns_records {
ttl = 10
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 1
}
}
resource "aws_service_discovery_service" "sd_service_back_blue_module" {
name = var.service_discovery_back_blue_name
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.sd_private_dns_namespace_module.id
dns_records {
ttl = 10
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 1
}
}
resource "aws_service_discovery_service" "sd_service_back_green_module" {
name = var.service_discovery_back_green_name
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.sd_private_dns_namespace_module.id
dns_records {
ttl = 10
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 1
}
}
modules/ecs-fargate/outputs.tf
output "out_service_discovery_front_bg" {
value = aws_service_discovery_service.sd_service_front_bg_module.name
}
output "out_service_discovery_back_blue" {
value = aws_service_discovery_service.sd_service_back_blue_module.name
}
output "out_service_discovery_back_green" {
value = aws_service_discovery_service.sd_service_back_green_module.name
}
modules/ecs-fargate/variables.tf
variable "service_discovery_front_bg_name" {}
variable "service_discovery_back_blue_name" {}
variable "service_discovery_back_green_name" {}
environments/development/ecs-fargate.tf
module "ecs-fargate" {
source = "../../modules/ecs-fargate"
# ・・・
################################
# Service Discovery
################################
service_discovery_front_bg_name = "frontend-bg"
service_discovery_back_blue_name = "backend-blue"
service_discovery_back_green_name = "backend-green"
}
environments/development/outputs.tf
# ・・・
################################
# Service Discovery
################################
output "out_service_discovery_front_bg_private_dns" {
value = "${module.ecs-fargate.out_service_discovery_front_bg}.${module.ecs-fargate.out_sd_private_dns_namespace}"
}
output "out_service_discovery_back_blue_private_dns" {
value = "${module.ecs-fargate.out_service_discovery_back_blue}.${module.ecs-fargate.out_sd_private_dns_namespace}"
}
output "out_service_discovery_back_green_private_dns" {
value = "${module.ecs-fargate.out_service_discovery_back_green}.${module.ecs-fargate.out_sd_private_dns_namespace}"
}
ECS Service Definition
ECS サービス定義への必要な設定変更については ecspresso advent calendar 2020 day 17 - CodeDeployとの連携 の CodeDeploy によるデプロイの実例 の説明に大変わかりやすく書かれています(感謝)。
具体的には以下がポイントになります。
- deploymentController の type に
CODE_DEPLOY
を指定する - deploymentConfiguration.deploymentCircuitBreaker の設定が記述されていたら削除する
deploymentController はサービス作成時に設定する必要があるので既存のものと同名のECSサービスを利用する場合は一旦削除して作成し直す必要がある点にも要注意かもしれません。
また、実習コードにおいて、タスク定義はデプロイ運用を見据えて、明確に blue と green にディレクトリを分けて ecspresso 用の設定一式を置いています。
- https://github.com/sogaoh/CodeDeployPractice/tree/develop/infra/environments/development/ecs/maintenance-blue
- https://github.com/sogaoh/CodeDeployPractice/tree/develop/infra/environments/development/ecs/maintenance-green
サービス定義は blue と green で同じになるので1つだけにして、blue・green 両方から参照する構成にしています。[4]
なお、実習コードの README にも記載していますが、infra/environments/development/ecs/maintenance-blue
ディレクトリで make create
でサービス作成を行った際、 context deadline exceeded で create FAILED. failed to wait service stable: RequestCanceled: waiter context canceled
となりましたが maintenance-bg ECS サービスは生成されて実行中のタスクが 1 になっていました。成功なんだけど表示が失敗となっていて気がかりですが、AWSマネジメントコンソールの ECS サービス「イベント」を見ても致命的な問題ではないように思ったので気にしないことにしました。
↓は脚注にもあるのですが公式のデプロイメントコントローラーの説明です。
CodeDeploy と IAM
Terraform で aws_codedeploy_app・aws_codedeploy_deployment_group リソースを作成するときのおそらく最大の注意点は、他のリソース一式とは分けて terraform apply することだと思います。自分は最初からそうはしなかったので、不足などで何度かの #terraform apply 爆死
をしました。
それともう1点、必要十分な IAM 権限を設定していないと CodeDeploy の管理画面が赤く染まるので、めげずに権限を設定する必要があります。
運用(案)
(0) 初回CodeDeploy前のリソース状態
blue 側でサービスが運用されている状態で、green 側一式にアップデートを行い、green 側の動作確認を行った上で green 側にサービス運用を切り替える際の想定デプロイ手順になります。
(1) 最新のコードベースで ECR を push
Fargate でサービスを稼働させるには中身のコンテナ一式が準備(ビルド)されている必要があります。
コードが fix された時点でコンテナのビルドを開始して、完了したら ECR に push しておきます。
コンテナのビルドに時間がかかるようなプロダクトは夜間にビルドと ECR push を行うようにしておいて、その日にコードのアップデートがなければデプロイだけ行えば OK 、という状態にしておくと良いかもしれません(緊急リリースのようなときにはビルドからやるしかないですが)。
実習コードにおいては以下の手順を行えばOKです。
git clone git@github.com:sogaoh/CodeDeployPractice.git
cd CodeDeployPractice/app
make green1 # docker build -> docker tag
make green2 # docker login -> docker push
(1) 時点の状態
(1) 時点の状態イメージ図
(2) バックエンドの ecspresso deploy
バックエンド側の ECS をアップデートするには通常のローリングデプロイを採用して以下のような手順を行えばOKだろうと思います。
(「だろう」と言うのは、この説明用にバックエンドの設定コードは未検証の状態で投入したためです。。)
# git clone git@github.com:sogaoh/CodeDeployPractice.git
cd CodeDeployPractice/infra/environments/development/ecs/backend-green
# vi .envrc # 下記 (*) の内容を設定
direnv allow
make verify # ecspresso --config config.yaml verify
make dry-deploy # ecspresso --config config.yaml deploy --dry-run
make deploy # ecspresso --config config.yaml deploy
記 (*) infra/environment/development/ecs/maintenance-green/.envrc example に記述例あり
(2) 時点の状態
(2) 時点の状態イメージ図
(3.1) フロントエンドの ecspresso register
フロントエンド側の ECS をアップデートは2段階になります。1段目として、新しいタスク定義を登録しておく、というオペレーションを行います。
実習コードにおいては以下の手順を行えばOKです。
# git clone git@github.com:sogaoh/CodeDeployPractice.git
cd CodeDeployPractice/infra/environments/development/ecs/maintenance-green
# vi .envrc # 上記 (*) の内容を設定
direnv allow
make verify # ecspresso --config config.yaml verify
make dry-register # ecspresso --config config.yaml register --dry-run
make register # ecspresso --config config.yaml register
(3.1) 時点の状態
(3.1) 時点の状態イメージ図
(3.2) フロントエンドの ecspresso deploy
フロントエンド側の ECS アップデート、2段階目は CodeDeploy を発動してテストトラフィックを開通させるオペレーションを行います。
実習コードにおいては以下の手順を行えばOKです。
# git clone git@github.com:sogaoh/CodeDeployPractice.git
cd CodeDeployPractice/infra/environments/development/ecs/maintenance-green
# vi .envrc # 上記 (*) の内容を設定
direnv allow
make verify # ecspresso --config config.yaml verify
make dry-deploy # ecspresso --config config.yaml deploy --dry-run --skip-task-definition --latest-task-definition
make deploy # ecspresso --config config.yaml deploy --skip-task-definition --latest-task-definition
(2) のときと make deploy
の内容が異なっていることにご注目ください。
現状の blue 側で動いているサービスはそのままに、(3.1) で登録しておいたタスク定義を、テストトラフィックを開通させた先のアプリケーションに使う、という指定です。
make deploy
を行ったあと、自動的に CodeDeploy の管理画面が呼び出されます。
(3.2) 時点の状態
(4) CodeDeploy でトラフィックを制御
テストトラフィックが開通されたので、新しいアプリケーションの動作チェックを適宜行います。
結果がOKと判断できたら トラフィックの再ルーティング
ボタンを押下して本番トラフィックを green 側に切り替え、
NGであれば デプロイを停止してロールバック
ボタンを押下してテストトラフィックを元に戻します(初回の場合、最初の green 側はタスク定義が登録されていないので 503 Service Temporarily Unavailable となりそう)。
(4) 時点の状態
(4) 時点(初回CodeDeploy後)のリソース状態
補足
ロールバックに備えた ecspresso appspec
一度トラフィックの再ルーティングを行った後に世代を戻したい、というケースは大いにありえると思います。
そのような場合に備えて、 ecspresso appspec
を実行しておいてコンソール出力された内容を採取しておくと有用です。
実習コードにおいては以下の手順を行えば採取できるようにしてあります。
# git clone git@github.com:sogaoh/CodeDeployPractice.git
cd CodeDeployPractice/infra/environments/development/ecs/maintenance-green
# vi .envrc # 上記 (*) の内容を設定
direnv allow
make appspec # ecspresso --config config.yaml appspec --task-definition=current
appspec 採取例
make appspec
ecspresso --config config.yaml appspec --task-definition=current
version: "0.0"
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: arn:aws:ecs:ap-northeast-1:123456789012:task-definition/dev-codedeploy-practice_maintenance-bg:26
LoadBalancerInfo:
ContainerName: maintenance-bg
ContainerPort: 80
PlatformVersion: 1.4.0
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- sg-XXXXXXXXXXXXXXXXX
Subnets:
- subnet-YYYYYYYYYYYYYYYYY
- subnet-ZZZZZZZZZZZZZZZZZ
戻す世代のコンテナ・タスク定義等が取り揃えて残っている前提ですが、CodeDeploy の管理画面から左のメニューの アプリケーション を選択し、デプロイグループ へ進んで デプロイの作成
を実行していけば任意の世代に戻すことができます。
任意の世代に戻すときの関連画面
実習環境の片付け
かんたんな実習環境ではありますが、そのままにしておくと 約 $4 / day かかるので、CodeDeploy をそれなりに理解できたら撤去するのが妥当と思います。
その手順概要は以下となります。
実習環境の片付け手順概要
- CodeDeploy リソースの削除
-
infra/environments/development/code_deploy
ディレクトリでterraform destroy
を実行するのが良いと思います。
-
- ECS サービスの全削除
- desiredCount を 0 にする(
make scale
でecspresso --config config.yaml scale --tasks=0
を実行) - ECS サービスの削除 (
ecspresso --config config.yaml delete
を実行し、確認でサービス名を入力)
- desiredCount を 0 にする(
- ECS タスク定義の全登録解除
- コマンドでの実現方法もあるとは思いますが、マネジメントコンソールで行うのが手っ取り早そうです。
- environments/development リソースの全削除
-
infra/environments/development
ディレクトリでterraform destroy
を実行するのが良いと思います。
-
- ECR リポジトリの削除
-
infra/environments/across
ディレクトリでterraform destroy
を実行するのが良いと思います。
-
簡素化に向けて
だいぶ把握ができたので、これからいよいよ簡素化、つまり CD Pipeline の整備をやっていく予定です。調整事項として今のところ想定しているのは以下2点です。
- tfstate を backend の S3 に保存する
- .envrc を S3 に置く形にして、Pipeline実行時に所定のパスからダウンロードする
CodeDeploy 前提でないローリングデプロイにおいては CircleCI と Bitbucket Pipeline で自動化を実現できているのですが、CodeDeploy を利用する場合の ecspresso deploy
で自動で CodeDeploy のデプロイメント管理画面が呼ばれるかどうかは未検証なので、この部分は AWS CLI で トラフィックの再ルーティング/デプロイを停止してロールバック を制御する方法を模索するかもしれません。
まとめ?
個人の感想として↓のように言ってますが、CodeDeploy の Deploy はたぶん Blue/Green Deployment の Deploy なんだろうな、と思い直しています。
最後まで読んでいただき、ありがとうございました。
-
ECS デプロイタイプ については 公式の サービス定義パラメータ の デプロイメントコントローラー に説明が記述されています ↩︎
-
aws_lb_listener を aws_codedeploy_deployment_group に設定する形になります ↩︎
-
https://github.com/sogaoh/CodeDeployPractice/blob/develop/infra/environments/development/ecs/maintenance-bg/ecs-service-def.json#L17 で blue の target group を指定してありますが、CodeDeploy によって blue -> green -> blue -> green ... と切り替わっていきます ↩︎
Discussion