Blue/Greenデプロイメントを構築しよう

このハンズオンでは、Amazon ECSのネイティブBlue/Greenデプロイメント機能を使って、安全なデプロイの仕組みを実際にハンズオン形式で手を動かしながら体験します。

  • ALBとターゲットグループの構成(本番ルール + テストルール)
  • ECSサービスのBlue/Green設定
  • Blue/Green切り替えの動作確認
  • デプロイ後の手動ロールバック体験
  • Lambdaライフサイクルフックによる自動テストとロールバック
  • 自動テスト + 手動承認を組み合わせた重要システム向けのフロー

1. 事前準備

このハンズオンでは、以下の講座の内容を前提としています。まだ修了していない場合は、先にこちらを修了してください。

Gitの基礎知識を習得していることを前提とします。自信のない方は先に以下の講座を実施してください。

また、以下のツールやアカウントが必要です。まだ準備できていない場合は、リンク先の手順に沿って準備をお願いします。

2. ハンズオンの概要

デプロイタイプの講座で、Blue/Greenデプロイメントの概要について学びました。このハンズオンでは、Amazon ECSが提供するネイティブのBlue/Greenデプロイメント機能を使って、実際にBlue/Greenデプロイを体験します。

2.1 Blue/Greenデプロイメントの復習

Blue/Greenデプロイメントは、現行バージョン(Blue)と新バージョン(Green)の2つの環境を用意し、Green環境で動作確認を行ってから本番トラフィックを切り替える方法です。切り替え後に問題が発生した場合は、すぐにBlue環境に戻せるため、安全にリリースを行えます。

2.2 ECSネイティブBlue/Greenの仕組み

Blue/Greenデプロイメントを実現する方法は複数あります。たとえばALBのリスナールールを手動で切り替える、AWS CodeDeployを使う、Route53の重み付きルーティングで切り替えるなどの方法があり、それぞれ実装の手間や運用負荷が異なります。

