1
2

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で「切替後に旧環境を残して、確認できたら落とす」をやる — PAUSEフックの置き場所について

1
Last updated at Posted at 2026-08-09

はじめに

こんにちは!KDDIアイレット株式会社の水野 永遠です。

この記事は「2026 Japan AWS Jr. Champions 真夏のQiitaリレー」の9日目の記事となります。
過去の投稿(リンク集)はこちらからご覧ください。


2025年7月17日、Amazon ECSがBlue/Greenデプロイ(以下B/G)を内蔵しました。それまではAWS CodeDeployと組み合わせるしかなかったものが、ECS単体で完結するようになったものです。

それから1年。canary、linear、承認待ちと機能が追加され、CodeDeployと比べて足りないものはかなり減りました。

ただ、CodeDeployから移そうとすると引っかかるところが1つあります。CodeDeployのコンソールにあった「元のタスクセットを終了」というボタンです。

このボタンで何をしていたのか、要件に分解するとこうなります。

  1. 本番トラフィックを新環境(Green)に切り替える
  2. 旧環境(Blue)を保持したまま様子を見る
  3. 確認が取れたら、保持をやめて終わらせる

ボタンそのものを再現したいわけではありません。この3つができればいい。

ネイティブB/Gでこれをどうやるか。最初は「できない」と結論しかけました。 4通り試して全部失敗したからです。

結果から言うと、できます。ただし探す場所が違っていました。

検証条件
検証日:2026年8月9日 / リージョン:ap-northeast-1
AWS CLI 2.36.19 / Terraform 1.13.3 + AWS Provider 6.58.0
Fargate(0.25vCPU / 0.5GB)+ ALB + ターゲットグループ2つ

この記事の結論

POST_PRODUCTION_TRAFFIC_SHIFT にPAUSEフックを置けば、3つとも満たせます。

本番トラフィックをGreenへ切替          ← 要件1
  ↓
POST_PRODUCTION_TRAFFIC_SHIFT
  ここでPAUSEフックが止める             ← 要件2
  本番 = Green / Blue = 起動したままhealthy
  ↓ 人が判断
  ├─ ContinueServiceDeployment CONTINUE  → Blue消滅   ← 要件3
  └─ ContinueServiceDeployment ROLLBACK  → Blueへ復帰

実測値です。

操作 所要時間
承認してからBlueが消えるまで 1分25秒(bake time 1分を含む)
ロールバックしてBlueに戻るまで 1分38秒

保持できる時間は今回60分で設定して、そのとおり動くことを確認しました。ドキュメント上は最大14日(20,160分)まで指定できます。CodeDeployの terminationWaitTimeInMinutes は上限2日(2,880分)なので、指定できる値としてはネイティブのほうが長いことになります。

ただし今回検証したのは60分だけです。長時間放置したときにBlueが切り戻せる状態を保つかは確認していません。

一方で、bake timeそのものを途中で打ち切ることはできません。 ここを何とかしようとして4回失敗しました。その記録も残します。

目次

  1. 前提 — この記事が扱う範囲
  2. CodeDeploy構成の何が面倒だったのか — 移行前の状態
  3. 1年で何が変わったか — アップデートの整理
  4. 検証:まず bake time を打ち切ろうとした — 4通りとも失敗
  5. 検証:bake timeでなくてもよかった — PAUSEフックの置き場所
  6. 要件は満たせたか、運用はどう変わるか
  7. 確認できていないこと

答えだけ知りたいなら5章、設計の勘所だけなら6章から読んでください。


1. 前提:この記事が扱う範囲

対象は「ALBの下にいるサービス」だけ

この記事で扱うのは、ALBの後ろで動くECSサービスです。

理由は、B/Gの仕組みそのものにあります。B/Gは「ターゲットグループを2つ用意して、ALBがどちらに振り分けるかを切り替える」やり方です。ALBがいて、そこにHTTPリクエストが来ていることが前提になります。

RunTask で単発起動するバッチや、SQSをポーリングするワーカーのようにLBの下にいないサービスは対象外です。振り分ける経路がないので、ECSは新しいタスクを起動して古いタスクを止めるだけになります(ヘッドレスサービスと呼ばれます)。

「旧環境を保持する」とはどういう状態か

要件2の「旧環境を保持したまま様子を見る」を、もう少し具体的にします。

B/Gではターゲットグループを2つ使います。ネイティブB/Gではリスナールールの重みで振り分けるので、切替は重みを 100:0 から 0:100 に変えるだけです。

このとき、重みが0になった側のタスクは止まりません。 ALBには登録されたまま、healthy な状態で残ります。だから重みを戻すだけで切り戻せる。これが「保持している」状態です。

戻す単位も特徴があります。ネイティブB/Gではサービスリビジョンという記録に戻ります。ECSサービスを作成・更新したときに自動で作られるもので、公式ドキュメントによるとタスク定義だけでなく、ネットワーク構成、ターゲットグループARN、Service Connect設定なども含まれます。読み取り専用で、後から書き換えられません。

そのため、ロードバランサーの向き先を変えたデプロイでも設定ごと戻せます。 CodeDeployのロールバックはタスク定義までなので、ここは差があります。

ただしこれは、旧環境が残っている間だけの話です。

保持している間、自動でも戻せる

人が判断する前に、ECS側で異常を検知して自動で戻す仕組みもあります。経路は2つです。

サーキットブレーカー CloudWatchアラーム
見ているもの タスクが起動して安定するか 数値が悪化していないか
見つけられる障害 起動失敗、クラッシュ、ヘルスチェック不通 5XXの増加、レスポンス遅延、業務上の異常値
見つけられない障害 起動はするが動きがおかしい アラームを作っていない種類の障害
異常に気づくまでの時間 短い アラームの評価期間の分だけかかる

カバーする範囲が違うので、両方入れておくのが無難です。

