0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECSの組み込みBlue/Greenデプロイと「ライフサイクルフック」を公式サンプルLambdaで試す

Posted at

0. はじめに

2025年7月、ECSの新機能として、組み込みBlue/Greenデプロイが発表されました。

従来、ECSでBlue/Greenデプロイを行うにはCodeDeployとの連携が必要でしたが、ECSネイティブのBlue/Greenデプロイが可能になりました。
この新機能の最大の特徴は、デプロイプロセスを詳細に制御できる「ライフサイクルフック」という仕組みが統合されたことです。
デプロイの特定のタイミングでLambda関数を呼び出し、デプロイプロセスを一時停止させ、検証ロジックを挟んだりすることができます。

本記事では、この新しいデプロイ機能を試し、AWS公式が提供しているサンプルLambdaを組み合わせて、その動作と設定方法を確認します。

本記事で扱うサンプルLambda

Lambda 役割 フックポイント
admissionFunction デプロイ前にコンテナイメージを検証し、ECR以外のイメージを拒否 スケールアップ前
approvalFunction S3に承認ファイルが置かれるまでデプロイを一時停止 テストトラフィック移行後

1. 「ライフサイクルフック」とは

ライフサイクルフックは、ブルーグリーンデプロイのプロセスを「ライフサイクルステージ」と呼ばれる複数のステップに分割し、各ステージの前後でLambda関数を実行できる機能です。

1-1. ブルーからグリーンへの移行プロセス

ライフサイクルフックの詳細説明の前に、デプロイ開始前から完了までのトラフィックとタスクの状態遷移を説明します。

Phase 0. 新環境デプロイ前(初期状態):

通常の稼働状態です。本番リスナーとテストリスナーへのトラフィックは、Blueに流れています。

image.png

Phase 1. Greenのデプロイ

新しいタスク定義でGreenが起動します。
この時点では本番トラフィックはBlueのままで影響を受けません。

image.png

Phase 2. テストトラフィック移行

テストリスナーのトラフィックがGreenに振り向けられます。
ライフサイクルフックを利用することで、この段階でGreenタスクの稼働確認を実施できます。

image.png

Phase 3. 本番トラフィック移行

本番リスナーのトラフィックがBlueからGreenに切り替えられます。

image.png

Phase 4. Blueの停止

本番トラフィックがGreenに移行した後、指定した「ベイク時間」が経過すると、Blueが終了します。
ベイク時間とは、本番トラフィック移行後にBlueを維持する期間です。この間に問題が発生した場合、すぐにBlueへロールバックできます。デフォルトは15分です。

image.png

1-2. ライフサイクルステージの詳細

ライフサイクルフックにより、上記のデプロイプロセスの「特定のタイミング」でLambdaを呼び出せます。
フックを設定しなければ、単にBlueからGreenに切り替わっていくだけの動きになります。

フックを設定できる主なライフサイクルステージは以下の通りです。

ステージ タイミング 主な用途
スケールアップ前(PRE_SCALE_UP) Greenタスク起動直前 デプロイ前検証(本記事で使用)。イメージのガバナンスチェック等
スケールアップ後(POST_SCALE_UP) Greenタスクがhealthyになった直後 タスク自体の初期テスト(ALB経由ではない)
テストトラフィック移行(TEST_TRAFFIC_SHIFT) テストリスナー移行中 ロールバック時にも呼び出される
テストトラフィック移行後(POST_TEST_TRAFFIC_SHIFT) テストリスナー移行完了直後 手動承認・自動テスト(本記事で使用)。最も重要なフックポイント
本番トラフィック移行(PRODUCTION_TRAFFIC_SHIFT) 本番リスナー移行中 ロールバック時にも呼び出される
本番トラフィック移行後(POST_PRODUCTION_TRAFFIC_SHIFT) 本番トラフィック移行完了直後 デプロイ完了の確認・通知

注意: 各ライフサイクルステージは最大24時間まで続きます。フックの実行により24時間を超えても完了しない場合、デプロイはタイムアウトし、自動的にロールバックされます。

1-3. Lambdaが返す「hookStatus」とは?

ライフサイクルフックで呼び出されたLambda関数は、ECSに対してデプロイをどうするか、3つの状態(hookStatus)のいずれかをJSONで返す必要があります。

