ECS の組み込み Blue / Green デプロイを試してみた

記事タイトルとURLをコピーする

こんにちは😸
カスタマーサクセス部の山本です。

前置き

ECS の組み込み Blue / Green デプロイが 2025 年 7 月 17 日にリリースになっています。
aws.amazon.com

公式ドキュメント(英語のみ):
Amazon ECS blue/green service deployments workflow - Amazon Elastic Container Service

試してみて、今後どういったユースケースがあるのか確認してみることにしました。

AWS マネジメントコンソールで確認

AWS マネジメントコンソールでのサービス作成時に以下の画面が新しくなっています。

新しい画面

 
 
アップデート前の画面  
 
CodeDeploy を使用する Blue/Green デプロイ がコンソール上から選択できなくなり、ECS 単体での Blue/Green が選択できるようになったようです。 また、AWS CLI や Terraform では引き続き CodeDeploy を選べますが、ECS 単体の Blue/Green デプロイが推奨となったようです。

We recommend that you use the Amazon ECS blue/green deployment. For more information, see Creating an Amazon ECS blue/green deployment .
和訳:Amazon ECS Blue/Greenデプロイメントを使用することをお勧めします。詳細は 「Amazon ECS Blue/Greenデプロイメントの作成」を参照ください。

参考:CodeDeploy blue/green deployments for Amazon ECS - Amazon Elastic Container Service

ベイク時間

本番の通信を新しい Green 環境に移行した後に、もともとの Blue 環境を削除するまでの猶予時間とのこと。最小: 0 分、最大: 1,440 分、つまり最大 24 時間、もともとの環境に切り戻しができるようです。

トラフィック移行

本番の通信を新しい Green 環境に移行する際のオプションのようです。
「すべてのトラフィックを更新済みの Amazon ECS コンテナに一度に移行」のみ選択可能なようです。
本番の通信を 10% ずつ新しい環境に移行といったことはできないようです
CodeDeploy での Blue/Green デプロイでは選択できていたので、要注意かなと思います。

デプロイライフサイクルフック - オプション

デプロイ中の適当なタイミングで Lambda を実行し、成否によって本番の通信を新しい Green 環境に移行するかを決めることができます。
失敗の場合はロールバックします。

タイミングは以下の通り。
Lambda は各タイミングにそれぞれ追加できるようです。

  • スケールアップ前(PRE_SCALE_UP): グリーンサービスのリビジョン開始前

    • デプロイ前の検証に使用します。
  • スケールアップ後(POST_SCALE_UP): グリーンサービスのリビジョンが開始されたが、トラフィックがルーティングされる前

    • 基本的な正常性チェックに使用します。
  • テストトラフィック移行(TEST_TRAFFIC_SHIFT): テストトラフィックがグリーンサービスのリビジョンへのルーティングを開始

    • 切り替え時の機能テストに使用します。
  • テストトラフィック移行後(POST_TEST_TRAFFIC_SHIFT): テストトラフィックがグリーンサービスのリビジョンにルーティングされた後

    • パフォーマンス検証に使用します。
  • 本番トラフィック移行(PRODUCTION_TRAFFIC_SHIFT): 本番トラフィックがグリーンサービスのリビジョンへのシフトを開始

    • 最終検証に使用します。
  • 本番トラフィック移行後(POST_PRODUCTION_TRAFFIC_SHIFT): 本番トラフィックがグリーンサービスのリビジョンにシフトした後

    • デプロイ後の検証に使用します。

※「サービス調整(Reconcile_Service)」という選択肢もあるのですが、説明が見つかりませんでした。

「テストトラフィック」とは、特定のリクエストのみを新しいGreen環境に転送することです。 例として ALB の場合はテスト用リスナーに指定したパス (例:/test)への通信のみを新しい Green 環境に転送します。 1111 が既存の本番環境のターゲットグループ、 2222 が新しい環境です。

Lambda によるテスト機能を使用する前提

ECS サービスが Lambda を実行するために IAM ロールを作成し指定することが必要になっています。

  • 許可ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lambda:InvokeFunction"
            ],
            "Resource": "arn:aws:lambda:*:*:function:ECS-*"
        },
        {
            "Effect": "Allow",
            "Action": ["iam:PassRole","lambda:InvokeFunction"],
            "Resource": "arn:aws:lambda:*:*:function:ECS-*"
        }
    ]
}
  • 信頼ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "ecs.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

許可ポリシーから分かるように、Lambda 関数名は ECS-* にしないと動きません。

参考:Permissions required for Lambda functions in Amazon ECS blue/green deployments - Amazon Elastic Container Service