このハンズオンでは、ECSが標準で提供するネイティブのBlue/Greenデプロイ機能を使います。理由は次のとおりです。

  • 以前はECSでBlue/Greenを実現するためにAWS CodeDeployが必須でしたが、ECSネイティブ機能ではCodeDeployなしで完結する
  • ECSサービス側でデプロイ戦略を指定するだけで、ALBターゲットグループの切り替え・ベイクタイム・ロールバックが自動的に行われる
  • 2025年7月にECSネイティブBlue/Greenが正式に提供開始され、現在はAWSが推奨する最新の方法となっている(Amazon ECS enables built-in blue/green deployments(AWS公式アナウンス)

それでは、ECSネイティブBlue/Greenの具体的な流れを見ていきましょう。デプロイは以下のステップで進みます。

  1. ECSが新しいタスク(Green)を起動し、ALBのテスト用ターゲットグループに登録する
  2. Green環境のヘルスチェックが通ると、ALBの本番ルールのトラフィックがBlue → Greenに切り替わる
  3. 一定時間、BlueとGreenの両方を維持し、問題があればロールバック可能な状態を保つ
  4. ベイクタイムが経過し問題がなければ、Blue環境のタスクを停止する

この一連の流れをライフサイクルステージと呼び、後のセクションでは各ステージにLambda関数を設定することで、デプロイの途中に自動テストを挟む仕組みも体験します。詳細な仕様はAmazon ECS blue/green deployments(AWS公式ドキュメント)に記載があります。

2.3 ハンズオンの進め方

今回のハンズオンは、学習しやすさを重視して以下の4段階で進めます。いきなり完成形を構築するのではなく、まず最小構成で基本動作を理解し、そこに必要な機能を順番に積み上げていく流れです。

フェーズ1:基本のBlue/Greenデプロイを構築

最初のフェーズでは、最もシンプルな構成でBlue/Greenデプロイの基本動作を体験します。具体的には、Green環境が起動 → 本番切り替え → ベイクタイム → Blue終了という一連の流れを、自分の目で確認します。

このフェーズを通して、ECSがBlue/Greenデプロイを実行する際にどんなイベントが発生し、どのタイミングで何が起こるのかを把握できます。これがBlue/Greenデプロイの土台になります。

フェーズ2:手動ロールバックの体験

次のフェーズでは、ベイクタイム中にECSコンソールからデプロイを手動で停止し、Blue環境に戻す操作を体験します。

Blue/Greenデプロイの大きなメリットは「問題があればすぐ戻せる」点ですが、実際にロールバックを操作してみないとその安心感は実感できません。このフェーズでは、本番切り替え後に問題が見つかった場面を想定し、手動でBlueに戻す流れを体験します。

フェーズ3:自動テストの追加

フェーズ3では、Lambda関数をライフサイクルフックとして追加し、自動テストと自動ロールバックまでを体験します。

ここで初登場の「ライフサイクルフック」について簡単に補足します。先ほど紹介したライフサイクルステージ(Green起動・本番切り替え・ベイクタイムなど)の各ステージの前後にLambda関数を割り込ませる仕組みのことを、ライフサイクルフックと呼びます。たとえば「テストルールがGreenに切り替わったタイミングでLambdaを実行して自動テストを行い、問題があれば本番切り替えの前にデプロイを止める」といった処理を組み込めます。

フェーズ2で「手動でロールバックできる」ことは確認できますが、毎回人間が監視するのは現実的ではありません。本番リリースは深夜や休日にも行われるため、人間の判断を介さずに自動でテスト・ロールバックする仕組みが必要になります。Lambdaをライフサイクルフックに組み込むことで、Green環境への自動テストと、テスト失敗時の自動ロールバックを実現できます。

フェーズ4:手動承認の組み込み

最後のフェーズでは、フェーズ3のLambdaをさらに拡張し、自動テストの合格に加えて人間の手動承認が揃わないと本番切り替えが進まない仕組みを構築します。

システムの重要度が高い場合や、リリース頻度が低い基幹システムでは、自動テストだけでなく人間の目による最終確認が求められることがあります。たとえば「画面の見た目や操作感は自動テストでは判定できない」「コンプライアンス上、承認記録を残す義務がある」といったケースです。

このフェーズでは、DynamoDBに承認レコードを管理する方式で、「DynamoDBコンソールでステータスを書き換え = 承認する」というシンプルなハンズオンを体験します。実業務で使われる様々な承認実装(Slackボタン・メール承認・専用管理画面など)の基礎となる仕組みを学べます。

このように段階的に進めることで、各機能の必要性を実感しながら学習できます。「最初から全部入りで作る」のではなく、「シンプルな構成で動かしてみて、足りないところを順に補っていく」アプローチは、実務でも複雑な仕組みを理解するときに役立つ進め方です。

💡 ポイント
本ハンズオンではデプロイ操作をすべて手動で行います。実務ではコンテナのビルドとデプロイを自動化しようで学んだようにGitHub Actionsと組み合わせて自動化するのが一般的ですが、ここではBlue/Greenデプロイメントの仕組み自体に集中するため、手動デプロイで進めます。

3. 環境構築

ハンズオンを始める前に、サンプルアプリと、AWS上のベースリソース(VPC・サブネット・ECR・ECSクラスタ・IAMロール・DynamoDBテーブルなど)を用意しておきます。ここで作成する土台は、フェーズ1〜4の全体で使います。

3.1 サンプルアプリの準備

このハンズオンでは、FastAPIで作成した最小構成のAPIを使います。今回はBlue/Greenデプロイメントの仕組み自体に集中したいため、前の講座のようにGitHubリポジトリやGitHub Actionsは使わず、ローカルフォルダから直接ビルド・デプロイする形で進めます。

以下のボタンからサンプルコードのZIPファイルをダウンロードしてください。

ダウンロードしたZIPファイルを、任意の場所に解凍してください。解凍するとdevopscamp-bg-handsonという名前のフォルダができます。このフォルダの中身は以下のようになっています。

devopscamp-bg-handson/
├── Dockerfile
├── requirements.txt
└── main.py

各ファイルの役割を以下の表にまとめます。

ファイル名 役割
main.py FastAPIアプリケーションのエントリーポイント。ヘルスチェック用の/healthと、動作確認用の/の2つのエンドポイントだけを持つシンプルな構成
requirements.txt Pythonプロジェクトで利用するパッケージの一覧を記載したファイル
Dockerfile コンテナイメージを作成するための手順を記述した設定ファイル

main.pyの中身は以下のとおりです。後のハンズオンでこのファイルを書き換えて、Blue/Greenデプロイメントの動作を体験します。

from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health():
    return {"status": "ok"}


@app.get("/")
def root():
    return {"message": "API呼び出しテストに成功しました"}

3.2 AWS環境の構築

VPC、サブネット、セキュリティグループ、ECR、ECSクラスタ、IAMロールなどのベースリソースをCloudFormationで作成します。これらはBlue/Greenデプロイメントの本質ではないため、テンプレートで一括構築します。

📝 CloudFormationとは
CloudFormationは、AWSのリソースをテンプレート(YAML/JSON)で定義し、自動的に作成・管理できるサービスです。手動でリソースを一つずつ作成する代わりに、テンプレートをアップロードするだけで必要なリソースをまとめて構築できます。なお、このハンズオンではCloudFormationテンプレートの内容を理解する必要はありません。テンプレートをそのままアップロードして環境を構築してください。

CloudFormationで作成されるリソースは以下のとおりです。

リソース 名前 説明
VPC bg-handson-vpc 10.0.0.0/16のアドレス範囲を持つ仮想ネットワーク
パブリックサブネット1 bg-handson-public-subnet-1 ap-northeast-1aに配置するサブネット(10.0.1.0/24)
パブリックサブネット2 bg-handson-public-subnet-2 ap-northeast-1cに配置するサブネット(10.0.2.0/24)。ALBにマルチAZ構成が必須のため2つ作成
インターネットゲートウェイ bg-handson-igw パブリックサブネットからのインターネットアクセス用
ルートテーブル bg-handson-rt パブリックサブネット用のルーティング設定
ALBセキュリティグループ bg-handson-alb-sg ALB用(HTTP:80を許可)
ECSセキュリティグループ bg-handson-ecs-sg ECSタスク用(ALBからのHTTP:80を許可)
ECRリポジトリ bg-handson-repo コンテナイメージを格納するリポジトリ
ECSクラスター bg-handson-cluster ECSサービスを管理する論理グループ
タスク実行ロール bg-handson-task-execution-role ECSタスクがCloudWatch Logsなどを操作するためのIAMロール
ECSインフラロール bg-handson-ecs-infra-role ECSがBlue/Greenデプロイ中にALBやターゲットグループを操作するためのIAMロール
CloudWatch Logs /ecs/bg-handson-task ECSタスクのログ出力先
DynamoDBテーブル bg-handson-approvals フェーズ4で使用する承認レコード管理用のテーブル。パーティションキーはdeployment_id

テンプレートファイルの準備

まず、CloudFormationにアップロードするテンプレートファイルを用意します。以下のボタンからテンプレートファイルをダウンロードしてください。

ファイルがダウンロードできたら、次のステップに進みます。

CloudFormationスタックの作成

AWSマネジメントコンソールでCloudFormationのダッシュボードを開きます。検索バーに「CloudFormation」と入力し、サービスを選択してください。

CloudFormationのダッシュボードが表示されたら、スタックの作成 > **新しいリソースを使用(標準)**をクリックします。

ステップ1: テンプレートの指定

テンプレートの指定画面が表示されます。下記表に従い、設定を行ってください。

設定項目 設定の基準
テンプレートの準備 既存のテンプレートを選択 事前に作成したテンプレートファイルを使用するため
テンプレートソース テンプレートファイルのアップロード ローカルのテンプレートファイルを直接アップロードするため
テンプレートファイルのアップロード 先ほど保存したblue-green-handson.yml ハンズオン環境を構築するテンプレートを指定するため

設定後、次へをクリックしてください。

ステップ2: スタックの詳細を指定

スタックの詳細を指定する画面が表示されます。下記表に従い、設定を行ってください。

設定項目 設定の基準
スタック名 bg-handson スタックを識別するための名前

設定後、次へをクリックしてください。

ステップ3: スタックオプションの設定

スタックオプションの設定画面が表示されます。画面下部の「AWS CloudFormation によって IAM リソースがカスタム名で作成される場合があることを承認します。」にチェックを入れてから、次へをクリックしてください。

ステップ4: 確認と作成

確認画面が表示されます。設定内容を確認し、送信をクリックします。

スタックの作成完了を確認

スタックの作成が開始されます。ステータスがCREATE_COMPLETEに変わったら、環境構築は完了です。完了まで数分かかります。

スタックの出力タブを開くと、後の手順で使用するリソースのID・ARNが一覧で表示されます。このページは後から参照するため、開いたままにしておいてください。

3.3 初回イメージのプッシュ

CloudFormationでECRリポジトリが作成されたので、先ほど解凍したサンプルアプリをコンテナイメージとしてビルドし、ECRにプッシュしておきます。フェーズ1以降でECSサービスを作成する際、タスク定義はECR上のイメージを参照するため、環境構築の段階で初回イメージを用意しておきます。

devopscamp-bg-handsonフォルダでターミナルを開き、以下のコマンドを順に実行します。

ECRにログインします。このハンズオンは東京リージョン(ap-northeast-1)で進めます。<アカウントID>はご自身のAWSアカウントIDに置き換えてください。

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com

イメージをビルドします。リポジトリのルートにDockerfileがあるため、カレントディレクトリ(.)を指定してビルドします。

このハンズオンではFargateをX86_64アーキテクチャで動かすため、--platform linux/amd64を指定してビルドします。Apple シリコン(M1/M2/M3など)のMacではこれを指定しないとarm64のイメージが作成され、Fargate上でexec format errorとなりコンテナが起動しません。Intel MacやWindowsでも付けて問題ないため、常にこのオプションを付けて進めます。

docker build --platform linux/amd64 -t bg-handson-repo .

イメージにECRのURIタグを付けます。

docker tag bg-handson-repo:latest <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest

ECRにプッシュします。

docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest

ECRのbg-handson-repoリポジトリにlatestタグのイメージが登録されていれば、環境構築は完了です。

4. フェーズ1: 基本のBlue/Greenデプロイを構築

ここからはフェーズ1の実装に入ります。最もシンプルな構成でBlue/Greenデプロイの基本動作を体験し、Green環境が起動 → 本番切り替え → ベイクタイム → Blue終了という一連の流れを、自分の目で確認していきます。

4.1 構成の説明

まずフェーズ1では、ALBECSサービスだけのシンプルな構成から始めます。それぞれの役割を順に見ていきましょう。

ALB

ポート80のリスナーに本番ルールテストルールの2つのリスナールールを設定し、それぞれ別のターゲットグループに転送します。本番ルール(デフォルトルール)は現行バージョン(Blue)に、テストルール(特定のHTTPヘッダー付きリクエストのみマッチ)は新バージョン(Green)にトラフィックを流すことで、本番に影響を与えずにGreen環境を検証できる構成です。ECSのネイティブBlue/Greenデプロイメントでは、本番用・テスト用のリスナールールを同じリスナー内から選択する必要があるため、この構成を採用します。

ECSサービス

Blue/Greenデプロイメント戦略を設定したECSサービスを用意します。新しいタスク定義をデプロイすると、ECSが自動的にGreenタスクを起動し、ALBの本番ルールをBlueからGreenに切り替えてくれます。ターゲットグループの差し替えやベイクタイムの管理もECSが代行してくれるため、運用者はデプロイの結果を確認するだけで済みます。

graph LR
    A["ユーザ<br>(通常リクエスト)"] -->|"ポート80"| B["ALB<br>(本番ルール)"]
    E["検証担当<br>(X-Test-Traffic: trueヘッダー付き)"] -->|"ポート80"| F["ALB<br>(テストルール)"]
    B --> C["Blue TG<br>(現行バージョン)"]
    B -.->|"切り替え後"| D["Green TG<br>(新バージョン)"]
    F --> D

    style C fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32

4.2 ターゲットグループの作成

ここからはBlue/Greenデプロイメントの核心部分を手動で構築していきます。まずは、トラフィックの振り分け先となるターゲットグループを作成します。

Blue/Greenデプロイメントでは、現行バージョン(Blue)と新バージョン(Green)を別々のターゲットグループに分けて管理します。ALBがリスナーごとにどちらのターゲットグループにトラフィックを送るかを切り替えることで、安全な切り替えとロールバックを実現します。

ここでは、Blue用とGreen用の2つのターゲットグループを順に作成します。

Blue用ターゲットグループの作成

まず、現行バージョンを配置するBlue用のターゲットグループを作成します。

EC2のダッシュボードを開き、左側のメニューから「ターゲットグループ」を選択し、ターゲットグループの作成をクリックします。

以下の設定でBlue用のターゲットグループを作成します。なお、ターゲットタイプはIPを選びますが、これはFargateとロードバランサーを組み合わせる際の必須要件です(Architect for AWS Fargate for Amazon ECS(AWS公式ドキュメント)に「choose ip as the target type」と明記されています)。

設定項目 設定の基準
ターゲットの種類 IP アドレス FargateではタスクごとにIPが割り当てられるため
ターゲットグループ名 bg-handson-blue-tg Blue環境用のターゲットグループ
プロトコル / ポート HTTP / 8000 FastAPIがリッスンするポート
VPC bg-handson-vpc CloudFormationで作成したVPC
ヘルスチェックパス /health FastAPIのヘルスチェックエンドポイント

「ターゲットを登録」の画面では、ターゲットを追加せずにターゲットグループを作成をクリックします。ECSがタスクを起動する際に自動的にターゲットを登録するため、ここでは空のまま作成します。

ターゲットグループ一覧にbg-handson-blue-tgが作成されていれば、ここまでの操作は完了です。

Green用ターゲットグループの作成

続いて、新バージョン(デプロイ時に切り替わる先)を配置するGreen用のターゲットグループを作成します。Blueとは別のターゲットグループに分けることで、ECSがデプロイ時に新旧を区別してリスナーの向き先を切り替えられるようになります。

EC2のダッシュボードのターゲットグループ画面で、再度ターゲットグループの作成をクリックします。

以下の設定でGreen用のターゲットグループを作成します。Blue用と異なるのはターゲットグループ名のみで、それ以外の設定はBlue用と同じです。

設定項目 設定の基準
ターゲットの種類 IP アドレス FargateではタスクごとにIPが割り当てられるため
ターゲットグループ名 bg-handson-green-tg Green環境用のターゲットグループ
プロトコル / ポート HTTP / 8000 FastAPIがリッスンするポート
VPC bg-handson-vpc CloudFormationで作成したVPC
ヘルスチェックパス /health FastAPIのヘルスチェックエンドポイント

「ターゲットを登録」の画面では、Blue用と同様にターゲットを追加せずにターゲットグループを作成をクリックします。

ターゲットグループ一覧にbg-handson-blue-tgbg-handson-green-tgの2つが並んでいれば、ここまでの操作は完了です。

4.3 ALBの作成

続いて、トラフィックの振り分けを担うALBを構築します。ここではまずALB本体と、本番トラフィックを受け付けるポート80のリスナーを作成します。テスト用の振り分け経路は、ALB作成後に「リスナールール」として追加します。

ALB本体の設定

ターゲットグループの準備ができたので、まずはトラフィックを受け付けるALBの土台を作ります。ここでは、ALBの名前・配置するVPCとサブネット・アクセス制御のためのセキュリティグループといった基本情報を入力します。

EC2のダッシュボードから「ロードバランサー」を選択し、ロードバランサーの作成をクリックします。「Application Load Balancer」の作成を選択します。

以下の設定でALBを作成します。

設定項目 設定の基準
ロードバランサー名 bg-handson-alb ハンズオン用のALB
スキーム インターネット向け インターネットからアクセスできるようにするため
VPC bg-handson-vpc CloudFormationで作成したVPC
サブネット bg-handson-public-subnet-1, bg-handson-public-subnet-2 2つのAZにまたがるパブリックサブネット
セキュリティグループ bg-handson-alb-sg ポート80を許可するセキュリティグループ

画面上でALBの基本情報がすべて入力されていれば、次の手順に進めます。

リスナーの設定

ALBの土台ができたので、次にインターネットからのアクセスを受け付けるリスナーを設定します。ポート80で受けたトラフィックを初期状態としてBlueターゲットグループに振り分ける構成にします。

同じ作成画面の「リスナーとルーティング」セクションで、以下のように設定します。

設定項目 設定の基準
プロトコル HTTP 今回はHTTPで構成
ポート 80 本番トラフィック用
デフォルトアクション bg-handson-blue-tg に転送 初期状態ではBlue TGにトラフィックを送る

入力欄にポート80・Blueターゲットグループ転送の内容が反映されていれば、リスナーの設定は完了です。

ALBの作成と確認

リスナーの設定が終わったので、ALBを作成します。作成後に詳細画面で確認し、ポート80のリスナーが意図通りに作られていることを確かめます。

画面下部のロードバランサーの作成をクリックします。

ALB一覧にbg-handson-albが表示され、詳細画面のリスナーとルールタブでポート80のリスナーが確認できれば、ALBの作成は完了です。

4.4 テスト用リスナールールの追加

ALBの本番リスナーができたので、続けてテスト用のリスナールールを追加します。ECSのネイティブBlue/Greenデプロイメントでは、本番トラフィック用とテストトラフィック用の2つのリスナールールを同じリスナー内から選ぶ必要があります。そこで、ポート80リスナーに「特定のHTTPヘッダーが付いたリクエストのみGreenターゲットグループに転送する」というルールを1つ追加し、これをテスト用として利用します。

📝 リスナールールとは
ALBのリスナーは、1つの「デフォルトルール」と、任意で追加できる「優先度付きルール」を持ちます。リクエストが到着すると、優先度の高いルールから順に条件マッチが評価され、最初にマッチしたルールの転送先へトラフィックが流れます。どのルールにもマッチしなければデフォルトルールが適用されます。今回はヘッダーX-Test-Traffic: trueが付いたリクエストだけGreen側に流す優先度ルールを追加することで、通常ユーザに影響を与えずにGreen環境の動作確認ができる経路を用意します。詳しい仕様はListener rules for your Application Load Balancer(AWS公式ドキュメント)に記載があります。

EC2のダッシュボードからロードバランサーを開き、bg-handson-albの詳細画面でリスナーとルールタブを選択します。HTTP:80のリスナーにチェックを入れ、ルールを管理 > ルールを追加をクリックします。

ルールの設定画面が表示されるので、以下のように設定します。

ステップ1: 名前とタグ

設定項目 設定の基準
名前 test-traffic テスト経路用のルールとわかる名前

ステップ2: ルール条件の定義

「条件を追加」をクリックし、HTTPヘッダーを選択します。

設定項目 設定の基準
ルールの種類 HTTPヘッダー 特定のヘッダー付きリクエストだけGreen側に流すため
HTTPヘッダー名 X-Test-Traffic テスト経路を表すヘッダー名
HTTPヘッダー値 true このヘッダー値のときだけテスト経路として扱う

ステップ3: ルールアクションの定義

設定項目 設定の基準
アクションのルーティング ターゲットグループへ転送 Green用ターゲットグループに振り分けるため
ターゲットグループ bg-handson-green-tg 新バージョン検証用のターゲットグループ

ステップ4: ルールの優先度の設定

設定項目 設定の基準
優先度 1 デフォルトルールより先に評価されるよう最優先に設定

ステップ5で設定内容を確認し、作成をクリックします。

HTTP:80リスナーのルール一覧に、優先度1test-trafficルール(デフォルトルールの上)と、デフォルトルール(bg-handson-blue-tgに転送)の2つが表示されていれば、設定は完了です。

4.5 タスク定義の作成

ALBとターゲットグループの準備ができたので、次にECSで動かすコンテナの設計書であるタスク定義を作成します。タスク定義は「どのイメージを、どのCPU/メモリで、どのIAMロールで動かすか」をまとめたもので、ECSサービスはこのタスク定義を元にコンテナを起動します。ECSサービスの作成ではこのタスク定義を指定するため、先に用意しておく必要があります。

ECSのダッシュボードを開き、左側のメニューからタスク定義を選択し、新しいタスク定義の作成をクリックします。

設定項目 設定の基準
タスク定義ファミリー bg-handson-task タスク定義の名前
起動タイプ AWS Fargate サーバレスで実行するため
CPU 1 vCPU 画面の初期値をそのまま使用
メモリ 3 GB 画面の初期値をそのまま使用
タスクロール bg-handson-task-execution-role CloudFormationで作成したロール

💡 ポイント
タスク定義画面のIAMロール欄は「タスクロール」と「タスク実行ロール」の2つに分かれています。タスクロールはアプリ自身がAWS APIを呼ぶときに使うロール、タスク実行ロールはECSエージェントがECRからイメージをPullしたりCloudWatch Logsへログを書き出すときに使うロールです。本ハンズオンでは「タスク実行ロール」はAWSが自動生成するecsTaskExecutionRoleをそのまま使い、「タスクロール」にCloudFormationで作成したロール(ECR Pull・CloudWatch Logs書き出し権限を持つ)を指定する構成にしています。

コンテナの設定は以下のとおりです。

設定項目 設定の基準
コンテナ名 app コンテナの識別名
イメージURI (CloudFormation出力のECRRepositoryUri):latest ECRリポジトリの最新イメージ
コンテナポート 8000 FastAPIがリッスンするポート
CPU 1 画面の初期値(タスク全体のvCPU値)をそのまま使用
メモリのハード制限 3 画面の初期値(タスク全体のメモリ値、GB単位)をそのまま使用

設定後、作成をクリックします。

タスク定義の一覧にbg-handson-taskのリビジョン1が表示されていれば完了です。

4.6 ECSサービスの作成

タスク定義ができたので、それを使って実際にコンテナを動かすECSサービスを作成します。ECSサービスは「何個のタスクを、どのネットワークで、どのロードバランサーに紐付けて動かすか」を管理する役割を担います。

ここが今回のハンズオンの肝となるセクションです。Blue/Greenデプロイメントを機能させるためには、デプロイ戦略ロードバランシングの設定が重要になります。それぞれどのような意味を持つのか、設定しながら押さえていきましょう。

ECSのダッシュボードからbg-handson-clusterを開き、サービスタブから作成をクリックします。

基本設定

まずは、作成するECSサービスの基本情報を設定します。先ほど作成したタスク定義を指定し、サービス名と起動するタスク数を決めます。

設定項目 設定の基準
タスク定義ファミリー bg-handson-task 先ほど作成したタスク定義
サービス名 bg-handson-service サービスの識別名

デプロイ設定

ここがBlue/Greenデプロイメントを有効化する中核の設定です。デプロイ戦略を切り替えることで、ECSの挙動が大きく変わります。

設定項目 設定の基準
必要なタスク 1 ハンズオン用に1タスクで実行
デプロイ戦略 ブルー/グリーン Blue/Greenデプロイを有効にするため
ベイクタイム 30 本番切り替え後にBlueを残す時間(分)。手動ロールバックの体験時間を確保するため長めに設定

各設定項目を解説します。

デプロイ戦略ブルー/グリーンを選択することで、ECSが新タスク(Green)と旧タスク(Blue)を両立させながらトラフィックを切り替えるモードで動作するようになります。ローリングアップデートを選んだ場合は、タスクを1つずつ順番に置き換えるため切り替え瞬間の明確な境目がなく、また本番切り替え前にGreen環境を検証するタイミングも作れません。Blue/Greenデプロイメントを体験するためには、このデプロイ戦略の選択が必須です。

ブルー/グリーンを選択するとトラフィックの切り替え方は「すべてのトラフィックを更新済みのAmazon ECSコンテナに一度に移行」する動作になり、画面下部のトラフィック移行欄にもその旨が表示されます。「ここで切り替わった」というイベントが明確でハンズオン向きです。同じデプロイ戦略の選択肢には、一部のトラフィックだけを新バージョンに流して様子を見てから全体を切り替えるCanary方式や、段階的にトラフィックを移行するリニア方式もあります。Canary方式は一般にカナリアデプロイ(カナリアリリース)と呼ばれる手法で、問題が起きても影響範囲を一部のユーザに限定できるメリットがあり、実運用ではサービスの重要度に応じて選択します。

ベイクタイムは、Greenへ本番切り替えが完了した後、Blue側のタスクをすぐに削除せず一定時間保持する期間です。この間であれば、問題が見つかったときにBlueへ戻す(ロールバック)判断を人間の手で行えます。実運用では5〜15分程度が目安ですが、今回はフェーズ2で手動ロールバックを落ち着いて体験するために30分に設定します。

💡 ポイント
「Greenへの切り替え」自体はECSが一瞬で行いますが、Blueを残すかどうかはベイクタイム次第です。ベイクタイムを0にすると、切り替え完了と同時にBlueが終了するため手動ロールバックの余地がなくなります。Blue/Greenデプロイメントの価値の半分は、この「切り戻しできる時間の確保」にあると言えます。BAKE_TIMEステージの厳密な挙動はAmazon ECS blue/green service deployments workflow(AWS公式ドキュメント)に記載があります。

ネットワーキング

デプロイ設定の次は、タスクを配置するネットワーク環境を指定します。VPC・サブネット・セキュリティグループの設定はBlue/Greenデプロイメント特有の内容ではなく、通常のECSサービスと同様ですが、ここで正しく指定しないとタスクがECRからイメージをPullできずに起動しないため、先に設定します。

設定項目 設定の基準
VPC bg-handson-vpc CloudFormationで作成したVPC
サブネット bg-handson-public-subnet-1, bg-handson-public-subnet-2 パブリックサブネット
セキュリティグループ 既存のセキュリティグループを使用 先ほど作成したSGを使うため
セキュリティグループ名 bg-handson-ecs-sg ECSタスク用のセキュリティグループ
パブリックIP オン タスクがECRからイメージをPullするため

💡 ポイント
本番運用では、ECSタスクはインターネットから直接到達されないようプライベートサブネットに配置するのが一般的です。ただし、プライベートサブネットのタスクがECRからイメージをPullしたりCloudWatch Logsに書き込んだりするには、NAT GatewayまたはVPCエンドポイント(ECR API / ECR DKR / S3 / CloudWatch Logs など)が必要になり、構築コストが増えます。このハンズオンではBlue/Greenデプロイの仕組みに集中してもらうため、NAT GatewayやVPCエンドポイントの構築を省略し、タスクをパブリックサブネットに配置してパブリックIPをオンにすることで外部通信を成立させています。実案件では、セキュリティ要件に応じて「プライベートサブネット+NAT Gateway」または「プライベートサブネット+VPCエンドポイント」の構成を選ぶのが定石です。詳しくはAmazon ECS のベストプラクティス - ネットワーキング(AWS公式ドキュメント)に記載があります。

VPC・サブネット・セキュリティグループが指定されていれば、次の手順に進めます。

ロードバランシングの設定

タスクの配置先が決まったので、最後にトラフィックの振り分け設定を行います。Blue/Greenデプロイメントは、ALBの2つのターゲットグループ2つのリスナールールを組み合わせて成り立っています。ここで「どちらのターゲットグループを本番用/テスト用にするか」「どのリスナールールを本番用/テスト用にするか」をECSに教えます。

設定項目 設定の基準
ロードバランシングを使用 チェックを入れる ECSサービスにALBを連携するため
ロール bg-handson-ecs-infra-role ECSがALBのリスナールールやターゲットグループを操作するためのIAMロール
ロードバランサーの種類 Application Load Balancer ALBを使用するため
ロードバランサー bg-handson-alb 先ほど作成したALB
リスナー HTTP:80 本番・テスト両方のルールが属するリスナー
本番リスナールール 優先度: default 現行バージョンに転送するデフォルトルール
テストリスナールール - オプション、推奨 優先度: 1 先ほど追加したヘッダーベースのルール
オプション(ターゲットグループ) 2つの既存のターゲットグループを使用 Blue/Greenデプロイで使う2つのTGを指定するため
ターゲットグループ bg-handson-blue-tg Blue用ターゲットグループ
グリーンターゲットグループ bg-handson-green-tg Green用ターゲットグループ

各設定項目を解説します。

ターゲットグループグリーンターゲットグループは、ECSに「初期ラベルとして」本番側TGと代替側TGを伝える設定です。ここではbg-handson-blue-tgを「本番側」、bg-handson-green-tgを「代替側(Green)」として登録します。

ただし、ECS native Blue/Greenはサービス作成時の初回デプロイも含めて、すべてのデプロイで新タスクを代替側TGに起動 → ヘルスチェック合格後に本番リスナールールを代替側TGへ切り替えるという流れで動作します(Amazon ECS blue/green service deployments workflow(AWS公式ドキュメント))。

そのため、サービス作成時の指定はあくまで「初期ラベル」であり、実際の本番TGはECSがデプロイ完了時に自動で更新します。具体的には、初回デプロイ完了直後の状態は「bg-handson-green-tgが本番側(ver1のタスクが稼働)、bg-handson-blue-tgが代替側(空)」になります。以後のデプロイでは、本番/代替の役割が**bg-handson-blue-tgbg-handson-green-tgの間で交互に入れ替わる**動きになる点を押さえておきましょう。

本番リスナールール(優先度: default)は、ユーザからのトラフィックを受ける本番経路です。ECSはBlue/Green切り替えのタイミングで、このルールの転送先ターゲットグループを自動的に差し替えます。

テストリスナールール(優先度: 1、test-traffic)は、本番トラフィックに影響を与えずにGreen側へアクセスするための経路です。X-Test-Traffic: trueヘッダー付きのリクエストのみこのルールにマッチします。ECSは新タスク起動後、テストルールを新タスクのターゲットグループ側に向けるため、本番切り替え前の動作確認経路として利用できます。

ロールbg-handson-ecs-infra-role)は、ECSが上記のリスナールールの差し替えやターゲットグループの付け替えをAWS側で実行するために引き受けるIAMロールです。このロールがないとECSはALB構成を書き換えられず、Blue/Greenの切り替えを実行できません。コンソール上の項目名は「ロール」ですが、AWS公式ドキュメントではインフラストラクチャIAMロールと呼ばれているものを指します。

