Amazon ECS のデプロイをCloudWatch Alarm で自動ロールバックする

  • URLをコピーしました!

こんにちは、PR TIMESでインターンをしている河野拓真です。

今回は、PR TIMESのサービスで一部使用しているAmazon ECS on Fargateに、CloudWatch Alarmと連携した自動ロールバックと、ロールバックに気づくためのSlack通知を導入したので、その仕組みと実際にハマったポイントを紹介します。

目次

背景

PR TIMESでは、一部のサービスをAmazon ECS on Fargate上で運用しています。

これまでの運用には課題がありました。ECSのデプロイが確認するのは「新しいタスクが起動し、ヘルスチェックに通ること」までであり、配置したアプリケーションが正しく動作しているかまでは保証しません。そのため、リリースした新リビジョンに不具合があり、たとえば特定のAPIが5xxエラーを返し続けるような状況になっても、デプロイの仕組みはそれを検知できませんでした。異常には人間がアラートやダッシュボードで気づき、手動でロールバックする必要がありました。

さらに、旧リビジョンへの切り戻しも通常のECSデプロイと同じ手順で実行されるため、完了までに時間がかかります。異常に気づいて手動でロールバックを開始しても、切り替えが完了するまでの間は不具合のある新リビジョンがリクエストを処理し続けるため、異常の検知からロールバック完了までの時間が、そのままユーザー影響の継続時間になっていました。

そこで今回、CloudWatch AlarmとECSのデプロイ設定を連携させ、特定のメトリクスの異常をトリガーにECSを自動でロールバックし、ターゲットリビジョンから直近の安定したリビジョンに戻す仕組みを導入しました。

ECSの自動ロールバックの仕組み

全体構成

今回作ったものは、大きく 自動ロールバックロールバックの通知 の2つの流れに分かれます。

  • 自動ロールバック: ecspresso でローリングアップデートを行い、デプロイ中〜bake time の間、ECS が ALB メトリクス(5xx・レイテンシ)を見る CloudWatch Alarm を監視します。ALARM になったらデプロイを失敗と判断し、直近の安定リビジョンへ自動で戻します。
  • 通知: デプロイ失敗イベントを EventBridge が拾い、SNS 経由で Amazon Q Developer in chat applications(旧 AWS Chatbot)から Slack に通知します。

以下今回導入した構成図です。

自動ロールバックの実現方式の選定

今回の目的は、ECSタスクが正常に起動していても、デプロイ後に「5xxレスポンスの増加」や「レイテンシの悪化」といったアプリケーション異常が発生した場合に、人手を介さず直前のリビジョンへ戻せるようにすることです。

この目的に対し、以下の観点で実現方式を比較しました。

  • アプリケーションの異常を検知できるか
  • 異常検知後に自動でロールバックできるか
  • 現行のローリングアップデート運用を維持できるか
  • 導入に伴う構成変更や運用負荷が大きすぎないか

デプロイサーキットブレーカー

ECSのデプロイサーキットブレーカーは、タスクが起動しない場合やヘルスチェックを通過できない場合にデプロイを失敗と判定し、自動でロールバックできます。

一方、タスクが正常に起動してヘルスチェックにも成功している状態で発生する、5xxレスポンスの増加やレイテンシの悪化は検知できません。そのため、今回対象とするアプリケーション異常への対策として、単独での採用は見送りました。

ただし、CloudWatch Alarmでは拾いにくいタスク起動障害を検知できるため、採用方式とは補完関係にあります。今後は両者を併用する予定です。

Blue/Greenデプロイおよびカナリアリリース

Blue/Greenデプロイでは、新しいリビジョンへ段階的にトラフィックを切り替えながら動作を評価できます。異常が見つかった場合に旧環境へ戻しやすく、本番トラフィックへの影響を抑えられる点で安全性の高い方式です。

一方で、ターゲットグループの二重化、リスナールール、デプロイ制御用のIAMロールなどが必要になり、現在のローリングアップデート構成からの変更範囲が大きくなります。デプロイ方式や運用手順も見直す必要があるため、今回の課題に対する初期対応としては導入コストが高いと判断し、採用を見送りました。

より高度なリリース制御が必要になった場合の将来的な選択肢とします。

CloudWatch Alarmを利用したECS Deployment Alarms

ECS Deployment Alarmsでは、CloudWatch Alarmがデプロイ中にALARM状態へ遷移した場合、そのデプロイを失敗と判定し、直前の正常なリビジョンへ自動でロールバックできます。

ALBのメトリクスを利用することで、タスクの起動状態だけでは判断できない以下の異常を検知できます。

  • HTTPCode_Target_5XX_Count による5xxレスポンスの増加
  • TargetResponseTime によるレイテンシの悪化

