この記事について
ここまででサービスの構築・デプロイ・監視が整いました。最後に、日次バッチ処理をどう組み立てるか、そしてコード化しきれない運用面(コスト、クリーンアップ、障害時の復旧手順)をどう設計するかを扱います。
- 基盤編: クラスタ、ネットワーク、タスク定義の分離設計、IAMロール
- デプロイ・可用性編: ECSサービス設定、Auto Scaling、ECS Exec、イメージ管理
- 監視・アラート編: CloudWatchアラーム、失敗通知、ログ監視
- バッチ・運用編(本記事): Step Functionsオーケストレーション、コスト管理、運用手順
ステップ1: バッチ処理を複数ジョブに分割する
日次バッチ処理を、単一の巨大なタスクではなく3段階のジョブに分割し、Step Functionsで順次実行する構成にします。各ジョブの役割は以下の通りです。
PaymentIntakeJob: (決済データの取り込み処理)
SalesAggregationJob: (取り込んだデータをもとにした売上集計処理)
SettlementExportJob: (集計結果の精算データ出力処理)
PaymentIntakeJob → SalesAggregationJob → SettlementExportJob
タスク定義自体は基盤編で作ったbatchタスクを使い回し、ContainerOverridesのコマンド引数だけを切り替えます。
locals {
# 3つのTask stateで共通のRunTask.syncパラメータ
batch_run_task_parameters = {
Cluster = aws_ecs_cluster.this.arn
TaskDefinition = aws_ecs_task_definition.batch.family
LaunchType = "FARGATE"
NetworkConfiguration = {
AwsvpcConfiguration = {
Subnets = var.batch_private_subnet_ids
SecurityGroups = [aws_security_group.ecs.id]
AssignPublicIp = "DISABLED"
}
}
}
# RunTask API呼び出し自体の一時的なエラー(スロットリング等)のみリトライ対象とする。
batch_run_task_retry = [{
ErrorEquals = ["ECS.AmazonECSException", "States.Timeout"]
IntervalSeconds = 30
MaxAttempts = 2
BackoffRate = 2.0
}]
}
resource "aws_sfn_state_machine" "batch_orchestrator" {
name = "${var.project}-${var.env}-batch-orchestrator"
role_arn = aws_iam_role.sfn_batch_orchestrator.arn
type = "STANDARD"
definition = jsonencode({
Comment = "日次売上集計バッチ: 3ジョブを順に実行し、前段が失敗したら後段を実行しない"
StartAt = "PaymentIntakeJob"
TimeoutSeconds = 10800
States = {
PaymentIntakeJob = {
Type = "Task"
Resource = "arn:aws:states:::ecs:runTask.sync"
Parameters = merge(local.batch_run_task_parameters, {
Overrides = {
ContainerOverrides = [
{ Name = "batch", Command = ["--job=paymentIntakeJob"] }
]
}
})
Retry = local.batch_run_task_retry
Next = "SalesAggregationJob"
}
SalesAggregationJob = {
Type = "Task"
Resource = "arn:aws:states:::ecs:runTask.sync"
Parameters = merge(local.batch_run_task_parameters, {
Overrides = {
ContainerOverrides = [
{ Name = "batch", Command = ["--job=salesAggregationJob"] }
]
}
})
Retry = local.batch_run_task_retry
Next = "SettlementExportJob"
}
SettlementExportJob = {
Type = "Task"
Resource = "arn:aws:states:::ecs:runTask.sync"
Parameters = merge(local.batch_run_task_parameters, {
Overrides = {
ContainerOverrides = [
{ Name = "batch", Command = ["--job=settlementExportJob"] }
]
}
})
Retry = local.batch_run_task_retry
End = true
}
}
})
}
各ステートはecs:runTask.syncを使い、前段のECSタスクが完了するまで後段を待機させます。
リトライ対象はRunTask呼び出し自体の一時的なエラーに限定し、コンテナが非0で終了した場合(ジョブ処理自体の失敗)はリトライせず即座に停止します。
これは「前段が失敗したら後段を実行しない」という設計意図を、インフラ的な一時エラーとアプリケーションロジックの失敗とで区別して扱うためです。ジョブの失敗検知そのものは前回設定したEventBridgeルールで拾います。
ステップ2: オーケストレーターに必要な権限だけを渡す
Step Functionsの実行ロールには、バッチタスクの起動に必要な権限のみを与えます。
resource "aws_iam_role_policy" "sfn_batch_orchestrator" {
name = "batch-orchestrator-run-task"
role = aws_iam_role.sfn_batch_orchestrator.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["ecs:RunTask"]
Resource = replace(aws_ecs_task_definition.batch.arn, "/:\\d+$/", ":*")
},
{
# RunTask.sync統合の実行状態監視・実行キャンセルに必要
Effect = "Allow"
Action = ["ecs:StopTask", "ecs:DescribeTasks"]
Resource = "*"
},
{
# RunTask.syncがECS Task State ChangeイベントをEventBridge経由で受け取るために必要なAWS管理ルール
Effect = "Allow"
Action = ["events:PutTargets", "events:PutRule", "events:DescribeRule"]
Resource = "arn:aws:events:${var.aws_region}:${data.aws_caller_identity.current.account_id}:rule/StepFunctionsGetEventForECSTaskRule"
},
{
Effect = "Allow"
Action = ["iam:PassRole"]
Resource = [aws_iam_role.task_execution.arn, aws_iam_role.task.arn]
}
]
})
}
ecs:RunTaskはバッチタスク定義のみに限定し、iam:PassRoleもバッチタスクが使う2つのロールに限定しています。ecs:StopTask/DescribeTasksとログ配信系のアクションは、RunTask.syncの仕様上リソースレベル権限に対応していないためResource = "*"が標準要件になります。
起動は EventBridge Scheduler から states:StartExecution 権限のみを持つ専用ロールで行います(DLQ設定は前回参照)。
スケジュール式はUTC基準で管理し、JST運用時刻からの変換は変数のデフォルト値側に寄せることで、Terraformコード自体はタイムゾーンに依存しない形にしています。
ステップ3: 障害時の復旧手順を文書化する
インフラコードだけでは解決しない部分として、障害発生時に「何をすればよいか」を先に決めておきます。
Flywayマイグレーション失敗時
- マイグレーションタスクが非0終了した場合、デプロイパイプラインはサービスデプロイに進みません(基盤編の設計通りです)。
- 失敗したマイグレーションSQLに対してdown migrationを用意していない場合、手動でのロールバック手順(該当バージョンのスキーマ変更を打ち消すSQLを別途作成し、flywayタスクを再実行する)を事前にRunbook化しておきます。
- 本番相当環境では、破壊的なマイグレーション(カラム削除、型変更など)を含むリリースはBlue/Greenデプロイに切り替え、旧環境を切り戻せる状態を保ったまま移行します。
日次バッチ失敗時
- どのジョブ(paymentIntakeJob/salesAggregationJob/settlementExportJob)まで成功したかは、Step Functionsの実行履歴とCloudWatch Logsから確認できます。
- 各ジョブが冪等(同じ入力で再実行しても結果が変わらない)であることを前提に設計し、失敗したジョブ以降のみを手動で再実行できるようにしておきます。冪等性が担保できていないジョブがある場合は、それ自体を先に解消すべき設計上の課題として扱います。
- 再実行はStep Functionsの「特定ステートからの再実行」機能、またはECSタスクを直接RunTaskで起動する手動オペレーションのいずれかを、事前にRunbookとして手順化しておきます。
まとめ
ECS Fargate環境を「土台(クラスタ・ネットワーク・タスク定義・IAM)→稼働(デプロイ・可用性・スケーリング)→検知(監視・アラート)→バッチと運用(オーケストレーション・コスト・復旧手順)」という順序で構築してきました。
構成管理の観点では、Terraformが担う範囲(インフラの構造)とCI/CDパイプラインが担う範囲(デプロイの中身)を明確に分け、lifecycle.ignore_changesで競合を避ける設計が一貫して重要になります。また、IAM権限・サービスのフラグ・監査ログという3点セットが揃って初めて機能する設定(ECS Execがその典型です)があることも、構築時に見落としやすいポイントです。