詳しくはApplication Load Balancer を用いたゼロダウンタイムデプロイ(AWS公式ドキュメント)に記載があります。

設定後、作成をクリックします。

しばらく待つと、サービスが起動し、タスクが1つ実行されます。サービスのデプロイタブを開き、デプロイがCOMPLETEDになっていれば、ここまでの操作は完了です。

初回デプロイの動作確認

ALBのDNS名を使って、デプロイされたAPIにアクセスします。ALBの詳細画面からDNS名をコピーし、以下のコマンドを実行します。

まず、本番ルート(ヘッダーなし)のヘルスチェックエンドポイントにアクセスします。

curl http://<ALBのDNS名>/health

以下のようにレスポンスが返ってくれば、正常にデプロイされています。

{"status":"ok"}

続いて、本番ルート(ヘッダーなし)の動作確認用エンドポイントにアクセスします。

curl http://<ALBのDNS名>/

以下のようにメッセージが返ってきます。

{"message":"API呼び出しテストに成功しました"}

最後に、テストルートにもアクセスしてみます。X-Test-Traffic: trueヘッダーを付けることで、先ほど追加したテストリスナールールにマッチし、bg-handson-green-tgへ転送されます。

ここで一度、ECSサービスの正常性とメトリクスタブにあるロードバランサーのターゲット正常性を確認してみてください。bg-handson-green-tg側に本番トラフィックのバッジが付き、ver1のタスクが1件正常に稼働していること、そして**bg-handson-blue-tgはターゲットなしで空**になっていることが確認できます。

これは前述のとおり、ECS native Blue/Greenが初回デプロイも「代替側TGに新タスクを起動 → ヘルスチェック合格後に本番リスナールールを代替側TGへ切り替え」という流れで処理した結果です。サービス作成時に「bg-handson-blue-tg = 本番側」と指定しましたが、初回デプロイ完了時点で本番リスナールール(default)の転送先はECSによって自動的にbg-handson-green-tgへ書き換えられています

そのため、X-Test-Traffic: trueヘッダー付きのリクエストも、本番ルートと同じくbg-handson-green-tgに転送され、同じver1のレスポンスが返ります。HTTPステータスコードも確認できるよう-iオプションを付けて実行してみましょう。

curl -i -H "X-Test-Traffic: true" http://<ALBのDNS名>/

以下のように、HTTPステータス200 OKでver1のメッセージが返れば、期待どおりの挙動です。

HTTP/1.1 200 OK
Content-Type: application/json
server: uvicorn

{"message":"API呼び出しテストに成功しました"}

この状態をまとめると以下のようになります。

経路 接続先 期待される結果
ヘッダーなし(本番ルール) bg-handson-green-tg(現本番側、ver1稼働中) {"message":"API呼び出しテストに成功しました"}
X-Test-Traffic: trueヘッダー付き(テストルール) bg-handson-green-tg(同上) {"message":"API呼び出しテストに成功しました"}

初回デプロイ完了時点では、本番ルール/テストルールがともにbg-handson-green-tgを指しているため、両方の経路で同じver1のレスポンスが返ります。次の「Blue/Green切り替えの体験」でver2をデプロイすると、現在「代替側」になっているbg-handson-blue-tgへ新タスクが起動され、テストルールがbg-handson-blue-tgへ切り替わるタイミングで本番ルートとテストルートで異なるバージョンが返るようになります。

4.7 Blue/Green切り替えの体験

ここからが今回のハンズオンの本題です。アプリケーションのコードを変更してイメージを再度プッシュし、ECSがBlue/Greenデプロイメントのライフサイクルを実行する様子を実際に観察します。

コードの変更

devopscamp-bg-handsonフォルダのmain.pyを開き、root関数のレスポンスメッセージに(ver2)を追加します。

@app.get("/")
def root():
    return {"message": "API呼び出しテストに成功しました(ver2)"}

ファイルを保存したら、次のステップに進みます。

イメージのビルドとプッシュ

変更したコードをコンテナイメージとしてビルドし、ECRにプッシュします。

docker build --platform linux/amd64 -t bg-handson-repo .
docker tag bg-handson-repo:latest <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest
docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest

latestタグの中身が新しいイメージに置き換わりました。ただし、これだけではECSは新しいイメージに気づきません。次のステップでECSに「新しいデプロイを開始して」と指示します。

デプロイの開始

ECSサービスに対して、新しいデプロイを開始するよう指示します。これにより、ECSが最新のlatestイメージを使って新しいタスク(Green)を起動し、Blue/Greenデプロイメントのライフサイクルに入ります。

ECSのダッシュボードからbg-handson-clusterを開き、サービス一覧でbg-handson-serviceにチェックを入れます。サービス一覧の右上にある更新▼ドロップダウンから新しいデプロイの強制をクリックします。

📝 「新しいデプロイの強制」とは
タスク定義の内容を変更せずに、ECSに「新しいタスクで再起動して」と指示するオプションです。イメージのタグが同じ(latest)でも、強制的に新しいタスクを起動してイメージをPullし直すため、コンテナイメージだけを更新したいときに使えます。AWS CLIではaws ecs update-service ... --force-new-deploymentで同じことができます。

確認ダイアログで実行を承認します。サービスのデプロイタブに、新しいデプロイがIN_PROGRESS状態で追加されていれば、デプロイの開始は完了です。

デプロイの進行を確認

ECSのコンソールで、サービスbg-handson-serviceデプロイタブを開きます。新しいデプロイが開始され、ライフサイクルステージが順番に進んでいく様子を確認します。

順番 ライフサイクルステージ(コンソール表示 / API名) 何が起きるか
1 スケールアップ / SCALE_UP Green環境のタスクが起動される
2 テストトラフィック移行 / TEST_TRAFFIC_SHIFT Greenのヘルスチェックが通ると、ALBのテストルールX-Test-Traffic: trueヘッダー付きリクエスト)のトラフィックがGreenに切り替わる。この時点では本番ルール(ヘッダーなしの通常リクエスト)はまだBlue(旧バージョン)のまま
3 本番トラフィック移行 / PRODUCTION_TRAFFIC_SHIFT 本番ルール(ヘッダーなしの通常リクエスト)のトラフィックもBlue → Greenに切り替わる
4 ベイク時間 / BAKE_TIME 設定した時間(今回は30分)、BlueとGreenの両方のタスクが稼働し続ける期間。この間は手動でロールバックすれば即座にBlueに戻せる。時間が経過すると自動的にクリーンアップへ進む
5 クリーンアップ / CLEAN_UP ベイクタイム経過後、Blue環境のタスクが停止される

ここで重要なのは、TEST_TRAFFIC_SHIFTPRODUCTION_TRAFFIC_SHIFT の間に時間差があることです。この間は「本番ルール(ヘッダーなし)は旧バージョン、テストルール(X-Test-Traffic: trueヘッダー付き)は新バージョン」という状態になります。実務ではこのタイミングでテストルート経由の動作確認を行い、新バージョンに問題がないことを確認してから本番に切り替えます。

💡 ポイント
本番ルール(ヘッダーなしの通常リクエスト)が新バージョンに切り替わるのは、PRODUCTION_TRAFFIC_SHIFTステージが完了した瞬間です。BAKE_TIMEはそれより後の待機時間(Blueタスクをすぐ終了させないための猶予期間)であり、切り替え自体はすでに完了しています。

デプロイ開始直後にcurlを叩いてもまだver1が返ってくるのは、PRODUCTION_TRAFFIC_SHIFTがまだ進行中だからです。ECSコンソールのデプロイタブでPRODUCTION_TRAFFIC_SHIFTが完了していることを確認してから本番ルート(ヘッダーなし)にアクセスしてください。

テストルート経由で新バージョンを先行確認

ライフサイクルステージがTEST_TRAFFIC_SHIFTに進んだら、素早くヘッダー付き/ヘッダーなしの両方でアクセスして、本番ルールとテストルールで異なるバージョンが返ることを確認します。

💡 ポイント
TEST_TRAFFIC_SHIFTからPRODUCTION_TRAFFIC_SHIFTに進むまでの時間は短いため、ECSコンソールでステージの進行を見ながら、すぐに以下のコマンドを実行してください。タイミングを逃した場合は、後の「ロールバックの体験」セクションでも同じ状態を再現できます。

まずテストルート(X-Test-Traffic: trueヘッダー付き)でアクセスします。

curl -H "X-Test-Traffic: true" http://<ALBのDNS名>/

新バージョンのメッセージが返れば、Green環境がテストルート経由で動作しています。

{"message":"API呼び出しテストに成功しました(ver2)"}

続けて、本番ルート(ヘッダーなし)でもアクセスします。

curl http://<ALBのDNS名>/

PRODUCTION_TRAFFIC_SHIFTの前であれば、こちらはまだ旧バージョンが返ります。

{"message":"API呼び出しテストに成功しました"}

この時点での経路ごとの状態は以下のとおりです。

経路 接続先 期待される結果
ヘッダーなし(本番ルール) bg-handson-green-tg(現本番側、ver1稼働中) {"message":"API呼び出しテストに成功しました"}
X-Test-Traffic: trueヘッダー付き(テストルール) bg-handson-blue-tg(現代替側、ver2稼働中) {"message":"API呼び出しテストに成功しました(ver2)"}

このように、テストルールを使うことで本番に影響を与えずに新バージョンを検証できます。これがテストルールを用意している理由であり、後のフェーズ3で扱うLambda自動テストもこのタイミングを利用しています。

本番切り替え後の動作確認

TEST_TRAFFIC_SHIFTが終わると、ECSは自動的にPRODUCTION_TRAFFIC_SHIFTステージへ進みます。特にこちらから操作は必要なく、1分以内に本番ルールの切り替えが完了します。

ECSコンソールのデプロイタブで、進行中のデプロイのステージ表示がPRODUCTION_TRAFFIC_SHIFTから次の段階(POST_PRODUCTION_TRAFFIC_SHIFTまたはBAKE_TIME)に進んでいれば、本番ルールの切り替えは完了しています。

この状態になったら、改めて本番ルート(ヘッダーなし)にアクセスします。

curl http://<ALBのDNS名>/

レスポンスのメッセージに(ver2)が含まれていればOKです。

{"message":"API呼び出しテストに成功しました(ver2)"}

テストルート(X-Test-Traffic: trueヘッダー付き)にもアクセスします。

curl -H "X-Test-Traffic: true" http://<ALBのDNS名>/

こちらも(ver2)が返ります。

{"message":"API呼び出しテストに成功しました(ver2)"}