また、現在のローリングアップデート方式を維持したまま導入でき、Blue/Greenデプロイと比較して構成変更や運用への影響を抑えられます。

以上から、今回の目的に対してはCloudWatch Alarmを利用したECS Deployment Alarmsが、検知対象・自動化・導入コストのバランスが最もよいと判断し、採用しました。

なお、この方式が監視するのはデプロイ中の異常です。通常運用中に発生した障害への対応は、既存の監視・通知フローで引き続き扱います。

CloudWatch Alarmの設計

どのメトリクスでロールバックさせるか

自動ロールバックのトリガーとして一般的なのは、ALB関連の以下のメトリクスです。

  • HTTPCode_Target_5XX_Count: サーバー側のアプリケーションエラーの急増を検知
  • HTTPCode_Target_4XX_Count: クライアントエラーの急増(ルーティングの失敗など)を検知
  • TargetResponseTime: 応答レイテンシーの悪化を検知(p99などのパーセンタイル値が推奨)
  • UnHealthyHostCount: ヘルスチェックに失敗しトラフィックを受けられないタスクの増加を検知

ただし4xx系には注意が必要です。404や401のような「何回リトライしても失敗するリクエスト」が定常的に存在するため、そのままアラームにすると誤検知の原因になります。導入する場合はカスタムメトリクスで対象を絞り込んでから閾値を決める必要があり、今回はまず5xx系から導入しました。

閾値の決め方

閾値は「とりあえず90%」のような根拠のない値にせず、Golden Signals(レイテンシー、トラフィック、エラー、サチュレーション)を意識して、実際のトラフィックを観測してから決めます。

本番で使用されているECSが出力をするCloudWatch Alarmのログを計測します。その値を元にメトリクスごとに割り当てるロールバックの閾値を決めます。

段階的な導入

一気に自動ロールバックまで有効化せず、以下の手順で導入しました。

  1. 本番でCloudWatch Alarm単体を作成する(ecspresso側は rollback: false, enable: false とし、アラーム検知時の通知のみを行い、自動ロールバックはまだ行わない)
  2. モニタリングしながらサービスごとの適切な閾値を決める
  3. ステージング環境で意図的にデプロイを失敗させ、自動ロールバックの動作を確認する
  4. 本番で rollback: true に切り替える
  5. その後、デプロイサーキットブレーカーを導入する

一気に導入しないのは、一度に有効化する機能が増えるほど検証のステップが複雑になり何の機能が正しく導入できたのかなどの確認が正確に取りにくくなるからです。

ステージングでは検証を早く回すため、ECSタスクが5xxエラーを返したらすぐALARM状態になる意図的に敏感な閾値を設定して動作確認しました。

ロールバックに気づくためのSlack通知

導入を進める中で、もう1つ問題があることがわかりました。ECSが自動ロールバックしても、開発者がそれに気づく手段がないことです。

ECSデプロイが失敗してロールバックされても、GitHub Actionsのワークフロー自体はFailになりません。これでは「デプロイしたつもりが実は古いリビジョンに戻っていた」ことに誰も気づけないため、通知の仕組みを合わせて構築しました。

構成は EventBridge → Amazon SNS → Amazon Q Developer in chat applications → Slack です。

  1. ECSのdeploymentが失敗すると、ECSがEventBridgeに「ECS Deployment State Change」イベントを送る
  2. EventBridgeルールが SERVICE_DEPLOYMENT_FAILED を拾う
  3. EventBridgeのinput transformerでQ Developer用のJSONに整形する
  4. SNSトピックにpublishする
  5. Q Developer in chat applicationsがSNSトピックを監視してSlackに通知する

イベントパターンは以下の形です。

{
  "source": ["aws.ecs"],
  "detail-type": ["ECS Deployment State Change"]
}

失敗時は detail.eventNameSERVICE_DEPLOYMENT_FAILED になります。またロールバックの開始は SERVICE_DEPLOYMENT_IN_PROGRESS として送られ、reasonにロールバック先のdeployment情報が入ります。

Q Developer in chat applicationsは、以下のcustom notification schemaのJSONをSNS経由で受け取るとSlackに通知してくれます。descriptionが本文になり、Markdown・絵文字・SlackのUser IDメンションが使えます。

{
  "version": "1.0",
  "source": "custom",
  "content": {
    "description": "通知本文"
  }
}

通知システムのテスト方法

いきなりend-to-endで確認しようとすると、毎回ECSのデプロイ失敗を起こす必要があり大変です。AWSドキュメントでも案内されている通り、まずSNSトピックに直接custom notification JSONをpublishしてSlackに出ることを確認し、その後にステージングで意図的にECSデプロイを失敗させて全体を通しで確認する、という順が安全でした。

注意点として、SNSトピックを作るだけではSlackに通知されません。そのトピックをQ Developer in chat applicationsのチャンネル設定に紐づける必要があり、またSNSトピックはStandardトピックである必要があります。