hookStatus 動作 使用例
SUCCEEDED 次のステージへ進む イメージ検証成功、手動承認完了
FAILED ロールバック開始 検証失敗、テスト失敗
IN_PROGRESS 現ステージに留まり、再度Lambdaを呼び出す 手動承認待ち、長時間テスト待ち

IN_PROGRESSを返すとき、callBackDelay(秒単位)を一緒に返すことで、次のLambda呼び出しまでの待機時間をカスタマイズできます(デフォルトは30秒)。

2. 事前準備

検証に必要なリソースを準備します。「Version 1.0.0が動いている本番環境を、裏でVersion 2.0.0を確認してから安全に切り替える」というストーリーで検証します。

2-1. 用意するリソース一覧

カテゴリ リソース 用途
アプリ関連 ECRリポジトリ 検証用アプリのイメージ格納
アプリ関連 ECSタスク定義 検証用アプリのタスク起動設定
アプリ関連 ECSクラスター タスクの実行基盤
ネットワーク関連 ALB トラフィック分散
ネットワーク関連 本番リスナー 本番トラフィック用
ネットワーク関連 テストリスナー テストトラフィック用
ネットワーク関連 ターゲットグループ × 2 Blue用とGreen用(同じ設定で2つ作成)
フック関連 admissionFunction(Lambda) デプロイ前検証
フック関連 approvalFunction(Lambda) 手動承認
フック関連 S3バケット 承認ファイル配置用
IAM Lambda実行ロール × 2 各Lambdaが使用
IAM ECS用Lambdaフックロール ECSがLambdaを呼び出す
IAM ECS用ALBロール ECSがリスナー設定を変更する

注意: ALBの事前作成が必要な理由
ECSサービス作成画面でALBを新規作成することも可能ですが、その場合は以下の制約があります。

  • ALBはECSタスクと同じセキュリティグループがアタッチされる
  • ALBはECSタスクと同じサブネットに配置される
  • ALBはスキームがインターネット向け(Internet-facing)として作成される

つまり、ALBとECSタスクがパブリックサブネット配置前提になります。
以下の場合は、ALB、リスナー、ターゲットグループを事前に作成してください。

  • プライベートサブネットにALBを配置してスキームを内部(Internal)にしたい
  • プライベートサブネットにECSタスクを配置したい
  • ALBとECSタスクで異なるセキュリティグループを使いたい

2-2. ALB・ターゲットグループ・リスナーの作成

ブルーグリーンデプロイでは、2つのターゲットグループと2つのリスナー(本番用・テスト用)が必要です。

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

同じ設定で2つのターゲットグループを作成します。
今回は、primary-tg、secondary-tgという2つのターゲットグループを作成しました。

image.png

作成時点ではターゲットの登録は不要です。ECSサービスが自動的にタスクを登録します。

ALBの作成

今回はスキームをInternalで作成してみました。

リスナーの作成

ALBにリスナーを作成します。
ALBにHTTP:80のリスナーを1つ作成します。
本番トラフィックとテストトラフィックは、パスベースのルーティングで分離します。

優先度 条件 アクション
1 パス = / primary-tg: 100%, secondary-tg: 0%
2 パス = /test primary-tg: 100%, secondary-tg: 0%
最後(デフォルト) 他のルールが適用されない場合 primary-tg: 100%

両方のリスナーをprimary-tgに向けておきました。
ECSサービスがデプロイ時に自動的にターゲットグループを切り替えます。

2-3. 検証用アプリケーション

「Hello World」のバージョンを表示するだけのシンプルなNginxサーバを用意します。
まずは「Version 1.0.0」と表示される状態でDockerイメージを作成し、ECRにプッシュします。

Dockerfile

# ベースイメージにAWSのパブリックレジストリのnginxを使用する。
FROM public.ecr.aws/nginx/nginx:latest

# index.htmlをローカルディレクトリからNginxのHTMLフォルダにコピーする。
COPY ./index.html /usr/share/nginx/html/index.html

# /testパス用の設定
RUN mkdir -p /usr/share/nginx/html/test
COPY ./index.html /usr/share/nginx/html/test/index.html

index.html (Version 1)

<h1>Hello World! version 1.0.0</h1>

2-3. タスク定義

ECRにプッシュしたイメージを使用するタスク定義を作成します。
今回、コンテナ名は「test-container」としました。
デプロイ検証時はimageの値を変更して新しいリビジョンを作成します。