Lambda 関数では hookStatus を返却する必要がある

Lambda 関数では、hookStatus を返却する必要があります。

  • SUCCEEDED
    • 次に進みます。
  • FAILED
    • ロールバックします。
  • IN_PROGRESS
    • 30 秒後に Lambda 関数を再試行します。
        # レスポンスに OK ステータスコード (200~299 の範囲) が含まれているかどうかを確認します
        if 200 <= response.status < 300:
            logger.info("File upload test passed - received OK status code")
            return {
                "hookStatus": "SUCCEEDED"
            }
        else:
            logger.error(f"File upload test failed - status code: {response.status}")
            return {
                "hookStatus": "FAILED"
            }
            
    except Exception as error:
        logger.error(f"File upload test failed: {str(error)}")
        return {
            "hookStatus": "FAILED"
        }

Service Connect

テストトラフィックについて、指定したヘッダ名と値を持つ通信を切り替え前のグリーン環境に転送できます。

ELB (ALB/NLB)

以前は、ELB 設定用の専用ロールは必要ありませんでした。 しかし、CodeDeploy を使用せずに ECS 単体で Blue/Green デプロイを実行する場合、ECS サービスが ELB のリスナー設定を変更するには、特定の IAM ロールが必要のようです。

  • 許可ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ELBReadOperations",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetHealth"
            ],
            "Resource": "*"
        },
        {
            "Sid": "TargetGroupOperations",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:DeregisterTargets"
            ],
            "Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*"
        },
        {
            "Sid": "ALBModifyListeners",
            "Effect": "Allow",
            "Action": "elasticloadbalancing:ModifyListener",
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*"
            ]
        },
        {
            "Sid": "NLBModifyListeners",
            "Effect": "Allow",
            "Action": "elasticloadbalancing:ModifyListener",
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*"
            ]
        },
        {
            "Sid": "ALBModifyRules",
            "Effect": "Allow",
            "Action": "elasticloadbalancing:ModifyRule",
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*/*"
            ]
        }
    ]
}
  • 信頼ポリシー
{
  "Version": "2012-10-17", 
  "Statement": [ 
    {
      "Sid": "AllowAccessToECSForInfrastructureManagement", 
      "Effect": "Allow", 
      "Principal": {
        "Service": "ecs.amazonaws.com" 
      }, 
      "Action": "sts:AssumeRole" 
    } 
  ] 
}

参考:Amazon ECS infrastructure IAM role for load balancers - Amazon Elastic Container Service

テスト用のリスナー

リスナーを作成する際にテスト用のリスナーを作成できるようになっています。

テスト用のリスナーはパスを指定するようになっています。

  • 本番トラフィック /
  • テストトラフィック /test

のような形で指定可能です。

「テストトラフィック移行後」にテスト用のリスナーに指定したパス (例:/test)への通信のみを新しい Green 環境に転送します。

公式ドキュメントを確認

公式ドキュメント(英語のみ):
Amazon ECS blue/green service deployments workflow - Amazon Elastic Container Service

以下のフェーズがあるようです。

準備フェーズ

  • 既存のブルー環境と並行してグリーン環境を構築
  • 新しいサービスリビジョンのプロビジョニング
  • ターゲットグループの準備

デプロイフェーズ

  • 新しいサービスリビジョンをグリーン環境にデプロイ
  • Amazon ECS が更新されたサービスリビジョンを使用して新しいタスクを起動
  • ブルー環境は本番環境トラフィックの処理を継続

テストフェーズ

  • テストトラフィックルーティングを使用してグリーン環境を検証
  • Application Load Balancer がテストリクエストをグリーン環境に転送
  • 本番環境トラフィックはブルー環境に維持

トラフィック移行フェーズ

  • 構成済みのデプロイメント戦略に基づいて、本番環境トラフィックをブルーからグリーンに移行
  • 監視と検証のチェックポイントを含む

監視フェーズ

  • ベイク期間中のアプリケーション監視
    • 健全性の確認
    • パフォーマンス指標の追跡
    • アラーム状態の監視
  • 問題検出時にロールバック操作を開始

完了フェーズ

  • 構成に応じて以下のいずれかを実施
    • ブルー環境の終了
    • 潜在的なロールバック シナリオのためにブルー環境を維持

実際にやってみた (ALB 編)

新しいタスク定義を作成し、サービスを更新してみました。
まず、Lambda は 1 つもない状態で実行してみました。

Lambda なしでの動作確認

新しいタスク定義を作成し、サービスを更新してみます。

「デプロイ」タブを見てみましょう。
「進行中」状態となり「スケールアップ」しています。

ブラウザを更新すると、「テストトラフィック移行後」になりました。

ELB のリスナーを見ると、テスト用リスナーに指定したパス (例:/test)への通信のみが新しい Green 環境に切り替わっていました。 もともとは 1111 のターゲットグループに向いていた通信が、2222 に切り替わっています。

ブラウザを更新すると、「本番トラフィック移行」になりました。

ブラウザを更新すると、「ベイク時間」になりました。

ELB のリスナーを見ると、本番の通信が新しい Green 環境に切り替わっていました。
もともとは 1111 のターゲットグループに向いていた通信が、2222 に切り替わっています。

ベイク時間に設定した時間が過ぎると、ステータスは「成功」になっていました。

なお、ベイク時間中は 2 つのターゲットグループ両方に ECS コンテナがターゲット登録されていました。

CodeDeploy を用いた Blue/Green デプロイとの違い

CodeDeploy を用いた Blue/Green デプロイでは待機時間を設定して、ユーザーがボタンを押したタイミングで新しい Green 環境への通信切り替えができたと思います。
ELB にテスト用のリスナールールを作って、通信切り替え前に新しい Green 環境を手動でテストするケースもありました。
今回検証している 新しい ECS 単体での Blue / Green では、「デプロイライフサイクルフック」に指定した Lambda 関数を使ってテストする仕組みになっています。 そのため、Lambda を設定しない場合は、本番の通信を新しい環境に切り替えるまでの待機時間を設定できません。

Lambda ありでの動作確認

Lambda を VPC 内に配置し、NAT Gateway を経由して ALB とその先の ECS タスクに通信する構成にしてみました。

  • Lambda は VPC の非公開サブネットに配置
  • Lambda からのインターネット通信は NAT Gateway 経由
  • ALB のセキュリティグループは NAT Gateway のパブリック IP からの通信を許可

Lambda のコードは以下のようにします。

import json
import urllib3
import logging
import os

# ログ記録を設定します
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

# HTTP クライアントを初期化します
http = urllib3.PoolManager(timeout=urllib3.Timeout(connect=5.0, read=10.0))

def lambda_handler(event, context):
    """
    Validation hook that tests the green environment by accessing root URL
    """
    logger.info(f"Event: {json.dumps(event)}")
    logger.info(f"Context: {context}")
    
    try:
        # 実際のシナリオでは、テストエンドポイント URL を作成します
        test_endpoint = os.getenv("APP_URL")
        
        if not test_endpoint:
            logger.error("APP_URL environment variable is not set")
            return {
                "hookStatus": "FAILED",
                "error": "Missing APP_URL configuration"
            }
        
        # ルートエンドポイントに GET リクエストを送信します
        try:
            response = http.request(
                'GET',
                test_endpoint,
                retries=urllib3.Retry(total=1, backoff_factor=0.1)
            )
            
            logger.info(f"GET / response status: {response.status}")
            
            # レスポンスに OK ステータスコード (200~299 の範囲) が含まれているかどうかを確認します
            if 200 <= response.status < 300:
                logger.info("Root URL access test passed - received OK status code")
                return {
                    "hookStatus": "SUCCEEDED",
                    "status": response.status
                }
            else:
                logger.error(f"Root URL access test failed - status code: {response.status}")
                return {
                    "hookStatus": "FAILED",
                    "status": response.status
                }
        
        except urllib3.exceptions.HTTPError as http_error:
            logger.error(f"HTTP connection error: {str(http_error)}")
            return {
                "hookStatus": "FAILED",
                "error": "HTTP connection error"
            }
        
    except Exception as error:
        logger.error(f"Root URL access test failed: {str(error)}")
        return {
            "hookStatus": "FAILED",
            "error": str(error)
        }

この Lambda 関数は、環境変数 APP_URL で指定された URL(例:https://youkoso.karukozaka46.click/test/)に対して接続テストを実行します。

  1. 環境変数 APP_URL から接続先 URL を取得します。
  2. その URL に GET リクエストを送信し、レスポンスのステータスコードを確認します。
  3. ステータスコードに応じて、以下のように hookStatus を返します:
    • 200~299 の正常なステータスコード:「SUCCEEDED」
    • それ以外のステータスコード:「FAILED」

関数名は ECS-* の命名規則に従っており、ECS サービスから実行されることを想定しています。

デプロイ完了後に Lambda のコンソールからテスト実行してみると/test には何も配置していないので、環境変数を変更するとFAILED になります

※ return 文なのでFAILED であっても Lambda 関数としては成功状態になります。

Lambda が FAILED になる状態で、ECS サービスを更新

Lambda が FAILED になる状態で、ECS サービスを更新します。
Lambda 関数、IAM ロール、ライフサイクルステージ(実行するタイミング)を指定します。

「テストトラフィック移行後」に設定し、/test への通信が新しい Green 環境に向いたときに Lambda を実行します。

タスクは同じバージョンを使うので、「新しいデプロイの強制」を使用して、入れ替えるようにサービスを更新します。

「デプロイ」タブを見てみましょう。
「進行中」状態となり「スケールアップ」しています。

「スケールアップ後」になりました。

「テストトラフィック移行」になりました。

「テストトラフィック移行後」になりました。ロールバックするでしょうか。

「ロールバックが進行中」になりました。

「タスク」タブを見てみると、新しい方のタスクが非アクティブ化しています。

「ロールバックが成功」になりました。

Lambda が SUCCEEDED になる状態で、ECS サービスを更新

Lambda が SUCCEEDED になる状態にしました。

デプロイは成功しました。詳細は割愛します。

Lambda が IN_PROGRESS になる状態で、ECS サービスを更新

  • IN_PROGRESS
    • 30 秒後に Lambda 関数を再試行します。

ソースコードの SUCCEEDEDIN_PROGRESS に差し替えて動作確認してみます。
Lambda の Deploy ボタン押し忘れないようにしましょう。

Deploy ボタン

ECS サービスの更新をすると、「テストトラフィック移行後」で止まっていました。

そして CloudWatch Logs を見ると、Lambda が 30 秒ごとに実行されているログがありました。

先日発表された新機能「ロールバック」が活躍するときですね。

ELBのターゲットグループから新しいタスクが登録解除されていきます。

そのあと、ECS タスクが非アクティブ化されています。

最後にロールバックが成功しました。

CodeDeploy のリソースがないことを確認

CodeDeploy には何も表示がなく、関連付いたリソースも作成されていませんでした。

最後に、次の記事で Service Connect 使用時の動作と、ALB が internal のパターンに触れようと思っています。

次の記事で Service Connect 使用時の動作と、ALB が internal のパターンに触れようと思っています。

まとめ

ECS の組み込み Blue/Green デプロイ機能

  • 2025年7月17日に、ECS の組み込み Blue/Green デプロイ機能がリリース
  • AWS マネジメントコンソールのインターフェースが更新
  • CodeDeploy を使用する Blue/Green デプロイから、ECS 単体での Blue/Green デプロイへ推奨が変更

デプロイメントの特徴

ベイク時間

  • 本番通信を新しい Green 環境に移行後、元の Blue 環境を削除するまでの猶予時間
  • 最小: 0 分、最大: 1,440 分(24時間)
  • ロールバックの柔軟性を提供

トラフィック移行

  • 「すべてのトラフィックを更新済みの Amazon ECS コンテナに一度に移行」のみ可能
  • 段階的なトラフィック移行(例: 10%ずつ)はサポートされない

デプロイライフサイクルフック

デプロイの各段階で Lambda 関数を実行可能

  • スケールアップ前/後
  • テストトラフィック移行前/後
  • 本番トラフィック移行前/後

Lambda 関数の制約

  • 関数名は ECS-* の命名規則に従う必要がある(ポリシーの記載次第ではある)
  • hookStatus を返却する必要がある:
    • SUCCEEDED: 次に進む
    • FAILED: ロールバック
    • IN_PROGRESS: 30秒後に再試行

Service Connect

  • 特定のヘッダを持つ通信を切り替え前の環境に転送可能

ELB 設定

  • ECS サービスが ELB のリスナー設定を変更するには、専用の IAM ロールが必要
  • テスト用のリスナーをパス(例:/test)を設定して作成可能

デプロイメントのフェーズ

  1. 準備フェーズ: 新しい Green 環境の構築
  2. デプロイフェーズ: 新しいサービスリビジョンのデプロイ
  3. テストフェーズ: テストトラフィックによる検証
  4. トラフィック移行フェーズ: 本番トラフィックの切り替え
  5. 監視フェーズ: アプリケーションの健全性確認
  6. 完了フェーズ: Blue 環境の削除

注意点

  • CodeDeploy を使用した従来の Blue/Green デプロイと比較して、手動での待機時間設定や段階的なトラフィック移行に制限がある
  • Lambda 関数を使用することで、デプロイプロセスの柔軟な検証が可能
  • IAM ロールと権限設定に注意を払う

  • ローリングアップデートからBlue/Green への変更が可能

余談

久しぶりに甲斐駒ヶ岳にいってきました。気持ちよかったです。

山本 哲也 (記事一覧)

カスタマーサクセス部のインフラエンジニア。

山を走るのが趣味です。