本番切り替え後の経路ごとの状態は以下のとおりです。

経路 接続先 期待される結果
ヘッダーなし(本番ルール) bg-handson-blue-tg(新本番側、ver2稼働中) {"message":"API呼び出しテストに成功しました(ver2)"}
X-Test-Traffic: trueヘッダー付き(テストルール) bg-handson-blue-tg(同上) {"message":"API呼び出しテストに成功しました(ver2)"}

両方のルールが新しい本番側TG(ver2)を指しているため、同じver2のレスポンスが返ってきます。なおこの時点で、本番側と代替側の役割は初回デプロイ完了時から入れ替わっており、bg-handson-blue-tgが本番側、bg-handson-green-tgが代替側になっています。

このタイミングでECSコンソールのタスクタブを見ると、新環境(ver2)と旧環境(ver1)の両方のタスクが稼働していることが確認できます。これがBlue/Greenデプロイメントの特徴で、ベイクタイムの間は両方を維持することで、問題があればすぐに旧環境に戻せる状態を保っています。

⚠️ 次のフェーズ2はベイクタイム中(30分以内)に実施する必要があります
次のフェーズ2では、いま開始したver2のデプロイをロールバックする操作を行います。ECSのロールバックはベイクタイム中(今回は30分)にのみ可能で、ベイクタイムが経過してCLEAN_UPステージへ進むと旧バージョン(ver1)のタスクが停止されてしまうため、ロールバックボタンも非活性になります。フェーズ1のver2デプロイ完了から30分以内にフェーズ2を進めてください。

5. フェーズ2: 手動ロールバックの体験

ここまでで「新バージョンに切り替わる」流れは体験できましたが、Blue/Greenデプロイメントの真価は「問題があったらすぐ前のバージョンに戻せる」ことにあります。

フェーズ1ではver2をデプロイし、両方の経路から(ver2)を含むメッセージが返るところまで確認しました。ここでは、本番投入後にユーザから「動作がおかしい」と報告が入った場面を想定し、ベイクタイム中(30分以内)にECSコンソールから手動でロールバックする操作を体験します。

5.1 ロールバックの実行

ECSのコンソールで、サービスbg-handson-serviceデプロイタブを開きます。フェーズ1でver2にデプロイした進行中のデプロイが、現在のデプロイ段階「ベイク時間」の状態で表示されているはずです。

進行中のデプロイの右上にあるロールバックをクリックします。確認ダイアログで実行を承認すると、ECSは以下を自動的に行います。

  1. ALB本番ルールの転送先を、ver1が稼働しているTG(bg-handson-green-tg)へ戻す
  2. ver2が稼働しているTG(bg-handson-blue-tg)のタスクを停止する

5.2 ロールバック後の動作確認

数十秒ほど待ってから、両方の経路にアクセスして状態を確認します。

本番ルート(ヘッダーなし)。

curl http://<ALBのDNS名>/
{"message":"API呼び出しテストに成功しました"}

テストルート(X-Test-Traffic: trueヘッダー付き)。

curl -H "X-Test-Traffic: true" http://<ALBのDNS名>/
{"message":"API呼び出しテストに成功しました"}

(ver2)の表記が消え、ver1のメッセージに戻っていれば、ロールバックは成功です。

ロールバック後の経路ごとの状態は以下のとおりです。

経路 接続先 期待される結果
ヘッダーなし(本番ルール) bg-handson-green-tg(ver1稼働中、本番に復帰) {"message":"API呼び出しテストに成功しました"}
X-Test-Traffic: trueヘッダー付き(テストルール) bg-handson-green-tg(同上) {"message":"API呼び出しテストに成功しました"}

両方のルートから(ver2)のないver1のメッセージが返っていれば、ver2は完全に本番から取り除かれた状態です。本番側/代替側の役割もver2デプロイ前と同じ「bg-handson-green-tgが本番側、bg-handson-blue-tgが代替側」に戻っています。

💡 ポイント
ベイクタイム中は前のバージョン(ver1)のタスクがまだ稼働しているため、ロールバック処理は非常に高速です。一方、ベイクタイムを過ぎてCLEAN_UPまで進んでしまうと前バージョンのタスクは停止され、ECSコンソールのロールバックボタンも非活性になります。この状態になってからバグに気づいた場合は、旧イメージを再度デプロイし直す形でしか戻せず、復旧に時間がかかります。ベイクタイムを長めに取ることで、万一の発覚に備える時間を確保できるというのがBlue/Greenデプロイメントの重要なメリットです。

すでにフェーズ1のver2デプロイから30分以上経過してロールバックボタンが非活性になっている場合は、今回のフェーズ2はスキップしても以降の流れには影響ありません。その場合、以降のフェーズで本番に稼働しているのはver1ではなくver2となるため、curlで返るメッセージに(ver2)が含まれる点だけ読み替えて進めてください。

6. フェーズ3: 自動テストの追加

ここまでで、Blue/Greenデプロイメントの基本動作とロールバックを体験しました。しかし現状の構成には大きな弱点があります。「バグに気づけるのはベイクタイム中だけで、気づかずにベイクタイムが過ぎるとBlueタスクがCLEAN_UPで停止され、すぐには戻せなくなる」という点です。

たとえば深夜や休日のデプロイで、人間が監視していないケース。ベイクタイムが経過したあとにバグが発覚しても、Blue環境は既に停止しているため、先ほど体験したような「ECSコンソールから即ロールバック」はできません。復旧するには旧イメージを再度デプロイし直す必要があり、その間もユーザにはバグ入りバージョンが表示され続けます。

また、ベイクタイム中であっても、バグに気づくまでの時間はユーザにバグ入りバージョンを見せてしまっている状態です。できれば本番切り替え自体を止められるほうが理想的です。

そこで、本番切り替え前にGreen環境で自動テストを実行する仕組みを追加します。テストが失敗した場合はECSが自動的にロールバックしてくれるため、ユーザがバグを目にする前に防げます。

この自動テストを担うのが、ECSのライフサイクルフックから呼び出されるLambda関数です。

📝 Lambdaについて
LambdaはAWSが提供するサーバレスのコード実行サービスで、「関数(コード)」を用意しておくと、イベント(HTTP呼び出し、スケジュール、別のAWSサービスからの通知など)をきっかけに自動で実行してくれます。サーバを自分で立てたり維持したりする必要がなく、必要なときだけ動くのが特徴です。今回はECSのライフサイクルフックから呼び出される「テスト実行役」として利用します。サービス全体の概要はWhat is AWS Lambda?(AWS公式ドキュメント)に記載があります。

このハンズオンの目的はLambda自体を習得することではなく、Blue/Greenデプロイを安全にするための手段としてLambdaを使う点に注意してください。Lambdaをより体系的に学びたい方は、後続のサーバレス章で取り扱う予定です。

6.1 ライフサイクルフックとは

ライフサイクルフックとは、ECSのBlue/Greenデプロイメントにおける各ステージの節目で任意の処理を差し込むための仕組みです。ECSがデプロイを進める途中で特定のLambda関数を呼び出し、そのLambdaが成功を返したら次のステージへ進み、失敗を返したらデプロイを停止して自動ロールバックする、という挙動を実現します。

デプロイの進行中にECSが呼び出せるフックポイントは、フェーズ1で確認したライフサイクルステージに対応する形で用意されています。AWSコンソールの設定画面では日本語名で選択するため、API名(コードやドキュメントで使う名前)と並べて整理します。

フックポイント(API名 / コンソール表示) 呼び出されるタイミング 主な用途
PRE_SCALE_UP / スケールアップ前 Green環境のタスクが起動する前 デプロイ前提条件のチェック
POST_SCALE_UP / スケールアップ後 Green環境のタスクが起動完了した後 起動後の初期セットアップ
TEST_TRAFFIC_SHIFT / テストトラフィック移行 テストルールがGreenを指している状態 自動テスト手動承認の受け付け
POST_TEST_TRAFFIC_SHIFT / テストトラフィック移行後 テストトラフィック切り替えが完了した後 テストルート確立後の追加処理
PRODUCTION_TRAFFIC_SHIFT / 本番トラフィック移行 本番ルールがGreenを指すタイミング 切り替え時のチェック
POST_PRODUCTION_TRAFFIC_SHIFT / 本番トラフィック移行後 本番トラフィック切り替えが完了した後 切り替え後の通知・メトリクス取得

このハンズオンでは、TEST_TRAFFIC_SHIFTフック(コンソールでは「テストトラフィック移行」)にLambda関数を登録します。このタイミングでは、ALBのテストルールがGreen環境のターゲットグループを指している状態のため、LambdaからX-Test-Traffic: trueヘッダー付きでアクセスすればGreen環境にリクエストが届きます。本番ユーザには影響を与えずにGreen環境を叩ける状態のため、このタイミングで自動テストを走らせるのが最適です。

フックのLambdaは、ECSから呼び出されると以下の3つのステータスのいずれかを返します。ECSはこの戻り値を見てデプロイを進めるか判断します。

戻り値 ECSの挙動
SUCCEEDED フック成功として次のステージへ進む
FAILED デプロイを停止し、自動ロールバックを開始する
IN_PROGRESS まだ判定が終わっていないとみなし、ECSが一定間隔で再度Lambdaを呼び出す(タイムアウトまで繰り返す)

詳しくはAmazon ECS deployment lifecycle stages(AWS公式ドキュメント)に記載があります。

それでは、この仕組みを実際に組み立てていきます。まずはLambdaを動かすためのIAMロール、次にLambda関数本体、最後にECSサービスにフックを設定する、という順で進めます。

6.2 IAMロールの作成

Lambda関数本体を作る前に、関連するIAMロールを2つ用意します。Lambdaを動かすには「Lambdaが自分自身として何をできるか」と「誰がそのLambdaを呼び出せるか」の両方を定義する必要があり、それぞれ別のロールで表現するためです。

今回必要になる2つのロールは以下のとおりです。

ロール名 誰が引き受けるか 目的
Lambda実行ロールbg-handson-lambda-role Lambda関数自身 Lambdaが実行時にCloudWatch Logsへログ出力するなど、自分の作業に必要な権限を与える
ライフサイクルフック呼び出しロールbg-handson-lifecycle-hook-role ECS(ecs.amazonaws.com ECSがライフサイクルフックとしてLambdaを呼び出す(lambda:InvokeFunction)権限を与える

この2つがそろっていないと、ECSがLambdaを呼び出せなかったり、Lambdaが動いてもログが出ず原因調査ができなかったりします。順に作成していきます。

Lambda実行ロール

まず、Lambda関数自身が引き受けるLambda実行ロールを作成します。このロールはCloudWatch Logsへのログ出力権限(AWSLambdaBasicExecutionRole)を持ち、Lambdaが実行された際の標準出力・エラー出力をログとして残せるようにします。

IAMのダッシュボードを開き、ロール > ロールを作成をクリックします。

ステップ1: 信頼されたエンティティを選択

「信頼されたエンティティタイプ」でAWSのサービスを選び、ユースケースのプルダウンでLambdaを選択します。

ステップ2: 許可ポリシーを追加

検索欄にAWSLambdaBasicExecutionRoleと入力し、表示されたポリシーにチェックを入れて次へをクリックします。

ステップ3: 名前を付けて作成

ロール名に**bg-handson-lambda-roleと入力し、内容を確認したらロールを作成**をクリックします。

ステップ4: 作成完了の確認

ロール一覧にbg-handson-lambda-roleが表示されていれば作成完了です。

設定値のまとめ:

設定項目 設定の基準
信頼されたエンティティタイプ AWSのサービス Lambda関数が使用するロールのため
ユースケース Lambda Lambda関数の実行ロール
許可ポリシー AWSLambdaBasicExecutionRole CloudWatch Logsへのログ出力に必要
ロール名 bg-handson-lambda-role Lambda関数の実行ロール

ライフサイクルフック呼び出しロール

続いて、ECSがLambda関数を呼び出すために引き受けるロールを作成します。Lambda実行ロールが「Lambda自身の権限」を表すのに対し、こちらは「呼び出し元であるECSの権限」を表すロールで、lambda:InvokeFunction権限を持たせます。このロールがないとECSはLambdaを呼べず、ライフサイクルフックが機能しません。

また、通常のLambdaロールと違い、信頼されたエンティティをECS(ecs.amazonaws.com)にする必要があるため、ウィザードの定型ユースケースではなくカスタム信頼ポリシーで作成します。

再度ロール > ロールを作成をクリックします。

ステップ1: カスタム信頼ポリシーを入力

「信頼されたエンティティタイプ」でカスタム信頼ポリシーを選び、JSONエディタに以下を入力します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

入力後、次へをクリックします。

ステップ2: 許可ポリシー画面はスキップ

許可ポリシーの画面では何も選ばず、そのまま次へをクリックします。許可ポリシーは、ロール作成後にインラインポリシーとして追加するためです。

ステップ3: 名前を付けて作成

ロール名に**bg-handson-lifecycle-hook-roleと入力し、信頼ポリシーの内容を確認してロールを作成**をクリックします。

ステップ4: 作成完了の確認

ロール一覧にbg-handson-lifecycle-hook-roleが追加されていれば作成完了です。

ステップ5: インラインポリシーの追加開始

作成したbg-handson-lifecycle-hook-roleの詳細画面を開き、許可を追加 > インラインポリシーを作成をクリックします。

ステップ6: ポリシーをJSONで入力

ポリシーエディタでJSONタブに切り替え、以下のポリシーを入力します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": "*"
    }
  ]
}

入力後、次へをクリックします。

ステップ7: ポリシー名を付けて確認

ポリシー名にInvokeLambdaPolicyと入力し、内容を確認してポリシーの作成をクリックします。

ステップ8: 作成完了の確認

bg-handson-lifecycle-hook-roleの許可ポリシー欄にInvokeLambdaPolicyが追加されていれば完了です。

設定値のまとめ:

設定項目 設定の基準
信頼されたエンティティタイプ カスタム信頼ポリシー ECSサービスが引き受けるロールのため
ロール名 bg-handson-lifecycle-hook-role ECSがLambdaを呼び出すためのロール
信頼ポリシー ecs.amazonaws.comからのsts:AssumeRole ECSにロール引き受けを許可
インラインポリシー名 InvokeLambdaPolicy 役割が分かる名前
インラインポリシー内容 lambda:InvokeFunctionを許可 LambdaをECSから呼び出すための権限

6.3 Lambda関数の作成

ロールが揃ったので、テストを実行するLambda関数本体を作成していきます。

ステップ1: Lambdaダッシュボードから関数作成を開始

AWSマネジメントコンソールの検索バーから「Lambda」を開き、左側メニューの関数を選択します。一覧画面右上の関数を作成をクリックします。

ステップ2: 基本情報の入力

一から作成」を選び、関数名とランタイムを入力します。

ステップ3: 実行ロールを既存ロールに変更