{
    "family": "test-taskdefinition",
    "networkMode": "awsvpc",
    "requiresCompatibilities": ["FARGATE"],
    "cpu": "256",
    "memory": "512",
    "executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    },
    "containerDefinitions": [
        {
            "name": "test-container",
            "image": "<ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/testrepo:version1.0.0",
            "essential": true,
            "portMappings": [
                {
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/test-taskdef",
                    "awslogs-create-group": "true",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs"
                }
            }
        }
    ]
}

デプロイ検証では、このimageの値を変更します。

シナリオ imageの値
Version 1.0.0(初期) <ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/testrepo:v1.0.0
Version 2.0.0(正常系) <ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/testrepo:v2.0.0
ECR以外(異常系) nginx:latest

2-4. ライフサイクルフック用Lambda関数

AWS公式サンプルから2つのLambda関数を用意します。

admissionFunction(デプロイ前検証)

  • リポジトリ: ecs-bluegreen-lifecycle-hooks/src/admissionFunction
  • フックポイント: スケールアップ前
  • 処理内容: タスク定義をスキャンし、ECR以外のコンテナイメージが使われていたらデプロイをFAILEDにする
  • admissionFunctionLambda関数(抜粋):
# コンテナイメージのリストを検証するメインロジック
def validate_container_images(container_image_list):
    # ECR Private/Public のリポジトリURLに一致する正規表現
    private_ecr_pattern = r"^(d+).dkr.ecr.([a-z0-9-]+).amazonaws.com/.*"
    public_ecr_pattern = r"^public.ecr.aws/aws-containers/.*"

    valid_images = True
    # タスク定義内の全コンテナをループ
    for container_image in container_image_list:
        image_url = container_image["image"]
        logger.info(f"Validating image: {image_url}")

        # ECRのパターンに一致するかチェック
        if re.match(private_ecr_pattern, image_url) or re.match(
            public_ecr_pattern, image_url
        ):
            logger.info(f"Image {image_url} is from an Amazon ECR repository")
        else:
            # ECR以外の場合、警告ログを出し、検証結果を 'False' にする
            logger.warning(f"Image {image_url} is NOT from an Amazon ECR repository")
            valid_images = False

    return valid_images

approvalFunction(手動承認)

  • リポジトリ: ecs-bluegreen-lifecycle-hooks/src/approvalFunction
  • フックポイント: テストトラフィック移行後
  • 処理内容: 指定したS3バケットに承認ファイル({リビジョンID}.txt)が置かれるまでIN_PROGRESSを返し続ける
  • 補足: 初回デプロイでは、approvalFunctionは自動的にSUCCEEDEDを返してスキップされます(ECSサービス初回デプロイの場合、承認ステップは不要という設計)。
  • approvalFunctionLambda関数(抜粋):
# S3に承認ファイルが存在するかチェック
def check_s3_file(s3_bucket, revision_arn):
    # リビジョンARNの末尾(例: 01275892)をファイル名として使う
    revision = revision_arn.split("/")[-1]
    file_name = f"{revision}.txt"
    logger.info(f"Checking if file {file_name} exists in bucket {s3_bucket}")
    s3_client = boto3.client("s3")

    try:
        # head_objectでファイルの存在確認
        s3_client.head_object(Bucket=s3_bucket, Key=file_name)
        logger.info(f"File {file_name} exists in bucket {s3_bucket}")
        return True # ファイルあり
    except ClientError as e:
        if e.response["Error"]["Code"] == "404":
            # 404エラーは「ファイルなし」と判断
            logger.info(f"File {file_name} does not exist in bucket {s3_bucket}")
            return False # ファイルなし
        else:
            # その他のエラーは異常系としてスロー
            logger.error(f"Error checking if file exists: {str(e)}")
            raise e

注意: このLambdaはhookDetailsからS3_BUCKET_NAMEを受け取る設計になっています。ECSサービス設定時に必ず指定してください。

2-5. IAMポリシー

Lambda自体が使用するIAMロールと、ECS自体が使用し、LambdaやALBを操作するためのIAMロールを用意します。

admissionFunction用IAMロール

許可ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ecs:DescribeServiceRevisions",
            "Resource": [
                "arn:aws:ecs:*:*:service/*/*",
                "arn:aws:ecs:*:*:service-revision/*/*/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "ecs:DescribeTaskDefinition",
            "Resource": "*"
        }
    ]
}