ここで大事なのは、自動ロールバックが効くのも旧環境が残っている間だけということです。アラームが鳴るまでの時間より保持時間が短いと、鳴る前に旧環境が消えます。6章で bake time の値を決めるときに効いてきます。

なお、サーキットブレーカーがネイティブB/Gで使えるかどうかは、ドキュメントを読んだだけでは判断できませんでした。書かれている粒度が場所によって違うためです。

出典 書かれていること
ECS APIリファレンス deploymentCircuitBreaker ローリングアップデート(ECS)デプロイタイプで使用できる
デプロイ戦略移行のトラブルシューティング ECSデプロイコントローラでサポート
CodeDeployからの移行ブログ ネイティブB/Gの利点としてサーキットブレーカーとの整合を挙げている

「デプロイタイプ」で読むとローリング限定、「コントローラ」で読むと ECSコントローラ + BLUE_GREEN も含まれます。B/Gサービスでも設定自体は保持されますが、実際に発動するかは試していません(末尾参照)。

引き換えに払うもの

  • タスクの数が一時的に2倍になる
  • デプロイが終わるまでの時間がローリングより長い
  • ターゲットグループ2つとリスナールールを先に用意しておく必要がある
  • 保持されるのはタスクだけで、DBやキャッシュなど共有しているものは別に考える必要がある

2. CodeDeploy構成の何が面倒だったのか

ネイティブB/Gが出る前、ECSでB/Gをやるには CodeDeploy と組み合わせるしかありませんでした。

よく挙げられる面倒さ

設定ファイルが3つ必要でした。

ファイル 中身
appspec.yaml どのタスク定義をどのコンテナ・ポートで動かすか、フックでどのLambdaを呼ぶか
taskdef.json タスク定義。イメージURIの部分だけ <IMAGE1_NAME> のような仮の文字列にしておき、デプロイ時に実際の値へ置き換えてもらう
imageDetail.json ビルドしたイメージのURI。ビルド側が出力して、デプロイ側が受け取る

アプリの中身とは関係ないのに、パイプラインの中でこの3つを組み立てる処理を書くことになります。

テスト用に別ポートを開ける必要がありました。

CodeDeployでは、本番用と検証用でALBのリスナーそのものを分けます。切り替えは「どのリスナーがどのターゲットグループを向くか」を入れ替えることで行います。

ネイティブB/Gはリスナールール単位で振り分けられるので、同じポートのままヘッダーやパスで検証トラフィックを分けられます。ただし今回の検証環境は、確認のしやすさを優先して :80 と :8080 の2本構成にしました。この利点は今回の検証では踏んでいません。

Service Connectとは組み合わせられませんでした。 サービス間通信をService Connectで組んでいる場合、B/Gを諦めてローリングにするしかありません。

デプロイ方式を後から変えられませんでした。 ローリングで作ったサービスをB/Gにしたければ、サービスごと作り直しです。

ネイティブB/Gではこれが変えられるようになりました。ただし方向が決まっています。後述します。

実務で重かったのはIAMのほう

上に挙げたものは、一度書けば終わるものが多いです。運用していて重かったのは権限設計でした。

パイプラインを共通管理アカウントに置き、ECSは別アカウントで動かす構成です。

共通管理アカウント A          実行アカウント B
├ CodePipeline               ├ ECSサービス
└ ECR                        ├ CodeDeploy
                             └ クロスアカウントロール(Aを信頼)
                                  ↑ AssumeRole

このときB側で必要になるものを並べます。

種類 中身 何のため
API権限 codedeploy:CreateDeployment / GetDeployment など パイプラインからデプロイを開始・状態確認する
iam:PassRole CodeDeployサービスロールを渡す権限 デプロイ実行時にCodeDeployへ渡すため
iam:PassRole タスク実行ロールを渡す権限 タスク起動時に渡すため
ロール本体 CodeDeployサービスロール CodeDeployがタスクセットを作り、ALBのリスナーを付け替えるための権限一式

CodeDeployサービスロールという管理対象が丸ごと1つ増えます。

さらに厄介なのが、デプロイ方式を変えると権限設計まで変わるところです。

  • ローリング → パイプラインに必要なのはecs:UpdateServiceだけ
  • B/G → codedeploy:CreateDeployment + CodeDeployサービスロール一式

同じアカウント内ならポリシーを直すだけですが、クロスアカウントだと「引き受け先のロールのポリシーを直す」「信頼ポリシーを確認する」「両方のアカウントで動作確認する」という手順を踏むことになります。

ネイティブB/Gだとどうなるか

CodeDeploy構成 ネイティブB/G
デプロイを実行するのは CodeDeploy ECS
ALBを操作するのは CodeDeploy ECS
必要なロール CodeDeployサービスロール ECSインフラストラクチャロール
パイプラインから叩くAPI codedeploy:CreateDeployment ecs:UpdateService
ローリングとB/Gで権限は 変わる 同じ

一番効くのは最後の行です。デプロイ戦略はdeploymentConfigurationの中身が変わるだけなので、クロスアカウントで通してある権限に手を入れる必要がありません。


3. 1年で何が変わったか