画面下部のその他の設定を展開し、カスタム実行ロールのトグルをオンにします。表示されたIAM アクセス許可欄から、先ほど作成した**bg-handson-lambda-roleを選択します。最後に関数を作成**をクリックします。

設定値のまとめ:

設定項目 設定の基準
関数名 bg-handson-e2e-test E2Eテスト用のLambda関数
ランタイム Python 3.13 テストコードをPythonで記述するため
カスタム実行ロール オン デフォルトの新規ロール作成ではなく、先ほど作成したロールを使うため
IAM アクセス許可 bg-handson-lambda-role Lambda実行ロール

6.4 テストコードの記述

Lambda関数が作成されたら、コードを記述します。関数の詳細画面でコードタブを開き、lambda_function.pyの内容を以下に置き換えます。

import json
import os
import urllib.request


def lambda_handler(event, context):
    """
    ECS Blue/Green ライフサイクルフック用のE2Eテスト関数。
    Green環境のルートエンドポイントにリクエストを送り、APIの振る舞いを検証する。
    """
    print("Event:", json.dumps(event))

    alb_dns_name = os.environ.get("ALB_DNS_NAME", "")
    if not alb_dns_name:
        print("ALB_DNS_NAMEが指定されていないため、スキップします")
        return {"hookStatus": "SUCCEEDED"}

    test_url = f"http://{alb_dns_name}/"

    try:
        print(f"テストURL: {test_url}")
        req = urllib.request.Request(
            test_url,
            method="GET",
            headers={"X-Test-Traffic": "true"},
        )
        with urllib.request.urlopen(req, timeout=10) as response:
            body = response.read().decode("utf-8")
            status_code = response.status
            print(f"ステータスコード: {status_code}")
            print(f"レスポンス: {body}")

            if status_code == 200:
                data = json.loads(body)
                message = data.get("message", "")
                if "API呼び出しテストに成功しました" in message:
                    print("E2Eテスト成功")
                    return {"hookStatus": "SUCCEEDED"}

        print("E2Eテスト失敗")
        return {"hookStatus": "FAILED"}

    except Exception as e:
        print(f"テスト実行エラー: {e}")
        return {"hookStatus": "FAILED"}

コードを解説します。

    alb_dns_name = os.environ.get("ALB_DNS_NAME", "")
    ...
    test_url = f"http://{alb_dns_name}/"

環境変数ALB_DNS_NAMEからALBのDNS名を取得し、テスト対象のURLを組み立てています。環境変数が空の場合はテストをスキップします。ALBのDNS名は後続の手順でLambda関数の環境変数として設定します。

💡 ポイント
なぜ/healthではなく/(ルート)をテストするのか? /healthはALBのヘルスチェックがすでに監視しているため、/healthが落ちる種類のバグはALBが先に検知してタスクを起動させません(=デプロイがそもそも進まない)。ALBヘルスチェックでは捉えられない、APIの振る舞いに関するバグ(レスポンス内容の誤り・ビジネスロジックの不具合・特定の経路でのみ起きる例外など)を検出するのがLambdaの役割です。そのため、ユーザに見える主要エンドポイントである/を叩いて振る舞いを検証します。
        req = urllib.request.Request(
            test_url,
            method="GET",
            headers={"X-Test-Traffic": "true"},
        )
        with urllib.request.urlopen(req, timeout=10) as response:

テスト対象のURL(ALBのルート/)にHTTPリクエストを送信しています。X-Test-Traffic: trueヘッダーを付けることで、ALBのテストルールにマッチし、Green環境(新バージョンのタスク)にルーティングされます。タイムアウトは10秒に設定しています。

            if status_code == 200:
                data = json.loads(body)
                message = data.get("message", "")
                if "API呼び出しテストに成功しました" in message:
                    return {"hookStatus": "SUCCEEDED"}

ステータスコードが200で、レスポンスのメッセージに想定の文字列API呼び出しテストに成功しましたが含まれていればテスト成功とします。単なる200レスポンスではなくメッセージ内容まで確認することで、「200は返すが中身が壊れている状態のバグも検知できます。成功の場合は{"hookStatus": "SUCCEEDED"}を返し、ECSはデプロイの次のステージ(本番切り替え)に進みます。失敗の場合は{"hookStatus": "FAILED"}を返し、ECSが自動ロールバックを実行します。

hookStatusキーはAWS公式のライフサイクルフック仕様で定められた固定名です。単なるstatusでは受け付けられず、必ずhookStatusを返す必要があります。

コードを置き換えたら、エディタ右上のDeployボタン(橙色)をクリックして保存します。Deployを押さないと変更がLambdaに反映されないため、必ず押してください。

⚠️ 画像のコードと最新教材のコードが異なる場合
上の画像はコードエディタを撮影した時点での旧バージョンが映っています。具体的には、test_url = f"http://{alb_dns_name}/health"data.get("status") == "ok"print("ヘルスチェック成功/失敗")のように、/healthを叩いてstatusキーで判定する古い実装が表示されています。実際に貼り付けるコードは、教材本文の最新コード/を叩いてmessageに「API呼び出しテストに成功しました」が含まれるかを判定するもの)を使ってください。

6.5 タイムアウトの設定

Lambda関数のデフォルトタイムアウトは3秒ですが、HTTPリクエストの送信には時間がかかる場合があるため、タイムアウトを延長しておきます。

設定タブ > 左メニューの一般設定 > 右上の編集をクリックし、タイムアウトを1分0秒に変更して保存をクリックします。

設定後、一般設定の画面に戻ったときに、タイムアウトが1分 0秒になっていればOKです。

6.6 環境変数の設定

Lambda関数がテスト対象のALBにアクセスできるよう、ALBのDNS名を環境変数として設定します。コード内でos.environ.get("ALB_DNS_NAME")によって参照される値です。

設定タブ > 左メニューの環境変数 > 右上の編集をクリックし、環境変数の追加で以下を追加します。