信頼ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
approvalFunction用IAMロール

許可ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ecs:ListServiceDeployments",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::*/*"
        },
        {
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::*"
        }
    ]
}

信頼ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
ECS用Lambdaフックロール(ECSがLambdaを呼び出す)

許可ポリシー

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

信頼ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
ECS用ALBロール(ECSがリスナー設定を変更する)

許可ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetHealth"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:DeregisterTargets"
            ],
            "Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*"
        },
        {
            "Effect": "Allow",
            "Action": "elasticloadbalancing:ModifyListener",
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "elasticloadbalancing:ModifyRule",
            "Resource": [
                "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*/*"
            ]
        }
    ]
}

信頼ポリシー

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

3. ECSサービスの設定

ECSサービスを作成し、ブルーグリーンデプロイとフックを紐付けます。

3-1. サービスの詳細

事前に用意したタスク定義を指定します。

image.png

3-2. デプロイ設定

  • デプロイ戦略: ブルー/グリーン
  • デプロイベイク時間: 15分(デフォルト)

image.png

3-3. デプロイライフサイクルフック

事前に準備したLambdaを設定します。

approvalFunctionについては、監視対象のS3バケット名をECSからLambdaへ渡すために、「フックの詳細」でペイロードを指定します。

image.png

3-4. ネットワーキング

VPC、サブネット、セキュリティグループを指定します。

image.png

3-5. ロードバランシング

  • ロール: 事前準備で作成した「ECS用ALBロール」を指定
  • Application Load Balancer: 既存のロードバランサーの作成
  • 本番リスナールール: 本番トラフィック用のリスナールールを指定
  • テストリスナールール: テストトラフィック用のリスナールールを指定
  • ターゲットグループ: 事前作成のターゲットグループを2つ指定

image.png

image.png

4. デプロイ検証

4-0. 初回デプロイ:Version 1.0.0 環境の構築

ECSサービスを新規作成すると、まずVersion 1.0.0のタスクが1つ起動します。
この時点ではBlue/Greenの切り替え対象となる「旧環境」は存在せず、単一の環境(Green)のみです。
初回デプロイでは、approvalFunctionは自動的にSUCCEEDEDを返してスキップされます。これは、サービスリビジョンが1つしか存在しない(=切り替え元の旧環境がない)ことを検出しているためです。

CloudWatch Logsで確認できます。

{"level":"INFO","location":"check_service_revision:42","message":"Retrieved 1 service revisions for arn:aws:ecs:ap-northeast-1:<ACCOUNT_ID>:service/test-ecs-cluster/test-taskdefinition-service",...}
{"level":"INFO","location":"check_service_revision:47","message":"Retrieved a single service Revision, therefore assuming createService",...}
{"level":"INFO","location":"hook_succeeded:11","message":"Sending hookStatus SUCCEEDED back to ECS",...}

image.png

動作確認:

$ curl <ALB-DNS>/
<h1>Hello World! version 1.0.0</h1>

リスナールールの状態(初回デプロイ後)

初回デプロイ完了後、リスナールール(HTTP:80)の重みは以下のように切り替わりました。
本番ルール(/)とテストルール(/test)の両方が、現在稼働中のターゲットグループ(secondary-tg)に100%転送されています。

image.png

ターゲットグループの状態(初回デプロイ後)

secondary-tgを確認すると、タスクが作成されていました。
image.png

image.png

この状態が「Phase 0. 初期状態」に相当します。次回以降のデプロイから、ブルーグリーンの流れになります。


以降、Version 1.0.0が稼働している状態から、Version 2.0.0へのデプロイを検証します。以下の2つのシナリオを試します。

シナリオ 内容 期待結果
4-1 正常系:承認OKでデプロイ完了 Greenへ切り替え成功
4-2 異常系:デプロイ前検証でNG(ECR以外のイメージ) 即座にロールバック

4-1. 正常系:承認OKでデプロイ完了

Version 2.0.0のイメージをECRにプッシュし、タスク定義のimageを変更して新しいリビジョンを作成します。

index.html (Version 2)

<h1>Hello World! version 2.0.0</h1>

タスク定義の変更箇所

- "image": "<ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/testrepo:v1.0.0"
+ "image": "<ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/testrepo:v2.0.0"

Step 1. デプロイ開始