時期 アップデート 何が解決したか
2024/11 サービスリビジョン / サービスデプロイメント デプロイの可視化・履歴管理の土台
2025/05/05 1クリックロールバック(StopServiceDeployment 検知に引っかからない障害でも手動で戻せる
2025/07/17 ネイティブB/G + ライフサイクルフック CodeDeployが不要に
2025/10/30 linear / canary 戦略 段階的な切り替えができる
2026/02/04 NLBでのlinear / canary対応 NLB構成でも段階的切り替えが可能に
2026/05/19 PAUSEフック + ContinueServiceDeployment Lambdaなしで承認待ちができる
2026/07 サーキットブレーカーのしきい値カスタマイズ 失敗判定を調整できる(B/Gで効くかは未確認)
2026/07 コンソールのデプロイ可視化強化 進行状況をリアルタイムで追える

2025年7月:ネイティブB/G登場

Amazon ECS enables built-in blue/green deployments

CodeDeployなしでB/Gができるようになりました。ALB、NLB、Service Connectのいずれでも使えます。

同時に入ったのが、ライフサイクルフック(Lambda)、bake time、リスナールールの重み付け、デプロイコントローラの後からの変更です。

最後の「後から変更できる」には方向がありますUpdate Amazon ECS service parameters が挙げている有効な遷移はこの4つです。

CODE_DEPLOY → ECS
CODE_DEPLOY → EXTERNAL
ECS         → EXTERNAL
EXTERNAL    → ECS

ECSCODE_DEPLOY がありません。 つまりCodeDeployからネイティブへは移せますが、逆は同じページに載っていません。

同ページはさらに、ECSコントローラのサービスが Service Connect や VPC Lattice を使っている場合、また deploymentConfiguration にアラーム・サーキットブレーカー・BLUE_GREEN 戦略のいずれかが設定されている場合は、ECSから他のコントローラへ変更できないとしています。

公式は「それ以外の手段が無い」とまでは書いていませんが、この記事が勧める構成(ECSコントローラ + BLUE_GREEN)は上の条件に該当するので、実質サービスの作り直しになると考えたほうがいいです。「試しに移してダメなら戻す」ができない、というのは移行判断で効いてきます。

2025年10月:linear と canary

Amazon ECS now supports built-in Linear and Canary deployments

登場時点では一括切替しかありませんでした。ここで2つの戦略が追加されます。

なお「CodeDeployと機能パリティを達成した」という表現は、このときのWhat's NewではなくCDK向けブログに出てきます。

In October 2025, Amazon ECS further enhanced this capability by adding support for canary and linear deployment strategies, achieving feature parity with CodeDeploy.

CodeDeployのプリセットとの対応はこうなります。

CodeDeployプリセット ネイティブでの書き方
ECSCanary10Percent5Minutes canaryConfiguration: { canaryPercent: 10, canaryBakeTimeInMinutes: 5 }
ECSLinear10PercentEvery1Minutes linearConfiguration: { stepPercent: 10, stepBakeTimeInMinutes: 1 }
AllAtOnce strategy: BLUE_GREENbakeTimeInMinutes

2026年5月:PAUSEフック

Amazon ECS introduces pause and continue controls for service deployments

この記事の答えになる機能です。

それまで、デプロイの途中に人の承認を挟みたい場合はLambdaを書く必要がありました。デプロイは一度始まると自動で進んでしまうので、止めるには外部の仕組みが要る。

PAUSEフックはこれを直接解決します。

  1. 設定したライフサイクルステージに到達するとデプロイが止まる
  2. ECSがhookIdを発行する
  3. EventBridgeにECS Hook State Change / HOOK_AWAITING_ACTIONというイベントが飛ぶ
  4. ContinueServiceDeploymentCONTINUEROLLBACKで呼ぶまで止まったまま

設定できる値です。

項目
置けるステージ RECONCILE_SERVICE / PRE_SCALE_UP / POST_SCALE_UP / POST_TEST_TRAFFIC_SHIFT / PRE_PRODUCTION_TRAFFIC_SHIFT / POST_PRODUCTION_TRAFFIC_SHIFT の6つ
timeoutInMinutes 1〜20,160(14日)。既定は1,440(24時間)
action ROLLBACK(既定)/ CONTINUE
個数の上限 1サービスあたり PAUSEフック10個 かつ Lambdaフック10個

POST_PRODUCTION_TRAFFIC_SHIFT に置けることが重要です。 後述します。

ステージには2種類あります。

  • 単一呼び出しRECONCILE_SERVICE / PRE_SCALE_UP / POST_SCALE_UP / TEST_TRAFFIC_SHIFT / POST_TEST_TRAFFIC_SHIFT / POST_PRODUCTION_TRAFFIC_SHIFT
  • 繰り返し呼び出しPRE_PRODUCTION_TRAFFIC_SHIFT / PRODUCTION_TRAFFIC_SHIFT(linear・canaryではトラフィックシフトのステップごとに呼ばれる)

PRE_PRODUCTION_TRAFFIC_SHIFT にPAUSEを置くと、linearで10%ずつ10回に分けるなら10回止まります。一方 POST_PRODUCTION_TRAFFIC_SHIFT は単一呼び出しなので、1デプロイにつき1回だけです。

TEST_TRAFFIC_SHIFTPRODUCTION_TRAFFIC_SHIFT には置けません。公式はその理由をこう説明しています。

Pause hooks cannot be configured at TEST_TRAFFIC_SHIFT or PRODUCTION_TRAFFIC_SHIFT because these stages are also invoked during rollback.

ロールバックのときにも通るステージなので、そこで止めるとロールバックを完了させるために追加の ContinueServiceDeployment が必要になってしまう、ということです。この説明は今回の検証で実際に裏付けられました(5章)。

なお、同じステージにLambdaフックとPAUSEフックを両方置けます。その場合は全部のフックが完了するまで進まず、並列で実行されます。

2026年7月:サーキットブレーカーの調整と可視化

configurable deployment circuit breaker settings / real-time deployment observability

しきい値を固定回数か希望タスク数に対する割合で指定でき、数え方もconsecutive(正常なタスクが起動したらリセット)とcumulative(積み上げ)から選べるようになりました。


4. 検証:まず bake time を打ち切ろうとした

ここからが検証です。まず失敗した記録から書きます。

何をしようとしたか

要件2「旧環境を保持したまま様子を見る」に対応するのは、ネイティブB/Gでは bakeTimeInMinutes です。

CodeDeployの terminationWaitTimeInMinutes と同じ位置づけに見えるので、素直に考えればこうなります。長めに設定しておいて、要件3「確認が取れたら終わらせる」のタイミングで途中で打ち切る。

そう思って4通り試しました。

検証環境

Internet
   │
   ▼  (自分のIPのみ許可)
  ALB ─── リスナー :80   ── ルール ── tg-blue / tg-green
   └───── リスナー :8080 ── ルール ── tg-blue / tg-green
   │
   ▼
 Fargate 0.25vCPU/0.5GB × 1(nginx)
  • NAT Gatewayなし。タスクをパブリックサブネットに置いてパブリックIPを付ける
  • イメージはpublic.ecr.aws/nginx/nginx:stableをそのまま使用。ECRもビルドも不要

素のnginxだと全部同じページを返してしまい、どちらのバージョンが応答したのか分かりません。そこで起動時に環境変数の値をindex.htmlに書き出すようにしました。

entryPoint = ["/bin/sh", "-c"]
command = [
  "echo \"$VERSION\" > /usr/share/nginx/html/index.html && exec nginx -g 'daemon off;'"
]

これでcurlするとv1v2が返ってきます。

準備でつまずいた2点

IAMポリシーのARN

B/GにはECSインフラストラクチャロールが必要です。ECSが利用者に代わってALBのリスナールールを操作するためのロールで、AWS管理ポリシーを付けるだけで済みます。ただしARNの形が紛らわしい。

ロール ポリシーARN
タスク実行ロール arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
ECSインフラストラクチャロール arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers

後者にservice-role/は付きません。付けるとこうなります。

NoSuchEntity: Policy arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRolePolicyForLoadBalancers
does not exist or is not attachable.

信頼するサービスも違います。タスク実行ロールはecs-tasks.amazonaws.com、インフラストラクチャロールはecs.amazonaws.comです。

リスナーではなくリスナールールのARNが要る

ECSサービスの設定に渡すのは、リスナーのARNではなくリスナールールのARNです。

ALBのリスナーにはデフォルトアクションがあり、ルールを作らなくても振り分けはできます。しかしデフォルトアクションにはARNがありません。なので明示的にリスナールールを作る必要があります。

さらに、ECSはデプロイのたびにリスナールールの重みを書き換えます。Terraformはコードに書いた値が正しいと思っているので、そのままapplyすると切り替えたばかりの重みが戻ってしまう。

resource "aws_lb_listener_rule" "production" {
  # ...
  lifecycle {
    # ECS がデプロイのたびに重みを書き換えるため、差分として扱わない
    ignore_changes = [action]
  }
}

失敗1:進行中に bake time を短くする

bakeTimeInMinutes の範囲は 0〜1440分です(後で実測しました)。上限いっぱいの1440で走らせておいて、途中で1分に変えれば早く終わるのではないか。

まず BAKE_TIME に到達したことを確認します。

$ aws ecs describe-service-deployments --service-deployment-arns $ARN \
  | jq '.serviceDeployments[0] | {status, lifecycleStage, deploymentConfiguration}'
{
  "status": "IN_PROGRESS",
  "lifecycleStage": "BAKE_TIME",
  "deploymentConfiguration": {
    "strategy": "BLUE_GREEN",
    "bakeTimeInMinutes": 1440
  }
}

1分に短縮します。

$ aws ecs update-service \
  --cluster $CLUSTER --service $SERVICE \
  --deployment-configuration '{"strategy":"BLUE_GREEN","bakeTimeInMinutes":1}' \
  --no-cli-pager | jq '.service.deploymentConfiguration.bakeTimeInMinutes'
1

エラーになりません。サービスの設定値は1に変わりました。では進行中のデプロイは。

$ aws ecs describe-service-deployments --service-deployment-arns $ARN \
  | jq '.serviceDeployments[0] | {lifecycleStage, bake: .deploymentConfiguration.bakeTimeInMinutes}'
{
  "lifecycleStage": "BAKE_TIME",
  "bake": 1440
}

変わっていません。

デプロイ開始時点の設定値がそのデプロイに焼き付けられていて、後から変えられない。サービスリビジョンが読み取り専用なのと同じ設計です。

マネジメントコンソールから変更しても同じでした。そして同じ画面の中に、2つの異なる値が並びます。

セクション デプロイベイク時間
進行中のデプロイ 1,440分
デプロイ設定 1分

サービスの設定と、走っているデプロイの設定は別物、ということです。エラーが出ないので効いたように見えますが、効いていません。 「1分に変えたから1分で終わるはず」と思って待つと24時間待たされます。

失敗2:ABORT で打ち切る

StopServiceDeploymentstopType は、説明文が「有効な値は ROLLBACK」なのに、直下の Valid Values には ABORT | ROLLBACK と書かれています。書かれている内容が揃っていないので、試してみました。

aws: [ERROR]: An error occurred (UnsupportedFeatureException) when calling
the StopServiceDeployment operation:
ECS StopServiceDeployment API with ABORT stopType is not supported.

説明文のほうが正しく、ABORT は列挙値に載っているだけでした。

失敗3:コンソールから終わらせる

進行中のデプロイ画面のボタンは「ロールバック」と「アクションログを表示」の2つだけ。前に進めるボタンはありません。

失敗4:BAKE_TIME にフックを置く

デプロイの途中に処理を差し込む仕組み(ライフサイクルフック)があります。bake time の最中にフックを置いて、そこから終わらせられないか。

Lifecycle hooks for Amazon ECS service deployments に、フックを置けるステージの一覧があります。そこに BAKE_TIMECLEAN_UP も載っていません。

ステージとしては存在するのに、フックを置ける場所ではない。ここで手が尽きました。

4方向すべて塞がっている

手段 結果 根拠
UpdateServiceでbake time短縮 受理されるが進行中デプロイには不適用 実機
StopServiceDeployment --stop-type ABORT UnsupportedFeatureException 実機
コンソールのボタン ロールバックのみ 実機
BAKE_TIMEにフックを置く 指定できるステージの一覧に無い ドキュメント

ここで「ネイティブB/Gには相当する機能がない」と結論しかけました。

間違いでした。


5. 検証:bake timeでなくてもよかった

見落としていたこと

bake timeを打ち切る方法ばかり探していました。

でも要件を見直すと、bake timeでなくてもいいはずです。要件2は「旧環境を保持したまま様子を見る」であって、「bake timeを使う」ではありません。保持した状態で止まれれば、手段は何でもいい。

そう思って、さっき見たフックの一覧をもう一度見ます。

Lifecycle stage Lambda hooks Pause hooks
RECONCILE_SERVICE Yes Yes
PRE_SCALE_UP Yes Yes
POST_SCALE_UP Yes Yes
TEST_TRAFFIC_SHIFT Yes No
POST_TEST_TRAFFIC_SHIFT Yes Yes
PRE_PRODUCTION_TRAFFIC_SHIFT Yes Yes
PRODUCTION_TRAFFIC_SHIFT Yes No
POST_PRODUCTION_TRAFFIC_SHIFT Yes Yes

BAKE_TIME が無いことばかり見ていて、一番下の行を見ていませんでした。

ステージの順番はこうです。

# ステージ 状態
8 PRODUCTION_TRAFFIC_SHIFT 本番トラフィックをGreenへ移している最中
9 POST_PRODUCTION_TRAFFIC_SHIFT 本番トラフィックの移行が完了
10 BAKE_TIME BlueとGreenの両方が動いている期間
11 CLEAN_UP Blueが0タスクまで縮小

POST_PRODUCTION_TRAFFIC_SHIFT本番切替が終わった直後で、まだ CLEAN_UP の前です。つまりBlueは生きています

そしてここにはPAUSEフックを置けます

  • 本番トラフィックはGreenへ切替済み → 要件1
  • Blueは起動したまま、そこで止まる → 要件2
  • ContinueServiceDeployment を叩けば進む → 要件3

3つとも満たせそうです。 試します。

設定

$ aws ecs update-service \
  --cluster $CLUSTER --service $SERVICE \
  --deployment-configuration '{
    "strategy": "BLUE_GREEN",
    "bakeTimeInMinutes": 1,
    "lifecycleHooks": [{
      "targetType": "PAUSE",
      "lifecycleStages": ["POST_PRODUCTION_TRAFFIC_SHIFT"],
      "timeoutConfiguration": {
        "timeoutInMinutes": 60,
        "action": "ROLLBACK"
      }
    }]
  }' --no-cli-pager