キー 設定の基準
ALB_DNS_NAME <ALBのDNS名>(例: bg-handson-alb-xxxx.ap-northeast-1.elb.amazonaws.com LambdaがE2Eテストを送る対象のALB

ALBのDNS名はEC2のダッシュボード > ロードバランサー > bg-handson-albの詳細画面でコピーできます。http://のプロトコル部分は含めずに値として設定してください。

保存をクリックし、Lambda関数の環境変数にALB_DNS_NAMEが登録されていれば設定は完了です。

Lambda関数の準備が整いました。次にECSサービス側にライフサイクルフックの設定を追加します。

6.7 ECSサービスにライフサイクルフックを設定

Lambda関数の準備ができたので、ECSサービス側に「TEST_TRAFFIC_SHIFTステージで先ほどのLambdaを呼び出す」という設定を追加します。

ステップ1: サービスの更新画面を開く

ECSのコンソールでbg-handson-clusterを開き、サービス一覧からbg-handson-serviceにチェックを入れて更新をクリックします。

ステップ2: ライフサイクルフックを追加

サービス更新画面を下にスクロールし、デプロイライフサイクルフック - オプションセクションを展開します。Lambda関数ロールライフサイクルステージのそれぞれに、以下の値を入力します。

設定項目 設定の基準
Lambda関数 bg-handson-e2e-test 先ほど作成したテスト用Lambda
ロール bg-handson-lifecycle-hook-role 先ほど作成したLambda呼び出し用のIAMロール
ライフサイクルステージ テストトラフィック移行(TEST_TRAFFIC_SHIFT テストトラフィック切り替え時にテストを実行するため

ライフサイクルステージのプルダウンでは、「テストトラフィック移行」という日本語表示で選択肢が並びます。これがAWS API上の TEST_TRAFFIC_SHIFT に対応します。

📝 TEST_TRAFFIC_SHIFTとは
TEST_TRAFFIC_SHIFTは、Green環境がテストルール経由でアクセス可能になったタイミングで呼ばれるライフサイクルステージです。このステージでLambdaがテストを実行し、成功すれば本番切り替えに進み、失敗すれば自動ロールバックが行われます。詳しくは公式ドキュメントに記載があります。

設定後、画面下部の更新をクリックします。これでECSサービスに新しい構成が反映され、次回のデプロイからLambdaによる自動テストが実行されるようになります。

6.8 成功ケースの確認

まずは正常なコードをデプロイして、Lambdaテストが通り、自動で本番切り替えが進むことを確認します。

main.pyのメッセージを元に戻し、(ver3)として新しくデプロイします。

@app.get("/")
def root():
    return {"message": "API呼び出しテストに成功しました(ver3)"}

イメージをビルド・プッシュします。

docker build --platform linux/amd64 -t bg-handson-repo .
docker tag bg-handson-repo:latest <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest
docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest

続いて、フェーズ1と同じ手順でECSに新しいデプロイを開始させます。ECSのダッシュボードからbg-handson-clusterbg-handson-serviceを開き、サービスの更新をクリックします。新しいデプロイの強制にチェックを入れ、更新をクリックします。

ECSコンソールのデプロイタブを開き、進行状況を確認します。デプロイがスケールアップ → テストトラフィック移行 → 本番トラフィック移行 → ベイクタイム、と進んでいくのが見えるはずです。TEST_TRAFFIC_SHIFTステージでLambdaが呼び出されます。

画面に「進行中のデプロイ」セクションが表示され、デプロイ戦略「ブルー/グリーン」、現在のデプロイ段階がベイク時間まで進んでいれば、Lambdaのテストが通って本番切り替えまで完了している状態です。

CloudWatch LogsでLambdaの実行ログを確認

Lambdaが想定どおりE2Eテストに成功したかを、CloudWatch Logsのログから確認します。Lambdaは実行のたびに標準出力・エラー出力を/aws/lambda/<関数名>というロググループに書き出すため、このロググループを開けば今回のフック実行時のログが確認できます。

AWSマネジメントコンソールの検索バーから「CloudWatch」を開き、左側メニューからログ > ロググループを選択します。

ロググループ一覧で、/aws/lambda/bg-handson-e2e-testを探してクリックします。ロググループが見つからない場合は、Lambdaがまだ一度も実行されていない可能性があるため、ECSのデプロイタブでTEST_TRAFFIC_SHIFTステージまで進んでいるかを再確認してください。

ロググループを開くと、ログストリームの一覧が表示されます。ログストリームはLambdaの実行単位で分かれており、最終イベント時間が最も新しいものが今回のフック呼び出しのログです。最新のログストリームをクリックします。

💡 ポイント
ロググループはLambda関数1つに対して1つ(/aws/lambda/<関数名>)作成され、その関数の全実行ログをまとめる入れ物です。ログストリームはLambdaの同じ実行環境(コンテナ)から出力されたログの連なりで、Lambdaが呼び出されるたびに新しいストリームが作られるわけではなく、既存の実行環境が再利用された場合は既存ストリームに追記されます。そのため「今回の実行のログを見たい」ときは、最終イベント時間が最新のストリームを開くのが確実です。

ログストリームを開くと、タイムスタンプ付きのログ行が並びます。以下のような出力が含まれていれば、Lambdaによる自動テストが成功しています。

テストURL: http://<ALBのDNS名>/
ステータスコード: 200
レスポンス: {"message":"API呼び出しテストに成功しました(ver3)"}
E2Eテスト成功

⚠️ 画像のログと最新教材のログ出力が異なる場合
上の画像はコードを撮影した時点での旧バージョンの実行ログが映っています。具体的には、テストURL: http://...../healthレスポンス: {"status":"ok"}ヘルスチェック成功が表示されていますが、実際に最新コードで動かすとテストURL: http://...../レスポンス: {"message":"API呼び出しテストに成功しました(ver3)"}E2Eテスト成功の3行が表示されます。最新コードでデプロイした場合は新しい出力が出ていれば正常です。

ステータスコード: 200E2Eテスト成功が確認できれば、Green環境への疎通とレスポンス内容の検証が正しく動いている状態です。

本番切り替え後の動作確認

Lambdaがテストに成功したため、ECSは次のステージ(PRODUCTION_TRAFFIC_SHIFT)に進み、本番トラフィックがGreenに切り替わります。APIを叩いて確認します。

curl http://<ALBのDNS名>/
{"message":"API呼び出しテストに成功しました(ver3)"}

(ver3)が返っていれば、Lambdaテスト経由の自動デプロイが成功しています。

6.9 失敗ケースの確認(自動ロールバック)

次に、わざとテストが失敗する状態を作って、自動ロールバックが動くことを確認します。ここでは、本番で起きがちなAPIは応答するが、レスポンスの内容が間違っているバグ」を再現します。ルートエンドポイント(/)が返すメッセージを「成功」→「失敗」に書き換え、LambdaのE2Eテストがこれを検知して自動ロールバックする流れを体験します。

💡 ポイント
バグは/healthではなく/(ルート)に仕込みます。/healthはALBのヘルスチェック対象なので、ここを壊すとALB側で先に異常検知され、タスクがそもそも起動完了しません(=デプロイがスケールアップ段階で止まる)。今回学びたいのは「ALBヘルスチェックは通り、HTTPステータスも200を返すのに、APIのレスポンス内容が間違っている」というケースをLambdaのE2Eテストで検知する流れです。これは現実の本番バグでよく起きるパターンで、ステータスコードだけ見ていては検知できません。

main.py/(ルート)エンドポイントを以下のように書き換え、メッセージを「成功しました」→「失敗しました」に変更します(バージョンはver4に更新)。新バージョン(ver4)として本番リリースを試みるが、Lambdaが想定外のメッセージを検知してロールバックされ、本番には到達しない、という流れを体験します。

@app.get("/")
def root():
    # バグを混入: メッセージ内容を「成功」→「失敗」に書き換え
    return {"message": "API呼び出しテストに失敗しました(ver4)"}
💡 ポイント
HTTPステータスコードは200のままで、エンドポイントは正常に応答しています。それでも、Lambdaはレスポンスのメッセージに想定の文字列API呼び出しテストに成功しましたが含まれているかを検証しているため、メッセージが「失敗しました」に変わっていることを検知してFAILEDを返します。

「200 OK = 健全」と単純に判定するのではなく、APIが期待通りの振る舞いをしているかまで踏み込んで検証するのがE2Eテストの本質です。これがALBヘルスチェック(疎通確認)との大きな違いで、Lambdaフックを組み合わせる価値もここにあります。

イメージをビルド・プッシュします。

docker build --platform linux/amd64 -t bg-handson-repo .
docker tag bg-handson-repo:latest <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest
docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest

続いて、フェーズ1と同じ手順でECSに新しいデプロイを開始させます。ECSのダッシュボードからbg-handson-clusterbg-handson-serviceを開き、サービスの更新をクリックします。新しいデプロイの強制にチェックを入れ、更新をクリックします。

デプロイの進行とロールバックを確認

ECSコンソールのデプロイタブで進行状況を確認します。今度はGreen環境でTEST_TRAFFIC_SHIFTステージに入ったタイミングでLambdaが呼ばれ、FAILEDが返るため、ECSが自動的にロールバックを開始します。

デプロイ開始直後はスケールアップ段階にあり、新しいGreenタスクが起動中の状態です。

CloudWatch LogsでLambdaの失敗ログを確認

Lambdaのロググループ(/aws/lambda/bg-handson-e2e-test)を開いて、最新のログストリームを確認します。Green環境の/(ルート)がステータス200で応答しているが、メッセージが想定と異なることをLambdaが検知して失敗判定している様子が残っています。

テストURL: http://<ALBのDNS名>/
ステータスコード: 200
レスポンス: {"message":"API呼び出しテストに失敗しました(ver4)"}
E2Eテスト失敗

E2Eテスト失敗が確認できれば、Lambdaが期待通り「メッセージが想定と違う」を理由にFAILEDを返している状態です。

ECSによる自動ロールバックの確認

LambdaがFAILEDを返した結果、ECSは本番切り替えを行わずに自動ロールバックを実行します。デプロイ画面では「進行中のデプロイ」が「前回のデプロイ」セクションに移動し、上部に以下のような警告メッセージが表示されます。

Service deployment rolled back because TEST_TRAFFIC_SHIFT lifecycle hook(s) failed.
Lifecycle hook target arn:aws:lambda:....:function:bg-handson-e2e-test returned FAILED status.

APIから前バージョンが返ることを確認

最後に、本番に向けてAPIを叩いて、本番ルート(ヘッダーなし)が前のバージョン(ver3)のままであることを確認します。

curl http://<ALBのDNS名>/
{"message":"API呼び出しテストに成功しました(ver3)"}

(ver3)のままであれば、ver4のバグ入りコードはGreen環境までは起動し、HTTPレスポンスも200を返したものの、Lambdaがメッセージ内容の異常を検知して本番切り替えを止め、自動でロールバックされたことが確認できました。本番のメッセージがver4に切り替わらずver3のままになっていることが、ロールバックが正しく動いた証拠です。ユーザには一切バグが見えない状態で、安全にリリースを防ぐことができています。

これがBlue/Greenデプロイメントとライフサイクルフックを組み合わせた完成形です。

コードを正常な状態に戻す

フェーズ4に進む前に、先ほどバグを仕込んだ/(ルート)エンドポイントを元の正常なコードに戻しておきます。フェーズ4でも続けてmain.pyを編集していくため、出発点を「テストが通る状態」に揃えておくのが目的です。バグ入りのまま進めてしまうと、Lambdaがメッセージ内容の異常を検知して自動ロールバックされ、フェーズ4で体験したい「手動承認で止まる挙動」が確認できなくなります。

main.py/エンドポイントを元に戻します。

@app.get("/")
def root():
    return {"message": "API呼び出しテストに成功しました(ver3)"}

この時点ではコードをローカルで戻しただけで、ECR上のイメージはまだバグ入りのままです。次のフェーズで新しいイメージをプッシュする際に、この正常なコードが使われるようになります。

7. フェーズ4: 手動承認の組み込み

フェーズ3で、自動テストの結果によって本番切り替えの可否を判定する仕組みができました。多くのシステムではこの構成で十分ですが、重要度の高いシステムではもう一段階安全装置を積みたいケースがあります。

たとえば以下のような場面です。

  • 画面の見た目・操作感など、自動テストでは判定できない要素の最終確認を行う場合。テストはAPIレスポンスを見るだけなので、表示崩れやユーザ体験までは判定できない
  • 「このタイミングでリリースして問題ないか」というビジネス要件の最終チェックは、人間の承認が必要になる
  • システムの重要度によっては「誰がいつ承認したか」の承認記録が求められる

このような要件に応えるため、このフェーズでは自動テストに加えて人間の手動承認が揃わないと本番切り替えが進まない仕組みを構築します。

7.1 構成の変更点

フェーズ3の「自動テスト合格 → 即本番切り替え」という仕組みに、人間の承認という新たなゲートを組み込みます。変更点は大きく3つで、それぞれがどう連携して「承認を待つ」挙動を実現するのかを理解しておくと、以降の実装がスムーズに進みます。

全体像を先に示すと、以下のようになります。ECSが30秒おきにメインLambdaを呼び出し、メインLambdaがDynamoDBの承認レコードを参照して結果を返す流れと、承認者がDynamoDBコンソールでレコードのステータスを直接書き換える流れの2系統が連携します。

graph TB
    ECS["ECS"] -->|"30秒おきに呼び出し"| ML["メインLambda"]
    ML -->|"自動テスト失敗"| F1["FAILED<br/>自動ロールバック"]
    ML -->|"テスト合格+承認済み"| S["SUCCEEDED<br/>本番切り替えへ進む"]
    ML -->|"テスト合格+却下"| F2["FAILED<br/>自動ロールバック"]
    ML -->|"テスト合格+承認待ち"| IP["IN_PROGRESS<br/>30秒後に再呼び出し"]
    IP -.->|"30秒後"| ECS

    Approver["承認者"] -->|"コンソールで status を<br/>approved/rejected に書き換え"| DB[("DynamoDB<br/>承認レコード")]
    DB -.->|"参照"| ML

    style ML fill:#e3f2fd,stroke:#1565c0
    style DB fill:#fff3e0,stroke:#e65100
    style S fill:#e8f5e9,stroke:#2e7d32
    style F1 fill:#ffebee,stroke:#c62828
    style F2 fill:#ffebee,stroke:#c62828
    style IP fill:#fffde7,stroke:#f57f17

ECSはメインLambdaがIN_PROGRESSを返すと同じステージで待機し、少し時間を置いてLambdaを再度呼び出します。このループによって「承認が来るまで本番切り替えを待つ」挙動が実現されます。

ここからは、3つの変更点を1つずつ詳しく見ていきます。

変更点1: DynamoDBに承認レコードを管理

まず、承認状態を保存する場所が必要になります。メインLambdaは30秒おきに呼び出されるため、「前回呼び出された時点で承認されたかどうか」をどこかに永続化しておく必要があるためです。この保存場所として、CloudFormationで事前に作成してあるbg-handson-approvalsというDynamoDBテーブルを利用します。

DynamoDBのレコードは、1件のデプロイメント(=1回の本番切り替え試行)に対して1レコードを持ちます。パーティションキーはdeployment_idで、ECSがライフサイクルフックから渡してくるtargetServiceRevisionArn(サービスリビジョンのARN)をそのまま利用します。サービスリビジョンARNはデプロイごとに一意のため、デプロイメントを識別するキーとして使えます。

レコードが持つステータスは以下の3種類です。

ステータス 意味 メインLambdaの挙動
pending 承認待ち(新規デプロイ時の初期値) IN_PROGRESSを返し、30秒後に再度呼び出される
approved 承認済み SUCCEEDEDを返し、本番切り替えへ進む
rejected 却下 FAILEDを返し、自動ロールバックへ進む
📝 DynamoDBについて
DynamoDBはAWSが提供するフルマネージドのNoSQLデータベースです。スキーマレスでレコード単位のデータ管理ができ、スケールや運用負荷を気にせず使えるのが特長です。今回は「承認レコード」を保管する用途で利用します。承認履歴を自動的に残せるため、監査要件のあるシステムにも対応しやすい利点があります。サービス全体の概要はWhat is Amazon DynamoDB?(AWS公式ドキュメント)に記載があります。

このハンズオンの目的はDynamoDB自体を習得することではなく、承認フローを実現するための手段として利用する点に注意してください。DynamoDBをより体系的に学びたい方は、後続のサーバレス章やデータ分析章で扱う予定です。

変更点2: メインLambdaの拡張

次に、フェーズ3で作成したメインLambda(bg-handson-e2e-test)を拡張して、自動テスト合格後に承認ステータスも確認するようにします。拡張後のメインLambdaが担う役割は以下のとおりです。

処理順 内容
1 テストルート(X-Test-Traffic: trueヘッダー付き)経由でGreen環境にリクエストし、自動テストを実行
2 テスト失敗 → FAILEDを返してデプロイ中止(フェーズ3と同じ挙動)
3 テスト合格 → DynamoDBから該当デプロイメントの承認レコードを取得(なければ新規作成してpendingにする
4 pendingIN_PROGRESSを返して次回呼び出しを待つ
5 approvedSUCCEEDEDを返して本番切り替えへ進む
6 rejectedFAILEDを返して自動ロールバック

特に重要なのが、IN_PROGRESSを返すことで承認待ち状態を表現している点です。フェーズ3の解説にあった「ライフサイクルフックの戻り値3種類」のうち、これまで使ってこなかったIN_PROGRESSがここで活きてきます。ECSはIN_PROGRESSを受け取ると、一定間隔(デフォルト30秒)で同じLambdaを何度も呼び出すため、承認が行われるまでデプロイが停止したように見える挙動になります。

また、処理順3で「レコードが無ければ新規作成してpendingにする」としている点にも意味があります。この承認フローでは、時系列的に最初に動くのはメインLambdaだからです。

順番 出来事 誰が動く
1 デプロイがTEST_TRAFFIC_SHIFTステージに到達 ECSがメインLambdaを呼ぶ(最初に動くのはこのLambda
2 メインLambdaがレコード無しを検知 → pendingレコードを作成 → IN_PROGRESSを返す メインLambda
3 承認者が承認操作を行う 承認者(DynamoDBコンソールでstatusを直接書き換える)
4 ECSが30秒間隔でメインLambdaを再呼び出し、承認レコードを再確認 ECS + メインLambda

つまり、「レコードを最初に作れる立場にあるのはメインLambda」です。承認者がコンソールで書き換えるのはpendingレコードのstatus属性のみのため、もしメインLambda側で初回put_itemを省略すると、コンソールで書き換える対象レコードがそもそも存在せず承認フローが成立しません。そのため、メインLambdaが「あれば取得、無ければ新規作成」というget_or_createパターンで振る舞う必要があります。

変更点3: 承認操作(DynamoDBレコードの直接更新)

最後に、承認者(人間)がDynamoDBのステータスを更新する手段を用意します。本ハンズオンでは、シンプルにDynamoDBコンソールから直接レコードのstatus属性をapprovedまたはrejectedに書き換える方式を採用します。

💡 ポイント
本ハンズオンでは、Blue/Greenデプロイメントの仕組みを掴むことを優先するため、承認操作を「DynamoDBコンソールでレコードを直接編集する」というシンプルな形にしています。ただし実業務では、以下の理由から承認専用のLambda関数を用意し、その関数経由でDynamoDBを更新するのが望ましい です。
  • 操作ミスの抑止:コンソール直編集だとstatus値(approved/rejected)のtypoや別属性の誤上書きが起きやすい。Lambdaなら入力検証を仕込める
  • 最小権限の付与:承認者にDynamoDBの編集権限を直接渡すよりも、承認Lambdaの実行権限だけを渡すほうが権限を絞れる
  • 承認UIの拡張性:Lambdaを介する形にしておくと、Slackボタン・メール承認URL・社内管理画面・AWS Systems Manager Change Managerなどの承認UIを後から差し替えやすい
  • 監査証跡の自動化updated_atdecided_bycommentなどの属性をLambda側で自動付与できる。コンソール直編集だと属性追加を毎回手動で行う必要があり抜け漏れが起きやすい
重要なのは「承認状態をDynamoDBに永続化し、メインLambdaが参照する」という基本構造です。この構造を知っておけば、承認UIだけを差し替えて様々な承認フローに応用できます。

7.2 Lambdaロールの権限追加

フェーズ3で作成したメインLambda実行ロールには、DynamoDBを読む権限がまだありません。権限を追加します。

IAMのダッシュボードでロールからbg-handson-lambda-roleを選択し、許可を追加 > インラインポリシーを作成をクリックします。

JSONエディタに以下のポリシーを貼り付けます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:GetItem",
                "dynamodb:PutItem"
            ],
            "Resource": "arn:aws:dynamodb:*:*:table/bg-handson-approvals"
        }
    ]
}

ポリシー名をbg-handson-dynamodb-policyとして、ポリシーの作成をクリックします。

7.3 メインLambdaの拡張

まずは、フェーズ3で作成したメインLambdabg-handson-e2e-test)を拡張し、自動テスト合格後にDynamoDBの承認ステータスを確認するようにします。承認フローを成立させるためには、最初に動くメインLambda側で承認レコードのget_or_createまで担う必要があります。

Lambdaコンソールでbg-handson-e2e-testを開き、コードタブでlambda_function.pyの内容を以下のように書き換えます。

import json
import os
import urllib.request
from datetime import datetime, timezone

import boto3

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("bg-handson-approvals")


def lambda_handler(event, context):
    print(f"Received event: {json.dumps(event)}")

    # フェーズ3から引き継ぎ:自動テストの実行
    test_url = get_test_url()
    if not run_test(test_url):
        return {
            "hookStatus": "FAILED",
            "statusReason": "Automated test failed",
        }

    # 追加:承認状態を確認する
    deployment_id = get_deployment_id(event)
    if not deployment_id:
        return {
            "hookStatus": "FAILED",
            "statusReason": "Could not extract deployment_id from event",
        }
    status = get_or_create_approval_status(deployment_id)
    print(f"Approval status: {status}")

    if status == "approved":
        return {
            "hookStatus": "SUCCEEDED",
            "statusReason": "Test passed and approved",
        }
    elif status == "rejected":
        return {
            "hookStatus": "FAILED",
            "statusReason": "Approval rejected",
        }
    else:
        # pending: 承認待ち。30秒後に再度呼び出してもらう
        return {
            "hookStatus": "IN_PROGRESS",
            "callBackDelay": 30,
        }


def get_deployment_id(event):
    """ECSライフサイクルフックのpayloadからデプロイメントIDを取得する。
    AWSのpayload構造(新しい平坦構造/古いネスト構造)の両方に対応する。
    """
    # 新しい平坦構造(AWS公式ドキュメント準拠)
    if "targetServiceRevisionArn" in event:
        return event["targetServiceRevisionArn"]
    # 古いネスト構造へのフォールバック
    details = event.get("executionDetails", {})
    return details.get("targetServiceRevisionArn") or details.get("serviceDeploymentId")


def get_test_url():
    """テストルート(X-Test-Traffic: trueヘッダー付き)のURLを組み立てる(フェーズ3と同じ)"""
    alb_dns_name = os.environ.get("ALB_DNS_NAME", "")
    if not alb_dns_name:
        return ""
    return f"http://{alb_dns_name}/"


def run_test(url):
    """テストルート経由でGreen環境の動作確認を行う(フェーズ3と同じ)"""
    if not url:
        return False
    try:
        req = urllib.request.Request(
            url,
            method="GET",
            headers={"X-Test-Traffic": "true"},
        )
        with urllib.request.urlopen(req, timeout=5) as response:
            data = json.loads(response.read())
            return "API呼び出しテストに成功しました" in data.get("message", "")
    except Exception as e:
        print(f"Test failed with exception: {e}")
        return False