ハマったポイント

ここからは導入の過程で実際にハマったポイントです。同じ構成を作る方の参考になれば幸いです。

1. CloudWatch Alarmを作成しただけではロールバックされない

最初、TerraformでCloudWatch Alarmを作成した状態で意図的にエラーを起こしても、ロールバックは発生しませんでした。CloudWatch Alarmは「メトリクスを監視して状態が変わるもの」でしかなく、ECS Serviceのデプロイ設定にアラームを紐づけて初めてロールバックのトリガーになります。ECSのデプロイライフサイクルの理解が足りず、ここで時間を使いました。

2. 不要なプロパティでアラーム監視自体が無効になっていた

アラームを紐づけたのにまだロールバックされない、という状態も経験しました。調査すると、ECSのdeployment設定でアラーム監視が効いておらず、原因はecspressoのservice定義に strategy: rolling のような明示不要なプロパティが入っていたことでした。デプロイ戦略はデフォルトのままにし、必要な設定だけを書くようにしました。

3. デプロイ開始時にアラームがALARM状態だと、そのデプロイは監視されない

検証で意図的にアラームを発報させた直後にデプロイすると、ロールバックが発生しませんでした。これはAWSドキュメントに記載のある仕様で、デプロイ開始時点でアラームが既にALARM状態の場合、ECSはそのデプロイ中のアラーム監視を行いません。失敗した初回デプロイを修正する新しいデプロイまでロールバックされ続けることを防ぐための挙動です。検証時はアラームがOKに戻るまで5分ほど待ってから次のデプロイを行う必要があります。

4. SNSトピックポリシーの権限を広くしすぎた

SNSトピックポリシーに、作成時のデフォルトに近い広い権限(SNS:DeleteTopic、SNS:SetTopicAttributesなど)をそのまま入れてしまいました。今回必要なのは events.amazonaws.com に対する sns:Publish だけです。

また、Q DeveloperがSNSトピックにアクセスする前提で chatbot.amazonaws.com への許可をトピックポリシーに追加したのも誤りで、Q Developer側のアクセス制御はリソースポリシーではなくチャンネル設定やサービスロール側で考えるべきものでした。アイデンティティポリシーとリソースポリシーの違いを整理する良いきっかけになりました。

5. 暗号化されたSNSトピックへEventBridgeからpublishする場合のKMS権限

セキュリティスキャン(Trivy)の「SNSトピックが暗号化されていない」という指摘を受けて暗号化を有効にしました。

resource "aws_sns_topic" "example" {
  name              = "example"
  kms_master_key_id = "alias/aws/sns"
}

ただし、AWSサービスをイベントソースとして暗号化済みSNSトピックにpublishさせる場合、サービスプリンシパル(EventBridgeなら events.amazonaws.com)に kms:GenerateDataKey* と kms:Decrypt の許可が必要です。AWSマネージドキー alias/aws/sns はキーポリシーを編集できないためこの権限を明示できず、確実に通すならcustomer managed keyを使う選択になります。一方でcustomer managed keyでも、このパスでは aws:SourceArn / aws:SourceAccount によるEventBridgeルール単位の絞り込みはできない制約があります。最終的にはステージングで正常に動作するかの確認が必要です。

まとめ

今回、ECS on Fargate のデプロイに CloudWatch Alarm を連携させることで、タスクの起動成功だけでは検知できない 5xx エラーやレイテンシ悪化をきっかけに、自動でロールバックできる仕組みを導入しました。

ECS のデプロイサーキットブレーカーだけでは、コンテナ起動失敗やヘルスチェック失敗は検知できますが、アプリケーションが起動した後に発生する 5xx の増加までは検知できません。そのため今回は、ALB メトリクスを CloudWatch Alarm で監視し、ECS Deployment Alarms と組み合わせる構成を採用しました。

また、自動ロールバックは発生しても開発者が気づけなければ運用上のリスクになります。そこで EventBridge、SNS、Amazon Q Developer in chat applications を利用して、ECS のデプロイ失敗を Slack に通知する仕組みも合わせて作りました。

導入してみて特に重要だったのは、CloudWatch Alarm を作るだけではロールバックは発生せず、ECS Service のデプロイ設定に正しく紐づける必要があることです。また、本番環境では通常時にも 5xx が発生する可能性があるため、閾値は固定値で決め打ちせず、実際のメトリクスを観測したうえでサービスごとに設計する必要があります。

自動ロールバックは便利な仕組みですが、誤検知すると正常なデプロイまで戻してしまう可能性があります。そのため、まずは通知とメトリクス計測から始め、ステージングで動作確認を行い、段階的に本番へ適用していく進め方が大切だと感じました。

  • URLをコピーしました!

この記事を書いた人

目次