この記事について
前回のブログでサービスが稼働し、デプロイとスケーリングの仕組みも整いました。
次に必要なのは「異常が起きたことにどう気づくか」です。
ECSサービス自体の状態異常、バッチ処理の失敗、アプリログ上のエラー急増という3つのレイヤーを、それぞれ別の仕組みで拾います。
- 基盤編: クラスタ、ネットワーク、タスク定義の分離設計、IAMロール
- デプロイ・可用性編: ECSサービス設定、Auto Scaling、ECS Exec、イメージ管理
- 監視・アラート編(本記事): CloudWatchアラーム、失敗通知、ログ監視
- バッチ・運用編: Step Functionsオーケストレーション、コスト管理、運用手順
前提として、通知先のSNSトピックを1つ用意しておきます。
resource "aws_sns_topic" "alerts" {
name = "${var.project}-${var.env}-alerts"
}
ステップ1: ECSサービス自体の異常を検知する
デプロイサーキットブレーカーはヘルスチェック失敗をトリガーにロールバックしますが、「ヘルスチェックは通っているのに実行中タスク数が想定より少ない」「CPU/メモリが張り付いている」といった状態は別の仕組みで検知する必要があります。
resource "aws_cloudwatch_metric_alarm" "ecs_running_task_count" {
alarm_name = "${var.project}-${var.env}-ecs-running-task-low"
comparison_operator = "LessThanThreshold"
evaluation_periods = 2
metric_name = "RunningTaskCount"
namespace = "ECS/ContainerInsights"
period = 60
statistic = "Average"
threshold = var.min_capacity
alarm_description = "実行中タスク数がmin_capacityを下回っている"
dimensions = {
ClusterName = aws_ecs_cluster.this.name
ServiceName = aws_ecs_service.app.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "ecs_cpu_high" {
alarm_name = "${var.project}-${var.env}-ecs-cpu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
period = 60
statistic = "Average"
threshold = 90
dimensions = {
ClusterName = aws_ecs_cluster.this.name
ServiceName = aws_ecs_service.app.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "ecs_memory_high" {
alarm_name = "${var.project}-${var.env}-ecs-memory-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "MemoryUtilization"
namespace = "AWS/ECS"
period = 60
statistic = "Average"
threshold = 90
dimensions = {
ClusterName = aws_ecs_cluster.this.name
ServiceName = aws_ecs_service.app.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
稼働タスク数のアラームは、稼働タスク数がmin_capacity以下、つまりAuto Scalingが正しく機能していない、あるいはタスクが繰り返し落ちているといった状態を拾います。
CPU/メモリのアラームは、Auto Scalingのターゲット値を超えてなお高止まりしている状態、つまりスケールアウトが追いついていない状態を検知します。
ステップ2: アプリログのエラー急増を検知する
CloudWatch Logsにはエラーログが出力されていますが、それだけでは誰も見ない限り気づけません。メトリクスフィルタでERRORレベルのログ出現数をカウントし、閾値超過でアラームを上げます。
resource "aws_cloudwatch_log_metric_filter" "app_error" {
name = "${var.project}-${var.env}-app-error-count"
log_group_name = aws_cloudwatch_log_group.app.name
pattern = "?ERROR ?Exception"
metric_transformation {
name = "AppErrorCount"
namespace = "${var.project}/${var.env}"
value = "1"
default_value = 0
}
}
resource "aws_cloudwatch_metric_alarm" "app_error_spike" {
alarm_name = "${var.project}-${var.env}-app-error-spike"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = aws_cloudwatch_log_metric_filter.app_error.metric_transformation[0].name
namespace = aws_cloudwatch_log_metric_filter.app_error.metric_transformation[0].namespace
period = 300
statistic = "Sum"
threshold = 20
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.alerts.arn]
}
パターンはERRORまたはExceptionを含む行を拾う単純なものにしています。ログフォーマットに応じてJSON構造化ログのフィールドマッチ({ $.level = "ERROR" }のような形式)に置き換えることもできますが、まずは文字列マッチで運用を始め、誤検知・見逃しの傾向を見てから調整する進め方が現実的です。
ステップ3: Step Functions(バッチ)の失敗を通知する
バッチのジョブ管理はStep Functionsで行います(詳細は次回)。
ECSタスクやデプロイの状態変化と同様、Step Functionsの実行状態もEventBridge経由で検知しますが、イベントの発行元(source)が異なるため、既存のルールとは別に新規のルールを追加する必要があります。
resource "aws_cloudwatch_event_rule" "batch_execution_failed" {
name = "${var.project}-${var.env}-batch-execution-failed"
description = "日次バッチのStep Functions実行が失敗/タイムアウト/中断した場合に検知"
event_pattern = jsonencode({
source = ["aws.states"]
detail-type = ["Step Functions Execution Status Change"]
detail = {
status = ["FAILED", "TIMED_OUT", "ABORTED"]
stateMachineArn = [aws_sfn_state_machine.batch_orchestrator.arn]
}
})
}
resource "aws_cloudwatch_event_target" "batch_execution_failed_sns" {
rule = aws_cloudwatch_event_rule.batch_execution_failed.name
target_id = "sns"
arn = aws_sns_topic.alerts.arn
}
resource "aws_sns_topic_policy" "alerts_allow_eventbridge" {
arn = aws_sns_topic.alerts.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "sns:Publish"
Resource = aws_sns_topic.alerts.arn
Condition = {
ArnEquals = { "aws:SourceArn" = aws_cloudwatch_event_rule.batch_execution_failed.arn }
}
}]
})
}
このルールでFAILED/TIMED_OUT/ABORTEDのステータス変化をsource = aws.statesのイベントから直接拾い、SNS経由で通知します。
ステップ4: EventBridge Schedulerの起動失敗を拾う
Step Functionsの実行自体が失敗するケースとは別に、EventBridge SchedulerがStep Functionsを起動すること自体に失敗するケース(権限エラーやスロットリングなど)があります。これはStep Functionsの実行イベントとして残らないため、デッドレターキュー(DLQ)を設定して拾います。
resource "aws_sqs_queue" "batch_scheduler_dlq" {
name = "${var.project}-${var.env}-batch-scheduler-dlq"
message_retention_seconds = 1209600 # 14日
}
resource "aws_sqs_queue_policy" "batch_scheduler_dlq" {
queue_url = aws_sqs_queue.batch_scheduler_dlq.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "scheduler.amazonaws.com" }
Action = "sqs:SendMessage"
Resource = aws_sqs_queue.batch_scheduler_dlq.arn
Condition = {
ArnEquals = { "aws:SourceArn" = aws_scheduler_schedule.batch_daily.arn }
}
}]
})
}
resource "aws_scheduler_schedule" "batch_daily" {
name = "${var.project}-${var.env}-batch-daily"
group_name = "default"
flexible_time_window {
mode = "OFF"
}
schedule_expression = var.batch_schedule_expression
schedule_expression_timezone = "UTC"
target {
arn = aws_sfn_state_machine.batch_orchestrator.arn
role_arn = aws_iam_role.batch_scheduler.arn
dead_letter_config {
arn = aws_sqs_queue.batch_scheduler_dlq.arn
}
retry_policy {
maximum_retry_attempts = 2
maximum_event_age_in_seconds = 3600
}
}
}
resource "aws_cloudwatch_metric_alarm" "batch_scheduler_dlq_not_empty" {
alarm_name = "${var.project}-${var.env}-batch-scheduler-dlq-not-empty"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 300
statistic = "Sum"
threshold = 0
dimensions = {
QueueName = aws_sqs_queue.batch_scheduler_dlq.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
DLQにメッセージが入るのは、スケジューラがStep Functionsの起動自体に(リトライ後も)失敗した場合です。DLQへのメッセージ到達をアラーム化しておくことで、起動失敗を即座に検知できます。
監視レイヤーの全体像
ここまでで設定したアラーム/通知経路を整理すると次のようになります。
| 検知対象 | 仕組み | 気づけること |
|---|---|---|
| ヘルスチェック失敗の連続 | デプロイサーキットブレーカー(前回) | デプロイ直後の異常、自動ロールバック |
| 実行中タスク数の不足 | CloudWatchアラーム(RunningTaskCount) | タスクが落ち続けている状態 |
| CPU/メモリの高止まり | CloudWatchアラーム | スケールアウトが追いついていない状態 |
| アプリログのエラー急増 | メトリクスフィルタ+アラーム | ヘルスチェックは通るが内部で異常が起きている状態 |
| バッチ処理の失敗 | EventBridge(Step Functions実行状態) | 日次集計の途中失敗 |
| バッチ起動自体の失敗 | EventBridge Scheduler DLQ | スケジューラがStep Functionsを起動できなかった状態 |
いずれも通知先はSNSトピック1つにまとめていますが、実運用ではSlack/PagerDuty等への連携をSNSサブスクリプションで追加し、重要度に応じてトピックを分ける運用に発展させることになります。
最終回は、日次バッチのStep Functionsオーケストレーション設計の詳細と、コスト管理・クリーンアップ・バックアップといった、コードだけでは解決しない運用面の論点を扱います。