def get_or_create_approval_status(deployment_id):
    """DynamoDBから承認状態を取得する。初回呼び出し時はレコードを作成する"""
    response = table.get_item(Key={"deployment_id": deployment_id})

    if "Item" in response:
        return response["Item"]["status"]

    # 初回呼び出し:pendingレコードを作成する
    now = datetime.now(timezone.utc).isoformat()
    table.put_item(
        Item={
            "deployment_id": deployment_id,
            "status": "pending",
            "created_at": now,
        }
    )
    return "pending"

追加したコードを解説します。

def get_deployment_id(event):
    if "targetServiceRevisionArn" in event:
        return event["targetServiceRevisionArn"]
    details = event.get("executionDetails", {})
    return details.get("targetServiceRevisionArn") or details.get("serviceDeploymentId")

ECSから渡されるeventからサービスリビジョンのARNを取得し、これをデプロイメントの一意な識別子として利用します。AWS公式のライフサイクルフックペイロード仕様では、targetServiceRevisionArnが「デプロイ対象のサービスリビジョンのARN」としてevent直下に定義されており、デプロイごとに一意になるためIDとして使えます。

💡 ポイント
AWS公式のライフサイクルペイロード仕様では、targetServiceRevisionArnを含む各フィールドはイベントの直下に置かれた平坦構造として定義されています。本関数では将来的なpayload仕様の変更に備えてフォールバックも用意していますが、現時点では1つ目の分岐(event直下から取得)で取得できる想定です。実際に届いているpayload構造は、LambdaコンソールのモニタリングタブからCloudWatch Logsを表示Received event: のログ行を確認できます。
deployment_id = get_deployment_id(event)
if not deployment_id:
    return {"hookStatus": "FAILED", ...}
status = get_or_create_approval_status(deployment_id)

取得したデプロイメントIDに紐づく承認レコードをDynamoDBから取得し、レコードがなければ新規作成してステータスpendingでスタートします。万が一payload構造が想定外でデプロイメントIDが取得できなかった場合はFAILEDを返してロールバックさせます。

if status == "approved":
    return {"hookStatus": "SUCCEEDED", ...}
elif status == "rejected":
    return {"hookStatus": "FAILED", ...}
else:
    return {"hookStatus": "IN_PROGRESS", "callBackDelay": 30}

承認ステータスに応じて3つのパターンで分岐しています。

  • approved: 承認済み → SUCCEEDEDで本番切り替えに進む
  • rejected: 却下された → FAILEDでロールバック
  • それ以外(pending): まだ承認されていない → IN_PROGRESSで30秒後に再度呼び出し
def get_or_create_approval_status(deployment_id):
    response = table.get_item(Key={"deployment_id": deployment_id})
    if "Item" in response:
        return response["Item"]["status"]
    # 初回呼び出し:pendingレコードを作成する
    ...

この関数は初回呼び出しのタイミングで承認待ちレコードを作成しています。メインLambdaは30秒おきに呼ばれますが、レコード作成は最初の1回だけで、以降は既存レコードのステータスを返します。

コードを変更したら、DeployボタンをクリックしてLambdaに反映します。

7.4 動作確認:承認待ちの状態を作る

それでは、承認フローを実際に体験します。まずmain.pyのメッセージを(ver5)に書き換えます。

@app.get("/")
def root():
    return {"message": "API呼び出しテストに成功しました(ver5)"}

イメージをビルド・プッシュします。

docker build --platform linux/amd64 -t bg-handson-repo .
docker tag bg-handson-repo:latest <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest
docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/bg-handson-repo:latest

続いて、フェーズ1と同じ手順でECSに新しいデプロイを開始させます。ECSのダッシュボードからbg-handson-clusterbg-handson-serviceを開き、サービスの更新をクリックします。新しいデプロイの強制にチェックを入れ、更新をクリックします。

ECSのコンソールでbg-handson-serviceデプロイタブを開きます。スケールアップが完了したあと、TEST_TRAFFIC_SHIFTステージで停止したように見えるはずです。

💡 ポイント
TEST_TRAFFIC_SHIFTステージで長く停滞しているように見えるかもしれませんが、これは正しい挙動です。メインLambdaが30秒おきに呼び出されてDynamoDBの承認ステータスを確認し、pendingの間はIN_PROGRESSを返し続けているためです。手動承認を待つデプロイなので、むしろこの状態になるべきです。

7.5 DynamoDBで承認レコードを確認

メインLambdaがTEST_TRAFFIC_SHIFTステージで初回呼び出しされたタイミングで、DynamoDBにpendingレコードが自動作成されています。動作確認や承認操作に進む前に、レコードが正しく作成されたことを確認します。

AWSマネジメントコンソールでDynamoDBを開き、左側のメニューからテーブルを選択し、bg-handson-approvalsをクリックします。

画面右上のテーブルの詳細を表示から表示モードに切り替えるか、ナビゲーションペイン内の項目を探索を開くと、メインLambdaが作成したレコードが1件表示されているはずです。deployment_idにサービスリビジョンのARNが入り、statuspendingになっていれば、承認待ちの状態が正しく作られています。

このレコードに対して、後ほど承認/却下の判断結果としてstatusを書き換えていきます。

7.6 テストルートで動作確認

承認を行う前に、テストルート(X-Test-Traffic: trueヘッダー付き)でGreen環境を動作確認します。テストルールはフェーズ3までの実装でGreen側に接続されているため、ver5の動作を確認できます。

テストルートと本番ルートの両方にアクセスしてみます。

curl -H "X-Test-Traffic: true" http://<ALBのDNS名>/
{"message":"API呼び出しテストに成功しました(ver5)"}
curl http://<ALBのDNS名>/
{"message":"API呼び出しテストに成功しました(ver3)"}

この時点での経路ごとの状態は以下のとおりです。

経路 接続先 期待される結果
ヘッダーなし(本番ルール) 旧TG(ver3が稼働中) {"message":"API呼び出しテストに成功しました(ver3)"}
X-Test-Traffic: trueヘッダー付き(テストルール) 新TG(ver5が稼働中) {"message":"API呼び出しテストに成功しました(ver5)"}

ECSは承認が来るまで本番切り替えを止めている状態です。実業務ではこのタイミングで、画面の見た目やビジネスロジックなど自動テストではカバーできない確認を行います。

7.7 DynamoDBで承認状態を更新する

動作確認に問題がなければ、先ほど確認したpendingレコードのstatusapprovedに書き換えて、本番切り替えを許可します。

bg-handson-approvalsテーブルの一覧画面で、該当レコードのチェックボックスを選択し、右上のアクションプルダウンから項目の編集をクリックします。

項目の編集画面が表示されます。status属性の値をpendingからapprovedに書き換え、画面右下の保存して閉じるをクリックします。

属性 操作 変更後の値
status 値を編集 approved
💡 ポイント
任意でupdated_at(書き換え日時)、decided_by(承認者の識別子)、comment(レビューコメント)の3属性を新しい属性の追加から手動で足しておくと、承認履歴として残せます。実業務ではこの3属性をLambdaが自動で書き込みますが、本ハンズオンでは「シンプルに動かす」ことを優先して手動操作の説明にとどめています。

一覧画面に戻り、画面上部に「項目は正常に保存されました。」のメッセージが表示され、該当レコードのstatus列がapprovedに変わっていれば、書き換え成功です。

7.8 本番切り替え後の動作確認

DynamoDBレコードの書き換えから最大30秒以内に、メインLambdaがDynamoDBの変更を検知してSUCCEEDEDを返します。すると ECS は次のライフサイクルステージであるPRODUCTION_TRAFFIC_SHIFTを実行し、本番ルールが新しいタスクのターゲットグループに切り替わります。本番切り替えが完了すると、続いてベイク時間段階(旧タスクを30分間残しておく期間)に入ります。

ECSサービスbg-handson-serviceデプロイタブを開くと、進行中のデプロイの「現在のデプロイ段階」がベイク時間になっていることが確認できます。ここまで進んでいれば、PRODUCTION_TRAFFIC_SHIFTは通過済みで本番リクエストが新バージョンに切り替わった状態です。

この状態で、両方の経路にアクセスして本番側もver5に切り替わっていることを確認します。

curl http://<ALBのDNS名>/
{"message":"API呼び出しテストに成功しました(ver5)"}
curl -H "X-Test-Traffic: true" http://<ALBのDNS名>/
{"message":"API呼び出しテストに成功しました(ver5)"}
経路 接続先 期待される結果
ヘッダーなし(本番ルール) 新TG(ver5が稼働中) {"message":"API呼び出しテストに成功しました(ver5)"}
X-Test-Traffic: trueヘッダー付き(テストルール) 新TG(ver5が稼働中) {"message":"API呼び出しテストに成功しました(ver5)"}

両方のルールが(ver5)に切り替わりました。これで「自動テスト + 手動承認」のゲートを通過した安全なデプロイが完了です。

7.9 参考:却下した場合の挙動

本ハンズオンでは承認するフローを体験しました。もし動作確認で問題が見つかった場合は、DynamoDBコンソールでstatus属性をapprovedの代わりにrejectedに書き換えることで、デプロイを却下できます。

属性 操作 変更後の値
status 値を編集 rejected

ステータスがrejectedに変わると、次にメインLambdaが呼び出されたタイミングでFAILEDが返り、ECSが自動的にロールバックします。本番ルールは旧バージョンのままで、ユーザには新バージョンが一切見えない状態で安全にデプロイを止めることができます。

実業務では「レビューで懸念が見つかったので却下」「セキュリティチームから待ったがかかった」などのケースで活用されます。任意で追加できるcomment属性に却下理由を残しておくことで、後から「なぜこのデプロイは却下されたか」を遡って確認できる監査証跡としても機能します。

7.10 参考:承認されないまま放置した場合

各ライフサイクルステージには24時間のタイムアウトがあり、さらにデプロイ全体としても36時間以内に完了する必要がありますAmazon ECS linear deployments(AWS公式ドキュメント))。承認レコードがpendingのまま放置されてこれらの上限を超えると、デプロイが自動的に失敗し、ロールバックされます。これは「承認を忘れている」「悪意ある第三者が承認を避けている」といった異常ケースへの安全装置です。本ハンズオンではこの上限経過までは扱いませんが、実運用ではこの安全装置があることを覚えておいてください。

7.11 承認履歴を残せるのがDynamoDBの強み

この仕組みの副次的なメリットとして、DynamoDBに承認履歴を残せることが挙げられます。承認操作の都度、以下のような属性をレコードに残しておくことで、後からデプロイ判断の経緯を辿れます。

属性 記録内容
deployment_id どのデプロイに対する承認か
status approved / rejected
created_at / updated_at 承認待ち開始から判断までの所要時間
decided_by 操作主体(承認者の識別子)
comment 承認/却下の根拠を表すレビューコメント

これらの履歴は、以下のような運用に応用できます。

  • 重要システムの監査証跡として活用(誰が・いつ・なぜ承認したかの記録)
  • インシデント時の遡及調査(過去のどのデプロイで問題が混入したかの特定)
  • 承認所要時間の統計分析created_atupdated_atの差分の平均)

本ハンズオンではstatus属性のみを手動で書き換える形にしていますが、実運用では承認専用Lambda(変更点3の💡 ポイント参照)がupdated_atdecided_bycommentを自動で書き込むよう設計するのが一般的です。decided_byにはSlackユーザ名・社員ID・メールアドレスなど実際の承認者の識別子が入ります。

8. リソースの削除

最後に、AWSのリソースを削除します。AWSのリソースは起動している間ずっと料金が発生し続けるため、必ず削除しておきましょう。

このハンズオンで作成したリソースは、ECSクラスター・ECRリポジトリ・VPC・IAMロールなど CloudFormationで自動作成したもの と、ECSサービス・ALB・ターゲットグループ・タスク定義・Lambda関数など マネジメントコンソールから手動作成したもの に分かれています。手動作成したものはCloudFormationの管理外のため、スタックを削除しても自動で消えません。さらに、ECSクラスターはサービスがアクティブな状態だと削除できないなど、リソース間に依存関係があるため、手動作成したものを先に削除し、最後にCloudFormationスタックを削除する 順序で進めます。

8.1 ECSサービスの削除

最初にECSサービスを削除します。ECSサービスはタスクが動いている状態だと削除できないため、まず 必要なタスク数を0 にしてタスクを停止させてから、サービス本体を削除します。

ECSのダッシュボードからbg-handson-clusterを開き、サービスタブでbg-handson-serviceにチェックを入れて、画面右上の更新をクリックします。

サービスの更新画面が開きます。必要なタスクの入力欄を0に変更し、画面下部の更新をクリックして保存します。

タスクが停止するまでしばらく待ちます。bg-handson-serviceの詳細画面を開き、タスクタブで「タスクがありません」と表示され、タスク (0 個が適切) になっていれば、タスクの停止が完了しています。

この状態になったら、画面右上のサービスを削除をクリックします。確認画面で「削除」と入力して削除をクリックすると、サービスが削除されます。サービス一覧からbg-handson-serviceが消えていれば完了です。

8.2 ALBとターゲットグループの削除

ECSサービスを削除しただけでは、サービスが利用していたALB(ロードバランサー)とターゲットグループは残ったままになります。これらも手動作成したリソースのため、続けて削除します。順序としては、ALBがターゲットグループを参照している ため、先にALBを削除してからターゲットグループを削除します。

EC2のダッシュボードを開き、左メニューからロードバランサーを選択します。bg-handson-albにチェックを入れ、アクション > ロードバランサーの削除をクリックします。確認画面で「確認」と入力して削除を実行します。

続いて、ターゲットグループを削除します。EC2のダッシュボードの左メニューからターゲットグループを選択し、bg-handson-blue-tgbg-handson-green-tgの両方にチェックを入れます。アクション > 削除をクリックし、確認画面で削除を実行します。

ロードバランサー一覧・ターゲットグループ一覧の両方からbg-handson-で始まるリソースが消えていれば完了です。

8.3 タスク定義の登録解除

ECSのタスク定義は「アクティブ」状態のままだと完全には消えないため、まず登録解除を行います。タスク定義は履歴として残り続けますが、登録解除しておけば新しいデプロイの対象から除外され、料金には影響しません。

ECSのダッシュボードから左メニューのタスク定義を開き、bg-handson-taskをクリックします。リビジョン一覧画面で全リビジョンにチェックを入れ、アクション > 登録解除をクリックして実行します。

リビジョンのステータスが「非アクティブ」になっていれば完了です。

8.4 Lambda関数とIAMロールの削除

フェーズ2以降で作成したLambda関数と、Lambda・ECSのライフサイクルフック用に作成したIAMロールも、CloudFormationの管理外のため手動で削除します。

まずLambda関数を削除します。Lambdaのダッシュボードを開き、bg-handson-e2e-test(フェーズ4まで進めた場合は手動承認のメインLambda関数)にチェックを入れます。アクション > 削除をクリックし、確認画面で「delete」と入力して削除を実行します。