timeoutInMinutes を60にしたのは、既定値が1440なので設定が効いているかを見分けるためです。

1回目:止まった状態を観察して、ロールバックする

デプロイ(v1 → v2)を打って、POST_PRODUCTION_TRAFFIC_SHIFT に到達したところで止まりました。

$ aws ecs describe-service-deployments --service-deployment-arns "$ARN" \
  | jq '.serviceDeployments[0] | {status, lifecycleStage, lifecycleHookDetails, sourceServiceRevisions, targetServiceRevision}'
{
  "status": "IN_PROGRESS",
  "lifecycleStage": "POST_PRODUCTION_TRAFFIC_SHIFT",
  "lifecycleHookDetails": [
    {
      "hookId": "ecs-pause-XXXXXXXX",
      "targetType": "PAUSE",
      "status": "AWAITING_ACTION",
      "expiresAt": "2026-08-09T16:17:27+09:00",      ← 15:17の60分後。設定が効いている
      "timeoutAction": "ROLLBACK"
    }
  ],
  "sourceServiceRevisions": [                         ← 旧(Blue)
    {
      "runningTaskCount": 1,                          ← 生きている
      "requestedProductionTrafficWeight": 0.0
    }
  ],
  "targetServiceRevision": {                          ← 新(Green)
    "runningTaskCount": 1,
    "requestedProductionTrafficWeight": 100.0         ← 本番を100%受けている
  }
}

