これまでECSはローリングアップデートのみが利用可能であり、B/Gデプロイを行う場合は、CodeDeployを別途作成する必要がありましたが、2025/07/17のアップデートにて、アプリケーション ロード バランサー(ALB)、ネットワーク ロード バランサー(NLB)、または ECS サービス コネクトからのトラフィックにサービスを提供するECSサービスに対してB/Gデプロイ戦略を取ることが可能になりました。
今回はALB+ECSの構成でECSのB/Gデプロイとライフサイクルフックを活用したカスタムテストの動作確認ができるTerraformを作成してみました。
インフラ構成
Internet
↓
ALB (Production Listener: 80, Test Listener: 8080)
↓
Target Groups (Blue/Green)
↓
ECS Fargate Tasks
コードの解説
1. 動作確認用簡易Webサーバ
以下2つのパスに対して応答するシンプルなWebサーバです。
-
/health
: ヘルスチェック用エンドポイント(ALBのターゲットグループで使用) -
/
: メインエンドポイント(Lambda関数によるカスタムテストの動作確認用)
main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Response struct {
Status string `json:"status,omitempty"`
Message string `json:"message,omitempty"`
Version string `json:"version"`
}
func main() {
version := "v1.0"
// Initialize Gin router
r := gin.Default()
// Health check endpoint
r.GET("/health", func(c *gin.Context) {
response := Response{
Status: "healthy",
Version: version,
}
c.JSON(http.StatusOK, response)
})
// Root endpoint
r.GET("/", func(c *gin.Context) {
response := Response{
Message: "Hello from Blue/Green Demo with Gin!",
Version: version,
}
c.JSON(http.StatusOK, response)
})
// Start server
r.Run(":8080")
}
2. Application Load Balancer (ALB)
ALBにはALB本体に加え以下6つのリソースが必要です。
- プロダクションリスナー
- プロダクションリスナー用カスタムルール
- テストリスナー
- テストリスナー用カスタムルール
- ターゲットグループ(グリーン環境用)
- ターゲットグループ(ブルー環境用)
リスナー
# Production Listener
resource "aws_lb_listener" "production" {
load_balancer_arn = aws_lb.app.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "NOT FOUND"
status_code = "404"
}
}
}
# Test Listener
resource "aws_lb_listener" "test" {
load_balancer_arn = aws_lb.app.arn
port = "8080"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "NOT FOUND"
status_code = "404"
}
}
}
リスナールール
# Production Listener Rule
resource "aws_lb_listener_rule" "production" {
listener_arn = aws_lb_listener.production.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
condition {
path_pattern {
values = ["*"]
}
}
lifecycle {
ignore_changes = [action] # ECSが管理するため変更を無視
}
}
# Test Listener Rule
resource "aws_lb_listener_rule" "test" {
listener_arn = aws_lb_listener.test.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.green.arn
}
condition {
path_pattern {
values = ["*"]
}
}
}
ターゲットグループ
# Target Group for Blue (Production)
resource "aws_lb_target_group" "blue" {
name = "${var.service_name}-blue-tg"
port = 8080
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 30
path = "/health"
matcher = "200"
port = "traffic-port"
protocol = "HTTP"
}
}
# Target Group for Green (Test)
resource "aws_lb_target_group" "green" {
name = "${var.service_name}-green-tg"
port = 8080
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 30
path = "/health"
matcher = "200"
port = "traffic-port"
protocol = "HTTP"
}
}
3. ECS Service
ECSサービス
resource "aws_ecs_service" "app" {
deployment_configuration {
strategy = "BLUE_GREEN" # B/Gデプロイを有効化
bake_time_in_minutes = 5 # 切り替え後の待機時間
lifecycle_hook {
hook_target_arn = aws_lambda_function.health_check_test.arn
role_arn = aws_iam_role.ecs_blue_green.arn
lifecycle_stages = ["POST_TEST_TRAFFIC_SHIFT"] # テスト後にフック実行
}
}
load_balancer {
target_group_arn = aws_lb_target_group.blue.arn
container_name = var.service_name
container_port = 8080
advanced_configuration {
alternate_target_group_arn = aws_lb_target_group.green.arn
production_listener_rule = aws_lb_listener_rule.production.arn
test_listener_rule = aws_lb_listener_rule.test.arn
role_arn = aws_iam_role.ecs_blue_green.arn
}
}
}
B/Gデプロイを構成する上で主要な属性を説明します。
deployment_configuration {
strategy = "BLUE_GREEN"
bake_time_in_minutes = 5
項目 | 説明 |
---|---|
strategy |
B/Gデプロイをする場合は、"BLUE_GREEN" を指定 |
bake_time_in_minutes |
ベイクタイム(本番トラフィックがグリーン環境にシフトしてからブルー環境が削除されるまでの期間)を分単位で指定 |
lifecycle_hook {
hook_target_arn = aws_lambda_function.health_check_test.arn
role_arn = aws_iam_role.ecs_blue_green.arn
lifecycle_stages = ["POST_TEST_TRAFFIC_SHIFT"]
}
項目 | 説明 |
---|---|
hook_target_arn |
指定したライフサイクルステージでトリガーするターゲットを指定 |
lifecycle_stages |
デプロイ中のどのライフサイクルステージでターゲットをトリガーするかを指定 |
今回はPOST_TEST_TRAFFIC_SHIFT
というステージでLambda関数をトリガーすることにしました。
load_balancer {
target_group_arn = aws_lb_target_group.blue.arn
container_name = var.service_name
container_port = 8080
advanced_configuration {
alternate_target_group_arn = aws_lb_target_group.green.arn
production_listener_rule = aws_lb_listener_rule.production.arn
test_listener_rule = aws_lb_listener_rule.test.arn
role_arn = aws_iam_role.ecs_blue_green.arn
}
}
項目 | 説明 |
---|---|
target_group_arn |
ブルー環境のターゲットグループのARN |
alternate_target_group_arn |
グリーン環境のターゲットグループのARN |
production_listener_rule |
プロダクションリスナールールのARN |
test_listener_rule |
テストリスナールールのARN |
参考
4. Lifecycle Hook (Lambda関数)
ECSサービスのlifecycle_hookで指定したLambda関数のコードです。
def lambda_handler(event, context):
health_check_url = urljoin(TEST_ENDPOINT_URL, '/')
try:
response = http.request('GET', health_check_url, timeout=30.0)
return {'hookStatus': 'SUCCEEDED' if response.status == 200 else 'FAILED'}
except:
return {'hookStatus': 'FAILED'}
テストリスナーの/
へアクセスし、200
を受け取った場合は、ECSに'hookStatus': 'SUCCEEDED'
というレスポンスを返すシンプルなものにしました。
'hookStatus': 'SUCCEEDED'
を返すように構成することで、ECSにてB/Gデプロイが続行させることができ、
'hookStatus': 'FAILED'
を返すように構成することで、ECSにてB/Gデプロイがロールバックささせることができます。
5. Provider
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.4.0"
}
}
}
AWSプロバイダのバージョンを6.4に上げるのをお忘れなく。
動作確認(カスタムテストが成功した場合)
デプロイ開始前の状態
プロダクションリスナールール
テストリスナールール
タスクの状態
どちらのリスナールールもブルー環境のターゲットグループに転送するようになっています。
B/Gデプロイの実行
下記のコマンドで強制的にデプロイを開始させます。
aws ecs update-service --cluster blue-green-demo --service blue-green-demo --force-new-deployment
↑グリーンタスクのプロビジョニングが始まりました。
↑デプロイステージがテストトラフィック移行
に切り替わりました。
↑テストリスナー(:8080
)のがグリーン環境へ転送するように変更されました!
↑プロダクションリスナー(:80
)はこの時点ではブルー環境へ転送するままです。
↑デプロイステージが本番トラフィック移行
に切り替わりました。
↑プロダクションリスナー(:80
)のトラフィック移行が完了しました。
↑プロダクションリスナーのトラフィック移行が完了し、無事ベイクタイムに入りました。
↑5分後、ベイクタイムが完了しました。
(期間はTerraformのbake_time_in_minutes
プロパティで制御可能です)
↑ベイクタイムが完了したため、ブルータスクが削除され、グリーンタスクのみになりました。
動作確認(カスタムテストが失敗した場合)
Lambda関数によるカスタムテストをわざと失敗させ、ロールバックされる様子も見てみましょう。
↑Ginのコードを変更し、/
へのGETで500
を返すようにします。
↑Lambda関数は上記の通りなので、ECSはLambda関数からFAILED
を受け取るようになるはずです。
その後、ECRにイメージをPushしタスク定義を更新します。(手順は割愛します)
aws ecs update-service --cluster blue-green-demo --service blue-green-demo
↑再度デプロイを始めます。(今回はタスク定義を更新したため、--force-new-deployment
は不要です)
↑今回も同様にテストトラフィック移行までは通常通りデプロイが進行します。
↑グリーンタスク(500
を返すバグタスク)が起動されました。
↑テストリスナー(:8080
)のトラフィックがブルーに切り替わりました。
(先ほど、ブルーからグリーンにトラフィックが移行したので、今回は逆向きになります)
↑デプロイステージがテストトラフィック移行後
になりました。
このタイミングでLambda関数が起動するので、テストが失敗するはずです。
↑狙い通り、テストが失敗しロールバックが開始されました。
各ステージの意味
ECSのB/Gデプロイは下記のデプロイステージがあり、表の✅️で示したステージでライフサイクルフックを利用することが可能です。
これにより例えば「テストトラフィックだけ移行している状態でテストリスナーに対してLambda関数を使ったテストをしたい!」といった要件に対応することができます。
(参考)
感想
「段階的にトラフィックをシフトさせたい」「テストトラフィックの移行段階でデプロイを一時停止したい」などの要件がある場合はCodeDeployが必要になりますが、CodeDeployを使わずに簡単にB/Gデプロイを行うことが構成できるようになったことでよりECSが使いやすくなるアップデートだと感じました。
ベイクタイムを長めに取れば、ブルータスクもしばらく並行稼働させられるので、グリーン環境で
障害発生した場合でもすばやく回復ができるのは大変ありがたいです。
※技術的に誤りがあればご指摘いただけますと幸いです。