はじめに
ECS のデプロイメントサーキットブレーカーを有効にしていれば、失敗しても前のバージョンに戻るから安心、と思っていませんか。
私はそう思っていました。ところが、ロールバックが効かない状況がいくつもあります。本記事では検証用のサービスを立て、ロールバックが失敗する状況にどんなパターンがあるか検証してみました。
先に要点を書きます。ロールバックするには、rolloutState が COMPLETED のデプロイが最低1つ残っているのが前提条件です。COMPLETED のデプロイが残っていても、ロールバックが失敗する経路があります。 しかも ROLLBACK_FAILED にすら落ちず、ロールバックが終わらないままリトライを繰り返し続けるケースがあります。
※以降、rolloutState が COMPLETED のデプロイを「ロールバック候補」と呼びます。
なお、CodeDeploy を使う blue/green(CODE_DEPLOY デプロイコントローラ)は扱いません。ECS 組み込みのローリング更新と blue/green だけを対象にしています。
前提と環境
ローリング更新の検証構成の環境は次のとおりです。
| 項目 | 値 |
|---|---|
| リージョン | ap-northeast-1 |
| 起動タイプ | Fargate(0.25 vCPU / 0.5 GB) |
desiredCount |
1 |
| デプロイ戦略 | ローリング更新(ROLLING) |
| サーキットブレーカー |
enable と rollback を有効 |
異常系として、起動すると即座に終了するコンテナを持つタスク定義を用意し、これを投げてサーキットブレーカーを踏ませます。
計測は一定周期でのサンプリングによるため、秒単位の値には幅があります。あくまで目安としてご覧ください。
ロールバック候補とは何か
describe-services のレスポンスにある deployments 配列の状態には、ロールバック可否を示す2種類があります。
| フィールド | 意味 | 値 |
|---|---|---|
status |
サービス内での役割 |
PRIMARY / ACTIVE
|
rolloutState |
ロールアウトの進行状態 |
IN_PROGRESS / COMPLETED / FAILED
|
サーキットブレーカーがデプロイの失敗を検知すると、最新の COMPLETED な deployment を探して、そこへ戻します。戻し始めると、その deployment の rolloutState は COMPLETED から IN_PROGRESS に変わります。戻し終わるまでは、その deployment はロールバック先の候補から外れます。そして COMPLETED な deployment が1つも見つからないと、ECS は新しいタスクを起動せずデプロイが止まります1。
状態遷移図としては下記の通り。
同じ「サーキットブレーカー作動」というきっかけでも、その時点で COMPLETED が残っているかどうかで行き先が変わります。
デプロイタイミングごとに整理すると、次のようになります。
| その時点の状態 |
deployments[] の中身 |
COMPLETED の件数 |
update-service を投げると |
|---|---|---|---|
| 安定 | PRIMARY / COMPLETED |
1 | 問題ない |
| デプロイ進行中 |
PRIMARY / IN_PROGRESS と ACTIVE / COMPLETED
|
1 | 問題ない |
| ロールバック進行中 |
PRIMARY / IN_PROGRESS のみ |
0 | 戻り先を失う |
| 失敗して止まった後 |
PRIMARY / FAILED のみ |
0 | 戻り先が無い |
つまずきポイントとしては、2番目と3番目が deployments[0] だけ見ると判別できないところです。PRIMARY / IN_PROGRESS はどちらも同じで、差は旧 deployment が ACTIVE / COMPLETED として残っているかどうかです。
実際にロールバックできない状況を再現させてみた
実際にロールバックできない状況を再現するために、安定した状態から異常系(デプロイが失敗するイメージ)のタスク定義を投げ、サーキットブレーカーを作動させ、ロールバックが終わる前にもう一度デプロイさせてみます。
まず初めに、タスクが安定状態であるか確認します。
aws ecs describe-services --cluster <cluster> --services <service> \
--query 'services[0].deployments[].{id:id,status:status,rollout:rolloutState,run:runningCount}' \
--output table
| 0 | ecs-svc/4868182749095120048 | 1 | COMPLETED | PRIMARY |
次に異常系の差し替えを実施します。
aws ecs update-service --cluster <cluster> --service <service> \
--task-definition <failing-task-definition>
差し替えた1秒後には、もう候補ができていました。
| ecs-svc/7305215183867168167 | IN_PROGRESS | 0 | PRIMARY | (異常系)
| ecs-svc/4868182749095120048 | COMPLETED | 1 | ACTIVE | (正常系)
新しい deployment が PRIMARY となり、それまで PRIMARY だったものが ACTIVE に変わります。この ACTIVE / COMPLETED がロールバック候補です。
そのまま置いておくと、タスクの起動失敗が繰り返され、サーキットブレーカーが作動します。
今回はデプロイ失敗回数が 3 回に達したときに発動するようにしました。
| ecs-svc/4868182749095120048 | IN_PROGRESS | ECS deployment circuit breaker: rolling back to deploymentId ecs-svc/4868182749095120048. | PRIMARY |
| ecs-svc/7305215183867168167 | FAILED | ECS deployment circuit breaker: tasks failed to start. | ACTIVE |
ロールバック先は ACTIVE から PRIMARY に戻り、rolloutState も COMPLETED から IN_PROGRESS に変わります。これによりロールバック候補から外れます。
一方、異常系タスクはもちろんデプロイ失敗するため、 deployment は ACTIVE / FAILED になり、タスクは DRAINING に移行します。サーキットブレーカー作動から約 50 秒で、この ACTIVE / FAILED は deployment の配列から消えました。
この時点で COMPLETED は 0 件です。この状態はロールバックが完走するまでの約 4 分続きました。
そして、そのECSサービスに対してもう一度デプロイを実施します。
時系列は下記の通りです。
| 経過時間 | 起きたこと | COMPLETED |
|---|---|---|
| 0:00 | 異常系を投入 | 1 |
| 2:41 | サーキットブレーカー作動、ロールバック開始 | 0 |
| 2:46 |
COMPLETED が 0 件になったことを確認 |
0 |
| 2:48 | ここで次のデプロイを投入 | 0 |
| 5:49 | ROLLBACK_FAILED |
0 |
update-service は拒否されず、受け付けられて新しい deployment が立ち上がります。
しかし、deployment が立ち上がってから 3 分 ほどで失敗します。失敗の理由は、下記の通りです。
aws ecs describe-service-deployments \
--service-deployment-arns <service-deployment-arn> \
--query 'serviceDeployments[0].{status:status,reason:statusReason}'
{
"status": "ROLLBACK_FAILED",
"reason": "No rollback candidate was found to run the rollback."
}
コンソールだと下図のように確認できます。
画面の読み方は次のとおりです。
- 「デプロイのステータス」が「ロールバックに失敗」
- タスクは「0 件実行中」。サービスは落ちています
- 右側のサーキットブレーカーが「トリガーされました」「失敗したタスク 3/3 件」
サーキットブレーカー自体は想定通り作動しました。しかし、ロールバック処理中は一時的にロールバック候補が存在しない状態になるため、このタイミングでデプロイを行うと ROLLBACK_FAILED となることが分かりました。
では、ロールバック候補が存在する状態であれば問題なくロールバックできるのでしょうか。
続いて、戻し先となる deployment が存在しているにもかかわらず、ロールバックに失敗するパターンを確認してみます。
戻し先があっても失敗する
ここまでは「戻り先が無い」場合の話をしました。ただ戻り先がある場合、rolloutState が COMPLETED として残っていれば問題がないかというと、そうでもありません。
戻し先のタスクが起動できない場合を試します。ECR に置いたイメージを参照するタスク定義でサービスを安定させ、そのイメージを ECR から削除してから異常系を投げます。ライフサイクルポリシーで古いイメージが消えている状況を想定して再現しました。
稼働中のタスクはイメージを消しても止まりません。問題となるのはロールバックが戻し先のタスクを起動しようとした時です。
aws ecs describe-tasks --cluster <cluster> --tasks <task-arn> \
--query 'tasks[].{stopped:stoppedReason,code:stopCode}'
CannotPullContainerError: pull image manifest has been retried 7 time(s):
failed to resolve ref <account>.dkr.ecr.ap-northeast-1.amazonaws.com/<repo>:v1@sha256:... : not found
サービスデプロイの状態は、ROLLBACK_FAILED にならず、ROLLBACK_IN_PROGRESS のままで、約 2 分周期でタスクの起動と失敗を繰り返してしまいました。
0s has started 1 tasks
+89s unable to place a task. CannotPullContainerError ... not found
+34s has started 1 tasks
+87s unable to place a task. CannotPullContainerError ... not found
+33s has started 1 tasks
イメージを再 push し、正常系のタスク定義で update-service を投げ直したところ、リトライ中でも受け付けられ、2 分 37 秒で COMPLETED に戻りました。
失敗イベント通知受け取ってみた
AWS は、サービスデプロイの失敗を EventBridge の SERVICE_DEPLOYMENT_FAILED で拾って通知するよう推奨しています1。ここまでの失敗でどのイベントが届くのか、EventBridge のルールで実際に受け取ってみました。なお SERVICE_DEPLOYMENT_IN_PROGRESS と SERVICE_DEPLOYMENT_COMPLETED が届くのは、正常なデプロイで確認済みです。
| 失敗 | 届くイベント |
|---|---|
| デプロイがサーキットブレーカーで失敗 |
SERVICE_DEPLOYMENT_FAILED(失敗した deployment ごとに 1 回) |
戻り先が無くロールバックが失敗(ROLLBACK_FAILED) |
上の SERVICE_DEPLOYMENT_FAILED だけ。ロールバックの失敗を示すイベントは届かない |
戻し先のタスクが起動できない(ROLLBACK_IN_PROGRESS のままリトライ) |
SERVICE_TASK_CONFIGURATION_FAILURE(リトライのたび) |
{
"eventName": "SERVICE_DEPLOYMENT_FAILED",
"deploymentId": "ecs-svc/6912815985047155502",
"reason": "ECS deployment circuit breaker: tasks failed to start."
}
{
"eventName": "SERVICE_TASK_CONFIGURATION_FAILURE",
"reason": "CannotPullContainerError: pull image manifest has been retried 7 time(s): ... not found"
}
SERVICE_DEPLOYMENT_FAILED は、deployments 配列内の各 deployment が失敗したタイミングで発行されます。reason に記録されるのも、その deployment が失敗した理由です。
一方で、ロールバック自体が失敗したことや、ロールバック先のタスク起動を繰り返しリトライしていることは、このイベントからは分かりません。
ロールバック中のタスク起動失敗など、実際の失敗理由を確認したい場合は SERVICE_TASK_CONFIGURATION_FAILURE を見る必要があります。
ECS ネイティブの blue/green デプロイでも検証してみた
ここまではローリング更新の話でした。ECS 組み込みの blue/green(BLUE_GREEN / CANARY)でも、ロールバックが失敗する状況を再現してみます。
ただし、再現方法は少し異なります。ここまで使ってきたサーキットブレーカーはローリング更新専用で、blue/green では利用できません2。blue/green のロールバックは CloudWatch アラームまたはライフサイクルフックの失敗をきっかけに開始されるため、今回はライフサイクルフックを失敗させることでロールバックを発生させ、そのロールバック自体も失敗する状態を作ってみました。
検証構成も少し変わります。blue/green では本番用とテスト用の 2 経路が必要になるため、ローリング更新の構成に ALB を追加しました。内部 ALB を 1 つ、ターゲットグループを blue / green の 2 つ、リスナーを本番用(:80)とテスト用(:8080)の 2 つ用意しています。
また、トラフィック切り替え時にリスナールールを書き換えるのは ECS 自身なので、その権限を持つ ECS インフラストラクチャロールも設定しています。
今回は、本番トラフィックが green へ切り替わった後で ECS インフラストラクチャロールから ELB の操作権限を外し、その状態でライフサイクルフックに FAILED を返しました。
すると ECS はロールバックを開始するものの、本番トラフィックを元のターゲットグループへ戻せないため、ロールバック自体が失敗します。
Rollback failed because PRODUCTION_TRAFFIC_SHIFT lifecycle hook(s) failed.
User: ... is not authorized to perform: elasticloadbalancing:DescribeRules ... (Status Code: 403)
結果として、blue/green でもローリング更新と同様に最終状態は ROLLBACK_FAILED になります。
しかし、ここから先の挙動は大きく異なりました。
ROLLBACK_FAILED になった直後 |
ローリング更新 | blue/green(CANARY) |
|---|---|---|
| 戻し先の deployment | 配列から消えている |
IN_PROGRESS のまま残っている |
| 新しいデプロイを投げると | タスクの起動が始まる | 受け付けられるが、タスクが起動しない |
| 本番トラフィック | −(LB なしの構成) | 失敗した側を向いたまま |
ローリング更新では ROLLBACK_FAILED になっても deployment は整理されるため、その後のデプロイで復旧を試せます。
一方、blue/green ではロールバックできなかった deployment が IN_PROGRESS のまま残り続けました。
この deployment は手動で削除できません。また、rolloutState を COMPLETED に戻す方法もありません。
さらに厄介なのは、この状態では新しいデプロイを受け付けるものの、実際にはタスクが起動せず処理が進まないことです。本番トラフィックも失敗した側を向いたままとなり、サービスはエラーを返し続けます。
つまり問題は ROLLBACK_FAILED になったことそのものではなく、失敗したロールバックの deployment が IN_PROGRESS のまま残り続けることです。
この状態から復旧する方法は、正常にデプロイできていた revision を再デプロイし、新しい COMPLETED deployment を作り直すことです。
今回も正常だった revision を再デプロイすると、残っていた deployment は消え、最終的に PRIMARY / COMPLETED の 1 件だけの状態へ戻りました。
blue/green は安全に切り戻せる仕組みと思われがちですが、ロールバック自体が失敗した場合は、ローリング更新よりも復旧の自由度が低い状態に陥るケースがあると分かりました。
まとめ
- ロールバックできるかどうかは、
rolloutStateがCOMPLETEDの deployment が残っているかで決まります - ロールバックが完走するまでの数分間は
COMPLETEDが 0 件です。ここにデプロイを重ねると戻り先を失います - 戻し先のタスクが起動できないと
ROLLBACK_FAILEDにはならず、ROLLBACK_IN_PROGRESSのままリトライを続けます - 届くイベントは失敗の種類で変わります。デプロイの失敗は
SERVICE_DEPLOYMENT_FAILED、戻し先の起動失敗はSERVICE_TASK_CONFIGURATION_FAILUREです - blue/green では、戻し損ねた deployment が
IN_PROGRESSのまま残り、手動では消せません。その状態では新しいデプロイもタスクが起動しません。復旧には、正常にデプロイできていた revision を再デプロイしてCOMPLETEDを作り直す必要があります
参考リンク
- How the Amazon ECS deployment circuit breaker detects failures
- View service history using Amazon ECS service deployments
- Troubleshooting Amazon ECS blue/green deployments
- Troubleshooting with Amazon ECS Action Logs