timeoutInMinutes に既定と違う60を入れておいたおかげで、設定が効いていることも同時に確認できました。

この状態でALBを叩きます。

$ curl -s http://$ALB/
v2
$ aws ecs describe-services --cluster $CLUSTER --services $SERVICE \
  --query 'services[0].[desiredCount,runningCount]' --output text
1       2

本番は新バージョン。タスクは新旧2本が動いている。

ALB側も確認します。

$ aws elbv2 describe-rules --rule-arns "$RULE" \
  --query 'Rules[0].Actions[0].ForwardConfig.TargetGroups'
[
  { "TargetGroupArn": ".../ecsbg-lab-tg-green/...", "Weight": 0   },
  { "TargetGroupArn": ".../ecsbg-lab-tg-blue/...",  "Weight": 100 }
]

$ aws elbv2 describe-target-health --target-group-arn "$TG_BLUE" \
  --query 'TargetHealthDescriptions[].{target:Target.Id,state:TargetHealth.State}'
[ { "target": "10.20.0.141", "state": "healthy" } ]

$ aws elbv2 describe-target-health --target-group-arn "$TG_GREEN" \
  --query 'TargetHealthDescriptions[].{target:Target.Id,state:TargetHealth.State}'
[ { "target": "10.20.0.192", "state": "healthy" } ]

両方のターゲットグループに healthy なタスクが登録されたままです。 重みを入れ替えるだけで切り戻せる状態が保たれています。

ただしこれは停止直後の1回だけの観測です。この後ロールバックするまでの十数分は保たれていましたが、それより長く放置したときにどうなるかは見ていません。

要件1と2が満たせていることが確認できました。

  • 本番トラフィックは新バージョン → 要件1
  • 旧バージョンは起動したまま、ALBにも登録されている → 要件2
  • デプロイは人の操作を待って止まっている