ECSコンソールからサービスを更新し、新しいタスク定義リビジョンを指定します。

image.png

デプロイが開始されると、ECSコンソールの「デプロイ」タブでステータスを確認できます。

image.png

Step 2. スケールアップ前 フック実行

admissionFunctionが呼び出され、タスク定義のコンテナイメージを検証します。ECRのイメージを使用しているため、SUCCEEDEDが返されます。

CloudWatch Logsで検証結果を確認できます。

{"level":"INFO","location":"validate_container_images:30","message":"Validating image: <ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/testrepo...",...}
{"level":"INFO","location":"validate_container_images:35","message":"Image <ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/testrepo... is from an Amazon ECR repository",...}
{"level":"INFO","location":"hook_succeeded:11","message":"Sending hookStatus SUCCEEDED back to ECS",...}

Step 3. Greenタスク起動・テストトラフィック移行後

スケールアップ前ステージが成功すると、Greenタスクが起動します。
タスクがhealthyになると、テストトラフィック移行ステージに入り、テストパス(/test)のトラフィックがGreenに移行します。

この時点で、テストパス(/test)経由でVersion 2.0.0の動作確認ができます。

$ curl <ALB-DNS>/test/
<h1>Hello World! version 2.0.0</h1>

本番パス(/)はまだVersion 1.0.0のままです。

$ curl http://<ALB-DNS>/
<h1>Hello World! version 1.0.0</h1>

リスナールールを確認すると、本番ルール(/)が現在稼働中のターゲットグループ(secondary-tg)に100%転送され、テストルール(/test)が、Greenタスク(primary-tg)に100%転送されています。
image.png

Step 4. テストトラフィック移行後 フック実行(承認待ち)

approvalFunctionが呼び出され、S3バケットに承認ファイルがあるかチェックします。ファイルがないため、IN_PROGRESSが返され、デプロイは一時停止します。
このライフサイクルステージで、テストトラフィックを利用してGreenタスクに対してテスト確認できます。

ECSコンソールでは「テストトラフィック移行後」ステージでデプロイのステージは「進行中」と表示されています。
Greenのタスクのサービスリビジョンは3806116924038813402であることが確認できます

image.png

CloudWatch Logsでは、3806116924038813402.txtファイルが存在しないことを確認後、IN_PROGRESSを返している様子が確認できます。

{"level":"INFO","location":"check_s3_file:65","message":"Checking if file 3806116924038813402.txt exists in bucket my-approval-bucket",...}
{"level":"INFO","location":"check_s3_file:78","message":"File 3806116924038813402.txt does not exist in bucket my-approval-bucket",...}
{"level":"INFO","location":"hook_in_progress:21","message":"Sending hookStatus IN_PROGRESS back to ECS",...}

Step 5. 承認ファイルをS3にアップロード

テスト確認が完了したら、承認ファイルをS3にアップロードします。ファイル名はサービスリビジョンIDに基づきます(今回は3806116924038813402.txtを格納しました)。

Step 6. 本番トラフィック移行・デプロイ完了

次回のLambda実行時に承認ファイルが検出され、SUCCEEDEDが返されます。

{"level":"INFO","location":"check_s3_file:65","message":"Checking if file 3806116924038813402.txt exists in bucket my-approval-bucket",...}
{"level":"INFO","location":"check_s3_file:73","message":"File 3806116924038813402.txt exists in bucket my-approval-bucket",...}
{"level":"INFO","location":"hook_succeeded:11","message":"Sending hookStatus SUCCEEDED back to ECS",...}

本番パスのトラフィックがGreenに移行し、ベイク時間経過後にBlueが停止します。

image.png

動作確認:

$ curl http://<ALB-DNS>/
<h1>Hello World! version 2.0.0</h1>

リスナールールの状態(デプロイ完了後)

デプロイ完了後、リスナールール(HTTP:80)の重みが切り替わります。本番ルール(/)とテストルール(/test)の両方が、primary-tgに100%転送されるようになります。

image.png

ターゲットグループの状態(デプロイ完了後)

primary-tgを確認すると、タスクが作成されていました。

image.png

4-2. 異常系:デプロイ前検証でNG(ECR以外のイメージ)

ECR以外のコンテナイメージを使用した場合の動作を確認します。

検証手順

タスク定義のimageをDocker Hubのイメージに変更して、サービスを更新します。

