🕹️

B/Gデプロイ可能なALB+ECSの構成をTerraformで実装

ECSのB/Gデプロイとライフサイクルフックを活用したカスタムテスト

これまでECSはローリングアップデートのみが利用可能であり、B/Gデプロイを行う場合は、CodeDeployを別途作成する必要がありましたが、2025/07/17のアップデートにて、アプリケーション ロード バランサー(ALB)、ネットワーク ロード バランサー(NLB)、または ECS サービス コネクトからのトラフィックにサービスを提供するECSサービスに対してB/Gデプロイ戦略を取ることが可能になりました。

https://aws.amazon.com/jp/about-aws/whats-new/2025/07/amazon-ecs-built-in-blue-green-deployments/

今回はALB+ECSの構成でECSのB/Gデプロイとライフサイクルフックを活用したカスタムテストの動作確認ができるTerraformを作成してみました。

https://github.com/Maxakali517/ecs-blue-green-deploy-demo

インフラ構成

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つのリソースが必要です。

  • プロダクションリスナー
  • プロダクションリスナー用カスタムルール
  • テストリスナー
  • テストリスナー用カスタムルール
  • ターゲットグループ(グリーン環境用)
  • ターゲットグループ(ブルー環境用)

image.png

リスナー
# 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

参考

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service

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デプロイがロールバックささせることができます。

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-lifecycle-hooks.html

5. Provider

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.4.0"
    }
  }
}

AWSプロバイダのバージョンを6.4に上げるのをお忘れなく。

動作確認(カスタムテストが成功した場合)

デプロイ開始前の状態

プロダクションリスナールール
image.png
テストリスナールール
image.png
タスクの状態
image.png
どちらのリスナールールもブルー環境のターゲットグループに転送するようになっています。

B/Gデプロイの実行

下記のコマンドで強制的にデプロイを開始させます。

aws ecs update-service --cluster blue-green-demo --service blue-green-demo --force-new-deployment

image.png
↑グリーンタスクのプロビジョニングが始まりました。
image.png
↑デプロイステージがテストトラフィック移行に切り替わりました。
image.png
↑テストリスナー(:8080)のがグリーン環境へ転送するように変更されました!
image.png
↑プロダクションリスナー(:80)はこの時点ではブルー環境へ転送するままです。
image.png
↑デプロイステージが本番トラフィック移行に切り替わりました。
image.png
↑プロダクションリスナー(:80)のトラフィック移行が完了しました。
image.png
↑プロダクションリスナーのトラフィック移行が完了し、無事ベイクタイムに入りました。
image.png
↑5分後、ベイクタイムが完了しました。
(期間はTerraformのbake_time_in_minutesプロパティで制御可能です)
image.png
↑ベイクタイムが完了したため、ブルータスクが削除され、グリーンタスクのみになりました。

動作確認(カスタムテストが失敗した場合)

Lambda関数によるカスタムテストをわざと失敗させ、ロールバックされる様子も見てみましょう。
image.png
↑Ginのコードを変更し、/へのGETで500を返すようにします。
image.png
↑Lambda関数は上記の通りなので、ECSはLambda関数からFAILEDを受け取るようになるはずです。
その後、ECRにイメージをPushしタスク定義を更新します。(手順は割愛します)

aws ecs update-service --cluster blue-green-demo --service blue-green-demo

↑再度デプロイを始めます。(今回はタスク定義を更新したため、--force-new-deploymentは不要です)
image.png
↑今回も同様にテストトラフィック移行までは通常通りデプロイが進行します。
image.png
↑グリーンタスク(500を返すバグタスク)が起動されました。
image.png
↑テストリスナー(:8080)のトラフィックがブルーに切り替わりました。
(先ほど、ブルーからグリーンにトラフィックが移行したので、今回は逆向きになります)

image.png
↑デプロイステージがテストトラフィック移行後になりました。
このタイミングでLambda関数が起動するので、テストが失敗するはずです。
image.png
↑狙い通り、テストが失敗しロールバックが開始されました。

各ステージの意味

ECSのB/Gデプロイは下記のデプロイステージがあり、表の✅️で示したステージでライフサイクルフックを利用することが可能です。
これにより例えば「テストトラフィックだけ移行している状態でテストリスナーに対してLambda関数を使ったテストをしたい!」といった要件に対応することができます。

ステージ 説明 ライフサイクルフック
(B/Gデプロイ前)
image.png
RECONCILE_SERVICE 複数のアクティブなサービスリビジョンがある場合の調整 ✅️
PRE_SCALE_UP Green環境の起動前 ✅️
SCALE_UP Green環境のスケールアップ中
image.png
❌️
POST_SCALE_UP Green環境の起動後 ✅️
TEST_TRAFFIC_SHIFT テストトラフィックの移行中
image.png
✅️
POST_TEST_TRAFFIC_SHIFT テストトラフィック移行完了後 ✅️
PRODUCTION_TRAFFIC_SHIFT 本番トラフィックの移行中
image.png
✅️
POST_PRODUCTION_TRAFFIC_SHIFT 本番トラフィック移行完了後 ✅️
BAKE_TIME 本番トラフィック以降後にブルー環境を削除するまでの期間 ❌️
CLEAN_UP Blue環境のクリーンアップ
image.png
❌️

(参考)

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/blue-green-deployment-how-it-works.html#blue-green-deployment-stages

感想

「段階的にトラフィックをシフトさせたい」「テストトラフィックの移行段階でデプロイを一時停止したい」などの要件がある場合はCodeDeployが必要になりますが、CodeDeployを使わずに簡単にB/Gデプロイを行うことが構成できるようになったことでよりECSが使いやすくなるアップデートだと感じました。
ベイクタイムを長めに取れば、ブルータスクもしばらく並行稼働させられるので、グリーン環境で
障害発生した場合でもすばやく回復ができるのは大変ありがたいです。

※技術的に誤りがあればご指摘いただけますと幸いです。

Discussion