あとは要件3「確認が取れたら終わらせる」ができるかです。

ロールバックしてみる

本番トラフィックが移ったから、本当に戻せるのか。

$ date +%H:%M:%S
15:28:40
$ aws ecs continue-service-deployment \
    --service-deployment-arn "$ARN" --hook-id "$HOOK" --action ROLLBACK --no-cli-pager

ステージの遷移です。

15:29:29  (+  2s)  ROLLBACK_IN_PROGRESS / TEST_TRAFFIC_SHIFT
15:29:50  (+ 23s)  ROLLBACK_IN_PROGRESS / CLEAN_UP
15:30:18  (+ 51s)  ROLLBACK_SUCCESSFUL
$ curl -s http://$ALB/
v1
$ aws ecs describe-services --cluster $CLUSTER --services $SERVICE \
  --query 'services[0].[desiredCount,runningCount]' --output text
1       1

1分38秒で旧バージョンに復帰しました。

ここで面白いのは、ロールバック中に TEST_TRAFFIC_SHIFT を通っていることです。公式ドキュメントが「PAUSEフックを TEST_TRAFFIC_SHIFTPRODUCTION_TRAFFIC_SHIFT に置けない」理由として挙げていた説明が、実機で確認できました。

because these stages are also invoked during rollback

ロールバック時にも通るステージなので、そこにPAUSEを置くとロールバックの途中で止まってしまう、ということです。

2回目:止めてから承認して完了させる

ロールバック後、フックの設定は保持されていました。もう一度デプロイ(v1 → v3)を打ちます。

同じく POST_PRODUCTION_TRAFFIC_SHIFT で止まりました。承認前の状態です。

$ curl -s http://$ALB/
v3
$ aws ecs describe-services --cluster $CLUSTER --services $SERVICE \
  --query 'services[0].[desiredCount,runningCount]' --output text
1       2

本番はv3、タスクは2本。1回目と同じ状態です。

承認します。

$ date +%H:%M:%S
15:36:32
$ aws ecs continue-service-deployment \
    --service-deployment-arn "$ARN" \
    --hook-id "$HOOK" \
    --action CONTINUE \
    --no-cli-pager
{
    "serviceDeploymentArn": "arn:aws:ecs:...:service-deployment/..."
}

結果です。

$ aws ecs describe-service-deployments --service-deployment-arns "$ARN" \
  | jq '.serviceDeployments[0] | {status, startedAt, finishedAt}'
{
  "status": "SUCCESSFUL",
  "startedAt":  "2026-08-09T15:31:56+09:00",
  "finishedAt": "2026-08-09T15:37:57+09:00"
}

$ aws ecs describe-services --cluster $CLUSTER --services $SERVICE \
  --query 'services[0].[desiredCount,runningCount]' --output text
1       1

承認(15:36:32)から完了(15:37:57)まで1分25秒。 タスク数が1に戻り、旧環境が片付きました。

ロールバック側と違ってステージ遷移のログを取っていないので内訳は正確には分かりませんが、bake time の1分を差し引くと残りは約25秒です。

bake time の境界値

ついでに範囲も確定させました。

$ aws ecs update-service --cluster $CLUSTER --service $SERVICE \
  --deployment-configuration '{"strategy":"BLUE_GREEN","bakeTimeInMinutes":0}' \
  --no-cli-pager | jq '.service.deploymentConfiguration.bakeTimeInMinutes'
0

$ aws ecs update-service --cluster $CLUSTER --service $SERVICE \
  --deployment-configuration '{"strategy":"BLUE_GREEN","bakeTimeInMinutes":1441}' \
  --no-cli-pager
aws: [ERROR]: An error occurred (InvalidParameterException) when calling the UpdateService operation:
The bake time must be between 0 and 1440 for BLUE_GREEN deployment strategy.

0〜1440分。 エラーメッセージが範囲を明示してくれるので分かりやすいです。

bakeTimeInMinutes の上限が24時間なのに対し、CodeDeployの terminationWaitTimeInMinutes は2,880分(48時間)まで指定できます。bake time単体で比べるとCodeDeployのほうが長いことになります。

なお 0update-service が受理することを確認しただけで、0を設定した状態でデプロイを走らせてはいません。

この2回の update-service を打った後、デプロイの本数を確認しました。

$ aws ecs list-service-deployments --cluster $CLUSTER --service $SERVICE \
  --query 'length(serviceDeployments)'
3

増えていません。deploymentConfiguration だけの変更ではデプロイが走らないということです。

ついでに分かったこと

continue-service-deployment に渡す引数

公式ドキュメントの例はこうです。

aws ecs continue-service-deployment \
    --hook-id ecs-pause-... \
    --action CONTINUE

このとおり打つと --service-deployment-arn が必須だと言われました。手元の CLI 2.36.19 では、--service-deployment-arn / --hook-id / --action の3つを渡す必要がありました。

CLIリファレンスの synopsis は確認できていないので1環境での観測ですが、同じところで詰まる人はいそうです。

BlueとGreenは固定の名前ではない

デプロイ中にサービスリビジョンの一覧を見ると、tg-blueという名前のターゲットグループに新しい方が入っていました。

初回デプロイでtg-greenを使ったからです。次のデプロイでは空いているtg-blueが使われ、その次はまたtg-greenに戻ります。デプロイのたびに交互に入れ替わります。

ECSの用語としてのBlue/Greenは「現在動いている方」「新しく出す方」という役割であって、ターゲットグループの名前とは無関係です。障害調査で「tg-blueだから旧環境のはず」と思い込むと、見る場所を間違えます。

切替後もテストリスナーは生きている

完了後に両方のリスナーを叩くと、どちらも新バージョンを返しました。