次に、IAMロールを削除します。IAMのダッシュボードからロールを開き、検索ボックスで bg-handson-lambda と入力します。bg-handson-lambda-roleにチェックを入れて、画面右上の削除をクリックします。

同じ要領で、bg-handson-lifecycle-hook-roleも削除します。検索ボックスで bg-handson-lifecycle-hook-role と入力し、ロールにチェックを入れて削除をクリックします。

IAMロール一覧にbg-handson-で始まるロールが残っていなければ完了です。

なお、DynamoDBテーブルbg-handson-approvals)はCloudFormationで管理しているため、後述のCloudFormationスタック削除時に自動で削除されます。

8.5 ECRリポジトリのイメージ削除

ECRリポジトリ自体はCloudFormationで管理していますが、リポジトリ内に プッシュされたコンテナイメージはCloudFormationの管理外 のため、イメージが残ったままだとスタック削除時にリポジトリ削除が失敗します。先にイメージをすべて削除しておきます。

ECRのダッシュボードを開き、bg-handson-repoリポジトリをクリックします。イメージタブで一覧の左上のチェックボックスをクリックして全イメージを選択し、削除をクリックします。

イメージ一覧が空になっていれば完了です。

8.6 CloudFormationスタックの削除

ここまでで手動作成したリソースの削除が完了しました。最後にCloudFormationスタックを削除すると、CloudFormationで作成した残りのリソース(VPC・サブネット・セキュリティグループ・ECSクラスター・ECRリポジトリ・DynamoDBテーブル・IAMロールなど)がすべてまとめて削除されます。

CloudFormationのダッシュボードを開き、bg-handsonスタックの左にあるラジオボタンを選択して、スタックを削除をクリックします。確認画面が表示されたら、そのまま削除を実行します。

スタックの削除には数分かかります。ステータスがDELETE_COMPLETEになれば、すべてのリソースが削除されています。

9. 参考: 同じ構成をTerraformで作る

ここまでのハンズオンでは、CloudFormation + マネジメントコンソール + AWS CLIを使って段階的にBlue/Greenデプロイの環境を構築しました。参考として、フェーズ4までを含めた最終形(自動テスト + 手動承認)をTerraformで一括構築する構成も紹介します。

Terraformで管理すると、今回ハンズオンで手動作成したALB・ターゲットグループ・ECSサービス・DynamoDB・Lambda・IAMロールまで1つのコードベースで管理でき、環境の再構築や複製、変更の追跡が容易になります。

9.1 Terraform AWS Providerのバージョン

本セクションのコードは、AWS Provider v6.40以降を前提としています。ECSのBlue/Green戦略をTerraformで定義するためのdeployment_configuration.strategy = "BLUE_GREEN"や、ライフサイクルフック用のlifecycle_hookブロック、load_balancer.advanced_configurationブロックは比較的新しい機能です。詳しくはTerraform AWS Provider 公式ドキュメント(aws_ecs_service)に記載があります。

9.2 コードのダウンロード

以下のボタンから、Terraformコード一式をダウンロードしてください。

9.3 ファイル構成

ZIPを解凍すると、以下の構成のフォルダができます。

terraform-bg-handson/
├── provider.tf          # Terraformとプロバイダのバージョン指定
├── variables.tf         # 入力変数(リージョン・プレフィックス)
├── network.tf           # VPC、サブネット、IGW、ルートテーブル、セキュリティグループ
├── ecr.tf               # ECRリポジトリ
├── iam.tf               # 各種IAMロール(タスク実行・インフラ・Lambda・フック呼び出し)
├── logs.tf              # CloudWatch Logs
├── alb.tf               # ALB、ターゲットグループ、リスナー、リスナールール
├── dynamodb.tf          # 承認レコード用DynamoDBテーブル
├── lambda.tf            # Lambda関数(自動テスト + 承認ステータス確認)
├── ecs.tf               # ECSクラスタ、タスク定義、サービス(Blue/Green設定)
├── outputs.tf           # 出力値(ALBのDNS名、ECRのURI、DynamoDBテーブル名など)
└── lambda/
    └── e2e_test/
        └── lambda_function.py   # メインLambda(自動テスト + 承認ステータス確認)
💡 ポイント
Terraformはファイル分割のルールを持たないため、上記のようにリソースの種類ごとに.tfファイルを分けるのがよくあるスタイルです。小規模な構成では1ファイルにまとめても動きますが、数が増えてくると役割ごとに分けたほうが可読性が上がります。

9.4 ポイントとなるコードの解説

すべてのファイルをここで解説すると長くなるため、Blue/Greenデプロイメントならではの部分に絞って解説します。他のファイル(VPCやIAMロールなど)は標準的な書き方なので、実際のコードを読みながら確認してください。

ALBのリスナールール

ECSのBlue/Green戦略では、本番トラフィックとテストトラフィックを別々のリスナールールとして明示的に定義する必要があります。ハンズオン本編と同じく、ポート80の単一リスナーに「テストルール(優先度1: ヘッダー条件)」と「本番ルール(優先度2: 全パス)」の2つを並べます。Terraformではaws_lb_listener_ruleを2つ定義し、それぞれのARNをECSサービスに渡します。

# テストリスナールール(優先度1: 最優先で評価)
# X-Test-Traffic: true ヘッダー付きリクエストのみGreenへ転送する
resource "aws_lb_listener_rule" "test" {
  listener_arn = aws_lb_listener.main.arn
  priority     = 1

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.green.arn
  }

  condition {
    http_header {
      http_header_name = "X-Test-Traffic"
      values           = ["true"]
    }
  }
}

# 本番リスナールール(優先度2: テストルールにマッチしないリクエストを処理)
resource "aws_lb_listener_rule" "production" {
  listener_arn = aws_lb_listener.main.arn
  priority     = 2

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.blue.arn
  }

  condition {
    path_pattern {
      values = ["/*"]
    }
  }
}

テストルールではX-Test-Traffic: trueヘッダー付きのリクエストのみをGreen用ターゲットグループへ転送し、本番ルールではそれ以外のすべてのリクエストをBlue用ターゲットグループへ転送します。これらのリスナールールのARNを、後述のECSサービスのadvanced_configurationproduction_listener_ruletest_listener_ruleとして指定します。

💡 ポイント
ハンズオン本編ではマネジメントコンソールで「本番ルール=デフォルトルール、テストルール=優先度1」という設定にしましたが、ECS native Blue/Greenのproduction_listener_ruleにはリスナールールのARNが必要なため、Terraformでは本番ルールも明示的なaws_lb_listener_ruleとして定義しています。リスナーのデフォルトアクションはfixed-responseの404にしておき、どちらのルールにもマッチしなかった場合のフォールバックとします。

Lambda関数の自動パッケージング

Lambda関数のコードはlambda/e2e_test/lambda_function.pyに置き、Terraformのarchive_fileデータソースで自動的にZIP化します。複数Lambdaへ拡張する場合に備えて、関数名ごとのディレクトリ構成にしています。

data "archive_file" "e2e_test" {
  type        = "zip"
  source_dir  = "${path.module}/lambda/e2e_test"
  output_path = "${path.module}/build/e2e_test.zip"
}

resource "aws_lambda_function" "e2e_test" {
  function_name    = "${var.project}-e2e-test"
  role             = aws_iam_role.lambda.arn
  handler          = "lambda_function.lambda_handler"
  runtime          = "python3.13"
  timeout          = 60
  filename         = data.archive_file.e2e_test.output_path
  source_code_hash = data.archive_file.e2e_test.output_base64sha256

  environment {
    variables = {
      ALB_DNS_NAME    = aws_lb.main.dns_name
      APPROVALS_TABLE = aws_dynamodb_table.approvals.name
    }
  }
}

source_code_hasharchive_fileのハッシュ値を指定しているのがポイントです。これによりlambda_function.pyを書き換えると次のterraform applyでLambdaが自動的に更新されます。手動でZIP化する必要がないため、コードの修正からデプロイまでがスムーズです。

また、environment.variablesALBのDNS名とDynamoDBテーブル名を環境変数として注入しています。ハンズオン本編でもALB_DNS_NAMEはマネジメントコンソールから手動設定しましたが、Terraformではaws_lb.main.dns_nameを直接参照することで、ALBのDNS名がLambdaへ自動で連携されます。APPROVALS_TABLEについてはハンズオン本編でコード内にハードコードしていましたが、Terraformで管理する場合は環境変数経由で渡すほうが、テーブル名をコード変更なしに切り替えられるため柔軟です。

ECSサービスのBlue/Green設定

本ハンズオンの核心部分です。ECSサービスにdeployment_configurationブロックでBlue/Green戦略とライフサイクルフックを定義し、load_balancerブロック配下のadvanced_configurationでBlue/Green用のターゲットグループ・リスナールール情報を渡します。

resource "aws_ecs_service" "main" {
  name            = "${var.project}-service"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.main.arn
  desired_count   = 1
  launch_type     = "FARGATE"

  deployment_configuration {
    strategy             = "BLUE_GREEN"
    bake_time_in_minutes = 30

    lifecycle_hook {
      hook_target_arn  = aws_lambda_function.e2e_test.arn
      role_arn         = aws_iam_role.lifecycle_hook.arn
      lifecycle_stages = ["TEST_TRAFFIC_SHIFT"]
    }
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.blue.arn
    container_name   = "app"
    container_port   = 8000

    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_infrastructure.arn
    }
  }

  # ...network_configuration など...
}

コードを解説します。

  deployment_configuration {
    strategy             = "BLUE_GREEN"
    bake_time_in_minutes = 30

strategy = "BLUE_GREEN"で、このサービスにBlue/Greenデプロイメントを適用します。bake_time_in_minutesは、本番切り替え後に旧環境(Blue)を残しておく時間です。ハンズオンと同じく30分に設定しています。

    lifecycle_hook {
      hook_target_arn  = aws_lambda_function.e2e_test.arn
      role_arn         = aws_iam_role.lifecycle_hook.arn
      lifecycle_stages = ["TEST_TRAFFIC_SHIFT"]
    }

ライフサイクルフックの設定です。TEST_TRAFFIC_SHIFTステージでLambda関数を呼び出すことで、本番切り替え前にGreen環境へ自動テストを実行します。テストがFAILEDを返せばECSが自動的にロールバックするため、バグ入りのバージョンが本番に出る前に止められます。

  load_balancer {
    target_group_arn = aws_lb_target_group.blue.arn
    ...
    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_infrastructure.arn
    }
  }

load_balancerブロックのtarget_group_arnには現在(初期状態で)本番に紐づくターゲットグループ(Blue)を指定します。advanced_configurationでBlue/Green専用の情報を追加しています。

  • alternate_target_group_arn: デプロイ時に切り替えるもう片方のターゲットグループ(Green)
  • production_listener_rule: 本番トラフィックを振り分けているリスナールールのARN
  • test_listener_rule: テストトラフィック用のリスナールールのARN
  • role_arn: ECSがALBのターゲットグループを操作するためのIAMロール(AmazonECSInfrastructureRolePolicyForLoadBalancersをアタッチ済み)

この設定により、ECSはデプロイ時にどのリスナールールを書き換えて、どのターゲットグループを入れ替えるかを自動的に判断できるようになります。

承認フロー用のDynamoDB

フェーズ4の手動承認を支えるDynamoDBテーブルもTerraformで一緒に作成します。DynamoDBは承認レコードの永続化を担い、承認操作は本ハンズオンと同様に DynamoDBコンソールから直接status属性を書き換える 形を取ります。

resource "aws_dynamodb_table" "approvals" {
  name         = "${var.project}-approvals"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "deployment_id"

  attribute {
    name = "deployment_id"
    type = "S"
  }
}

DynamoDBテーブルはbilling_mode = "PAY_PER_REQUEST"にしているため、使った分だけの従量課金でキャパシティの事前設定が不要です。本ハンズオンのような低頻度アクセスのワークロードに向いています。

💡 ポイント
本ハンズオンでは承認をシンプルにDynamoDBコンソール直接更新で行う構成にしていますが、実業務では誤操作防止・最小権限の付与・承認UIの拡張性・監査証跡の自動化の観点から、承認専用のLambda関数を別途用意し、その関数経由でDynamoDBを更新する構成が望ましい です。Lambdaを増やす場合は、lambda.tfにもう1つaws_lambda_functionリソースを追加し、iam.tfにDynamoDBのUpdateItem権限を持つ専用ロールを定義する形になります。

9.5 使い方

ダウンロードしたフォルダ内で、以下のコマンドを順に実行します。

初回セットアップです。

terraform init

実行計画を確認します。

terraform plan

問題がなければ適用します。

terraform apply

terraform applyが完了すると、ハンズオンと同じリソース一式(VPC・ALB・ECS・DynamoDB・Lambda・IAM)が作成されます。あとはハンズオン本編と同じように、ECRへイメージをプッシュしてから、ECSコンソールで新しいデプロイの強制を実行すれば、Blue/Greenデプロイメントとライフサイクルフックによる自動テスト、そして手動承認フローまで動作します。

承認/却下はハンズオン本編と同様、DynamoDBコンソールでbg-handson-approvalsテーブルの該当レコードを開き、status属性をapprovedまたはrejectedに書き換える形で行います。

リソースを削除する際は、以下のコマンド一発で一括削除できます。

terraform destroy

CloudFormationやマネジメントコンソールでの個別削除と比べ、削除漏れが起きにくいのもTerraformで管理する大きなメリットです。

10. まとめ

このハンズオンでは、Amazon ECSのネイティブBlue/Greenデプロイメント機能を使った安全なデプロイの仕組みを、段階的に体験しました。

  • ECSのネイティブBlue/Green機能により、CodeDeployなしでBlue/Greenデプロイが実現できる
  • ALBのポート80リスナーに本番ルール(デフォルトルール)とテストルールX-Test-Traffic: trueヘッダーベース)の2つのルールを構成し、Green環境を事前に検証できる経路を用意する
  • ECSサービスにBlue/Green戦略を設定することで、update-serviceの実行だけでGreen起動 → 本番切り替え → ベイクタイム → Blue終了、という一連のライフサイクルが自動で進む
  • ベイクタイムを長めに取ることで、問題発覚時に手動ロールバックする時間を確保できる
  • ECSのライフサイクルフックにLambda関数を設定することで、本番切り替え前に自動テストを挟み、失敗時は自動ロールバックまで行える
  • LambdaがIN_PROGRESSを返しながらDynamoDBの承認レコードをチェックすることで、自動テスト + 手動承認を組み合わせた重要システム向けのフローも実現できる
  • 承認レコードをDynamoDBで管理することで、承認履歴も自動で残せて監査要件に対応しやすい
  • 実務では承認操作をSlackボタンやメール承認URLに置き換えるパターンが主流だが、ベースとなる「Lambdaで承認状態を管理する」仕組みは本ハンズオンと同じ
  • 実務では本ハンズオンの手動デプロイをコンテナのビルドとデプロイを自動化しようで学んだGitHub Actionsと組み合わせ、CI/CDパイプライン全体として自動化するのが一般的