- "image": "<ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/testrepo:v1.0.0"
+ "image": "nginx:latest"

image.png

結果

デプロイを開始すると、スケールアップ前ステージでadmissionFunctionが呼び出されます。イメージURLがECRのパターンに一致しないため、即座にFAILEDが返されます。

CloudWatch Logsで拒否された理由を確認できます。

{"level":"INFO","location":"validate_container_images:30","message":"Validating image: nginx:latest",...}
{"level":"WARNING","location":"validate_container_images:37","message":"Image nginx:latest is NOT from an Amazon ECR repository",...}
{"level":"INFO","location":"hook_failed:16","message":"Sending hookStatus FAILED back to ECS",...}

ECSコンソールでは、デプロイがロールバックされた旨が表示されます。Greenタスクは起動されず、Blueがそのまま稼働を継続します。

image.png

検証結果まとめ

シナリオ フック hookStatus 結果
正常系 admissionFunction SUCCEEDED 検証通過
正常系 approvalFunction IN_PROGRESS → SUCCEEDED 承認後に本番移行
ECR以外のイメージ admissionFunction FAILED 即座にロールバック

5. 知っておきたい考慮点

今回試した中での注意点や、さらに進んだ使い方についてです。

LambdaのIAM権限の分離

ECSがLambdaを呼び出すロールと、Lambda自身が実行に必要なロールは別物です。
権限の分離を意識して設定してください。

リソース消費

デプロイ中はBlue/Green両方のタスクが起動するため、一時的にリソース(タスク数、ENIなど)が最大2倍必要になります。クラスターのキャパシティに余裕を持たせてください。

hookDetailsでの状態保持

IN_PROGRESSを返すとき、hookDetails辞書にデータを含めると、次のポーリング時にそのデータを受け取れます。
DynamoDB等の外部ストレージなしで、Lambdaの実行間で簡単な状態を管理できます。
以下のようなものを用意し、テスト結果をカウントしながら、最大3回まで再試行するということもできます。

{
    "hookStatus": "IN_PROGRESS",
    "callBackDelay": 30,
    "hookDetails": {"test_count": str(test_count)}
}

制約: この状態保持は、同一フック・同一デプロイメント中のみ有効です。

公式ドキュメントには以下のようにあります。

Furthermore, you can also use the hookDetails dictionary to pass state between invocations of the same lifecycle hook. If a hook returns the IN_PROGRESS status, you can also include the hookDetails dictionary with multiple key / value pairs, removing the need to store state elsewhere. Common use cases include passing metric counters or Amazon Resource Names (ARNs) of external resources between invocations. Note data added to hookDetails in this way only persists between invocations of the same hook within a single deployment. Data doesn’t carry over between different hooks within the same deployment or the same hook in different deployments.

{
   "hookStatus": " IN_PROGRESS",
   "callBackDelay": 90,
   "hookDetails": {
       "SFN_EXECUTION_ARN": "arn:aws:states:<region>:<account_id>:execution:<sfn>:<id>"
   }
}

ロールバックのトリガー

ロールバックは、ライフサイクルフックの失敗(FAILED)以外にも、複数の条件で自動的に発生します。

トリガー 説明
ライフサイクルフック フックがFAILEDを返す
AmazonECSデプロイサーキットブレーカー タスク起動失敗またはヘルスチェック失敗が閾値超過
CloudWatch アラーム デプロイ連動アラームがALARM状態に遷移
手動 aws ecs stop-deploymentコマンドで明示的に指示

image.png

ロールバック時のフック再実行

ロールバックが発生すると、トラフィックをGreenからBlueに戻すために、本番トラフィック移行テストトラフィック移行のフックが再実行されます。

フック内で外部リソース(Slack通知、ログ出力等)を操作している場合、同じ処理が複数回実行される可能性があります。フックのロジックは可能であれば冪等に設計してください。

6. まとめ

ECSの組み込みBlue/Greenデプロイは、ECSサービスの設定のみで完結するようになり、非常にシンプルになりました。

今回はAWS公式サンプルをそのまま使用しましたが、Lambda関数を拡張することで以下のようなユースケースにも対応できそうです。

  • Slackやメールでの承認通知
  • 自動E2Eテストの実行と結果に基づく判定

CodeDeploy不要でここまでできるようになったのは嬉しいアップデートです。

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?