$ curl -s http://$ALB/        # 本番リスナー
v3
$ curl -s http://$ALB:8080/   # テストリスナー
v3

テストリスナーは自動では無効化されません。本番と同じバージョンに到達できる経路が2つ残る、ということです。止めたい場合は POST_PRODUCTION_TRAFFIC_SHIFT にLambdaフックを置いて自分で無効化することになります。

CLIが古いと機能が無いように見える

検証の途中でこんなエラーが出ました。

Parameter validation failed:
Unknown parameter in deploymentConfiguration: "bakeTimeInMinutes",
must be one of: deploymentCircuitBreaker, maximumPercent, minimumHealthyPercent, alarms

CLI 2.19.4(2024年11月リリース)を使っていたためです。ネイティブB/Gが出る前のバージョンなのでパラメータを知らず、ローカルで弾いていました。APIには到達していません。

エラーメッセージは「そんなパラメータは無い」としか言わないので、機能自体が存在しないと誤解しやすいところです。新機能を試すときは、まずCLIを最新にしておくのが早いです。

(もう一つ細かい話として、update-service の直後に list-service-deployments --status IN_PROGRESS を叩いてもまだ何も返ってきません。数秒のラグがあるので、スクリプトでARNを取るならリトライが要ります)

TerraformではPAUSEフックが書けない

hashicorp/aws プロバイダで書こうとすると通りません。

$ terraform validate
Error: Missing required argument
  The argument "hook_target_arn" is required, but no definition was found.
Error: Unsupported argument
  An argument named "target_type" is not expected here.
Error: Unsupported block type
  Blocks of type "timeout_configuration" are not expected here.

スキーマを直接見ます。

$ terraform providers schema -json > schema.json
$ jq -r '.. | objects | select(has("lifecycle_hook")) | .lifecycle_hook' schema.json
{
  "nesting_mode": "set",
  "block": {
    "attributes": {
      "hook_details":     { "type": "string", "optional": true },
      "hook_target_arn":  { "type": "string", "required": true },
      "lifecycle_stages": { "type": ["list","string"], "required": true },
      "role_arn":         { "type": "string", "required": true }
    }
  }
}

target_typetimeout_configuration もありません。PAUSEフックは hookTargetArnroleArn を使わない仕様なので、まだ追いついていない状態のようです。

今回使ったのは v6.58.0 です。Terraform側にこの設定を触らせないようにして、CLIから入れました。

lifecycle {
  ignore_changes = [
    task_definition,
    # PAUSEフックはプロバイダ未対応のためCLIで設定する
    deployment_configuration,
  ]
}

なお PAUSEフックのWhat's New には、CloudFormationやCDKと並べてTerraformも挙げられています。CloudFormationでは TargetType / TimeoutConfiguration が使えるので、プロバイダ側の対応を待つ形になりそうです。


6. 要件は満たせたか、運用はどう変わるか

要件は満たせたのか

冒頭に挙げた3つに戻ります。

要件 結果
1. 本番トラフィックを新環境に切り替える 満たせる(実測:切替後に curl で新バージョンを確認)
2. 旧環境を保持したまま様子を見る 満たせる(実測:Blueが healthy のまま両TGに登録された状態で停止)
3. 確認が取れたら保持をやめて終わらせる 満たせる(実測:承認から1分25秒でBlue消滅)

3つとも満たせました。 ついでにロールバック(1分38秒でBlueへ復帰)も確認できています。

基本の構成

{
  "strategy": "BLUE_GREEN",
  "bakeTimeInMinutes": 1,
  "lifecycleHooks": [{
    "targetType": "PAUSE",
    "lifecycleStages": ["POST_PRODUCTION_TRAFFIC_SHIFT"],
    "timeoutConfiguration": {
      "timeoutInMinutes": 60,
      "action": "CONTINUE"
    }
  }]
}

「保持する」役目をbake timeからPAUSEフックに移すのがポイントです。

timeoutInMinutes は「旧環境を最大どれくらい保持したいか」で決めてください。上の60は今回の検証値です(既定の1440と区別するために選んだ値で、推奨値ではありません)。1〜20,160分の範囲で指定できます。

bake timeは打ち切れませんが、PAUSEフックは人が操作するまで止まったままです。要件2の「保持したまま様子を見る」も、要件3の「確認が取れたら終わらせる」も、こちらで担えます。

決めるべきこと:確認しないまま放置したらどうするか

要件3は「確認が取れたら終わらせる」です。裏を返すと、確認しないまま放置したときの結末を決める必要があります。

timeoutConfiguration.action で選べます。

action 放置したときの結末 向いている運用
CONTINUE 時間が来たら完了。本番は新環境のまま 確認は推奨だが、忘れても前に進んでほしい
ROLLBACK(既定) 時間が来たら切り戻る 確認を必須にしたい。誰も見なかったら安全側に倒す

CodeDeployの terminationWaitTimeInMinutes は前者だけでした。ネイティブは選べます。

ROLLBACK を選ぶときは、何が起きるか具体的に考えてください。 たとえばタイムアウトを24時間にしていると、24時間ずっと本番トラフィックを受けていたGreenを、誰も操作しなかったという理由だけで24時間前のBlueへ無人で戻すことになります。その間にDBへの書き込みが積み上がっていれば、旧コードで読める保証はありません。

既定だからという理由で選ばないほうがいいところです。

なお上の構成では CONTINUE を書いていますが、今回検証したのは ROLLBACK のほうです。 タイムアウトして自動的に CONTINUE されるかは試していません。

決めるべきこと:bakeTimeInMinutes をいくつにするか

PAUSEフックで止めている時点で要件2は満たせているので、bake timeは短くて構いません。今回は1分で測りました。

ただし1分に安全上の意味はありません。「承認した後もしばらく自動ロールバックを効かせたい」と思って1分にしたのですが、CloudWatchアラームは最短でも1分周期で、評価期間を掛けるとさらにかかります。アラームが鳴る前にBlueが消えるので、実効はありません。 1分の実効は「CLEAN_UPが1分遅れる」だけです。

決め方は2択です。

  • 承認の時点で人が確認済みなので、その後は自動検知に頼らない → 0〜1分
  • 承認後も一定時間は自動で戻せるようにしたい → アラームが発報するまでの時間より長く(数分〜十数分)

後者を選ぶと「承認したら即片付く」という設計とぶつかります。どちらを取るかは要件次第です。

移行するときに1つ気をつけること

要件は満たせますが、「今回は止めなくていい」を選ぶのが少し面倒です。

lifecycleHooks はサービスの deploymentConfiguration にあります。失敗1で確認したとおり、進行中のデプロイには設定変更が効きません。 緊急のホットフィックスで「今すぐ出したい」ときは、デプロイを打つ前に update-service でフックを外しておくことになります。

対処としては、action: CONTINUE にしてタイムアウトを短くしておく、CI/CD側で「フックを外す → デプロイ → フックを戻す」を組む、承認を自動化する(EventBridgeイベントを拾って ContinueServiceDeployment を叩く)、あたりです。

CodeDeployを選ぶ理由は残るのか

AWS自身の推奨は条件付きです。CDK向けの公式ブログにはこう書かれています。

CodeDeploy remains a valid option for existing customers or those with specific CodePipeline dependencies and established governance processes, but AWS's direction is to migrate toward ECS-native for new deployments.

「切替後に旧環境を保持して、確認できたら落とす、ができないから」は理由になりません。 今回の検証で満たせることが確認できました。

条件別に整理します。AWSが公式に挙げている基準(★)に、今回分かったことを足しています。

状況 選択
新規構築、ALB配下、単一アカウント ネイティブB/G
Service Connectを使っている ネイティブB/G(CodeDeployは非対応)★
複数ターゲットグループを使う ネイティブB/G
切替後に旧環境を保持して人の判断で落としたい ネイティブB/G(PAUSEフックで満たせる)
既存のCodePipeline + CodeDeployが動いている 急いで移す理由がなければCodeDeploy継続
1リリースで複数アカウント・リージョン・サービスをまとめたい CodeDeploy
CodeDeploy中心の統制プロセス・監査証跡が確立している CodeDeploy
PAUSEフックをTerraformでコード管理したい 要検証hashicorp/aws v6.58.0 では書けなかった)

移行を決める前に、1つだけ押さえてください。

移行は片道です。 3章で書いたとおり、ECSCODE_DEPLOY は公式が挙げる有効な遷移に含まれていません。移してから「やっぱり戻したい」となったら、実質サービスの作り直しになります。まず検証環境で手順を組み直してから本番に持っていくのが安全です。

この記事で言いたかったこと

3つあります。

1つ目。「機能」ではなく「要件」で探す。

これが一番の学びでした。

最初は「CodeDeployのボタンに相当する機能はどれか」と探しました。terminationWaitTimeInMinutes に対応するのは bakeTimeInMinutes だから、それを打ち切る方法があるはずだ、と。4通り試して全部失敗しています。

でも冒頭に書いた3つの要件に分解すれば、話は変わります。要件2は「旧環境を保持したまま様子を見る」であって、「bake timeを使う」ではありません。保持した状態で止まれれば、手段は何でもよかった。

POST_PRODUCTION_TRAFFIC_SHIFT にPAUSEフックを置けることは公式ドキュメントに書いてあります。読んでいたのに、「bake timeをどうにかする」という枠から出られませんでした。

移行のときは、移行元の機能をそのまま探すのではなく、その機能で何をしていたのかを要件に落としてから探す。 今回はこれを忘れていました。

2つ目。エラーが出ないからといって、効いているとは限らない。

UpdateService でbake timeを変えても受理されます。コンソールの設定値も変わります。でも進行中のデプロイには適用されません。同じ画面の中に1,440分と1分が並んで表示されるのを見たときは、かなり戸惑いました。

設定が受理されたことと、意図した動作をすることは別です。

3つ目。新しい機能は、1回叩いてから設計に組み込む。

今回いくつか、ドキュメントを読んだだけでは分からないことがありました。

  • stopTypeABORT が使えるかどうか(説明文と Valid Values で書かれている内容が違った)
  • continue-service-deployment に必要な引数(公式の例のとおりでは手元では通らなかった)
  • サーキットブレーカーがB/Gで使えるかどうか(書かれている粒度が場所によって違う)

出たばかりの機能はドキュメントの整備が追いつく前に触ることになるので、こういうことは起きます。手元で1回叩けば分かるので、確かめてから設計に組み込むのが早いです。


7. 確認できていないこと

正確を期すため、今回試していないことをまとめておきます。

項目 状況
長時間の停止 検証したのは60分まで。数時間〜数日放置したときにBlueがhealthyのまま残るかは未確認
サーキットブレーカー ネイティブB/Gで実際に発動するかは未確認
PAUSE中の監視 止まっている間もCloudWatchアラームが監視を続けるかは未確認
bakeTimeInMinutes: 0 update-service が受理することは確認したが、0でデプロイを走らせてはいない
action: CONTINUE でのタイムアウト 検証したのは ROLLBACK。タイムアウトして自動的に完了へ進むかは未確認
CLIの必須引数 continue-service-deployment は手元では --service-deployment-arn が必要だった。CLIリファレンスの synopsis は確認できていない
コントローラの戻し方 ECSCODE_DEPLOY が公式の遷移一覧に無いことは確認。それ以外の手段が本当に無いかは未確認

特に1つ目が本質的です。要件2「旧環境を保持したまま様子を見る」が長時間でも成立するかは、保持できる時間の上限より重要です。ここは追って確かめたいところです。


参考

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?