AWS

Step FunctionsでECSタスクを実行する:runTask.syncの定義例とコンテナ上書き・失敗時の扱い

Step FunctionsでECSタスクを実行する:runTask.syncの定義例とコンテナ上書き・失敗時の扱い

AWS Step FunctionsからAmazon ECS(Fargate)のタスクを起動するには、TaskステートのResourceにarn:aws:states:::ecs:runTask.syncを指定します。本記事では、JSONataで書いたそのまま動く定義例を起点に、ContainerOverridesでコマンドや環境変数を実行ごとに差し替える方法、実行ロールに必要なIAMとiam:PassRole、配置失敗・非ゼロ終了・タイムアウト・中断のそれぞれでタスクがどうなるかを整理する内容です。コンテナから処理結果を返したい場合のwaitForTaskTokenの組み方と、AWS Batchとの使い分けまで扱います。内容は2026年10月時点のAWS公式ドキュメントに基づきます。

まとめ:Step FunctionsでECSバッチを組むときの結論と設計の勘所

完了まで待つ夜間バッチならrunTask.syncを標準ワークフローで使い、コンテナの処理結果をワークフローへ返したいときだけrunTask.waitForTaskTokenに切り替えます。Express workflowでは.syncも待機も使えません。

運用で事故になりやすいのは3か所です。TimeoutSecondsの既定値が99,999,999秒のままで、止まらないタスクを放置してしまうこと。overridesの8,192文字上限を超える値を環境変数で渡そうとすること。そしてアプリの失敗にまでRetryを掛け、同じバッチを二重に流すことです。リトライは配置失敗(AmazonECS.Unknown)に絞り、アプリの失敗はCatchで通知へ流すのが基本形になります。

ジョブキューが要るならAWS Batch、数分で終わる軽い処理ならLambdaへ寄せます。

ECSを呼ぶ3つの統合パターンとrunTask.syncが完了を待つ仕組み

Step Functions全体の仕組みや料金はAWS Step Functionsとは?ステートマシンの仕組み・料金と2種のワークフロー・採用判断を実装者目線で解説にまとめています。ここではECS統合に固有の動きだけを扱います。

3つの統合パターンの待ち方の違いと使えるワークフローで選ぶ基準

ECS向けの統合で呼べるAPIはRunTaskの1つだけで、末尾の指定によって待ち方が変わります。統合パターンの公式ページの対応表では、ECS/FargateはRequest Responseが標準とExpressの両方、.syncとwaitForTaskTokenは標準ワークフローのみとされています。

runTaskの末尾 次のStateへ進む時点 使えるワークフロー 向く用途
(なし) RunTask APIの応答直後 標準・Express 起動だけして結果を待たない処理
.sync タスクが停止した後 標準のみ 夜間バッチ・データ変換
.waitForTaskToken コンテナがSendTaskSuccessを呼んだ後 標準のみ 処理結果をワークフローへ返す場合

迷ったら.syncから始めます。結果の受け渡しが必要になった時点でwaitForTaskTokenへ移すほうが、最初から作り込むより手戻りが小さく済みます。

.syncがEventBridgeとポーリングで停止を検知する仕組みと権限

同一アカウント内の.syncでは、Step FunctionsはEventBridgeのイベントと指定APIのポーリングを併用してタスクの状態を監視します。クロスアカウントの場合はポーリングのみです。このためステートマシンの実行ロールには、ecs:DescribeTasksと、StepFunctionsGetEventsForECSTaskRuleという名前のEventBridgeルールを作る権限が必要になります。ポーリングはアカウントのAPIクォータを消費するので、数百のタスクを同時に待たせる設計ではDescribeTasksのスロットリングに注意します。

runTask.syncでFargateバッチを動かす定義例とIAMと実行手順

JSONataで書くrunTask.syncの最小構成とネットワーク設定の必須項目

次の定義は、入力のdateをコマンド引数に渡し、Fargateでタスクを1つ実行して停止まで待ちます。ECS統合の公式ページの例を、実務で必須になる項目まで埋めたものです。

{
  "QueryLanguage": "JSONata",
  "StartAt": "RunBatch",
  "States": {
    "RunBatch": {
      "Type": "Task",
      "Resource": "arn:aws:states:::ecs:runTask.sync",
      "Arguments": {
        "Cluster": "arn:aws:ecs:ap-northeast-1:111122223333:cluster/batch",
        "TaskDefinition": "arn:aws:ecs:ap-northeast-1:111122223333:task-definition/daily-job:7",
        "LaunchType": "FARGATE",
        "NetworkConfiguration": {
          "AwsvpcConfiguration": {
            "Subnets": ["subnet-0aaa1111", "subnet-0bbb2222"],
            "SecurityGroups": ["sg-0ccc3333"],
            "AssignPublicIp": "DISABLED"
          }
        },
        "StartedBy": "{% $states.context.Execution.Name %}",
        "Overrides": {
          "ContainerOverrides": [
            {
              "Name": "app",
              "Command": "{% ['python', 'main.py', '--date', $states.input.date] %}"
            }
          ]
        }
      },
      "TimeoutSeconds": 3600,
      "Output": "{% { 'taskArn': $states.result.TaskArn, 'stopCode': $states.result.StopCode } %}",
      "End": true
    }
  }
}

Fargateのタスク定義はawsvpcネットワークモードなので、NetworkConfigurationを省略すると起動できません。RunTask APIのリファレンスにあるとおり、ネイティブAPIはcamelCaseですが、Step FunctionsではClusterやTaskDefinitionのようにPascalCaseで書きます。プライベートサブネットでAssignPublicIpを無効にする場合は、ECRからイメージを取得するためにNATゲートウェイかVPCエンドポイントが要ります。

.syncの結果にはタスクの記述がそのまま入ります。Outputで必要なキーだけに絞れば、後続Stateの入出力が256KiBの上限に近づくのを防げます。キー名は、初回実行時にコンソールの実行履歴で実際の出力を見てから合わせてください。JSONataの書き方そのものはStep FunctionsのJSONata:JSONPathからの移行手順とArguments・Output・Assignの書き方で詳しく解説しています。

ステートマシン実行ロールのIAMポリシーとiam:PassRoleの落とし穴

公式のIAMテンプレートを元にした、.sync用の実行ロールのポリシーです。RunTaskはタスク定義のARNで絞れますが、停止と状態取得はタスクIDが事前に分からないため"*"になります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["ecs:RunTask"],
      "Resource": ["arn:aws:ecs:ap-northeast-1:111122223333:task-definition/daily-job:*"]
    },
    {
      "Effect": "Allow",
      "Action": ["ecs:StopTask", "ecs:DescribeTasks"],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": ["events:PutTargets", "events:PutRule", "events:DescribeRule"],
      "Resource": ["arn:aws:events:ap-northeast-1:111122223333:rule/StepFunctionsGetEventsForECSTaskRule"]
    },
    {
      "Effect": "Allow",
      "Action": ["iam:PassRole"],
      "Resource": [
        "arn:aws:iam::111122223333:role/daily-job-execution-role",
        "arn:aws:iam::111122223333:role/daily-job-task-role"
      ]
    }
  ]
}

見落としやすいのは最後のiam:PassRoleです。公式ページには、タスク実行ロール・タスクロール・タスクロールの上書きを使う場合、それぞれへのPassRoleを呼び出し元(ここではStep Functions)のロールに付ける必要があると書かれています。漏れるとRunTaskがAccessDeniedで失敗します。なお、公式の注記では"*"にしていても、Step Functionsが停止できるのは自分で起動したタスクだけです。

AWS CLIで実行を開始して停止理由と終了コードを確かめる手順

定義を作成したら、CLIから実行してタスクの終わり方を確認します。

aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:ap-northeast-1:111122223333:stateMachine:daily-batch \
  --name daily-20261017 \
  --input '{"date":"2026-10-17"}'

aws ecs list-tasks --cluster batch --started-by daily-20261017 --desired-status STOPPED

aws ecs describe-tasks --cluster batch --tasks TASK_ARN \
  --query 'tasks[0].{stopCode:stopCode,reason:stoppedReason,exit:containers[0].exitCode}'

実行名をStartedByに入れておいたので、list-tasks --started-byで、その実行が起動したタスクだけを引けます。startedByに使えるのは英数字・ハイフン・スラッシュ・アンダースコアで、長さは128文字までです。Step Functionsの実行名は80文字以内ですが、ドットや丸括弧なども許されるため、そのまま渡すとRunTaskが失敗する場合があります。実行名は英数字・ハイフン・アンダースコアだけで付ける規則にしておきます。

ContainerOverridesでコマンド・環境変数・タスクサイズを実行ごとに上書き

CommandとEnvironmentを入力から差し替える書き方と型の注意

ContainerOverrideの仕様では、Commandは文字列の配列で、イメージやタスク定義の既定コマンドを置き換えます。Environmentは既存の環境変数を上書きするか、新しい変数を追加します。どちらも対象コンテナのNameが必須です。

"ContainerOverrides": [
  {
    "Name": "app",
    "Command": "{% ['python', 'main.py', '--date', $states.input.date] %}",
    "Environment": [
      { "Name": "TARGET_DATE", "Value": "{% $states.input.date %}" },
      { "Name": "DRY_RUN", "Value": "{% $string($states.input.dryRun) %}" }
    ]
  }
]

環境変数のValueは文字列でなければなりません。入力の真偽値や数値をそのまま入れると型が合わないので、JSONataの$string()で変換します。Commandを1本の文字列で渡すとシェルを経由せず1つの引数として解釈されるため、配列で組むのが安全です。

overridesの8,192文字上限と大きなパラメータをS3キーで渡す設計

RunTask APIのリファレンスによると、overridesの合計は8,192文字までで、この数にはJSONの記号も含まれます。対象IDの一覧や設定ファイルの中身を環境変数へ詰め込む設計は、件数が増えた日に突然失敗します。

大きな入力はS3に置き、コンテナにはオブジェクトのキーだけを渡します。Step Functions側の入出力の上限256KiBも同時に避けられます。

タスクサイズの上書きとコンテナのmemory上書きを取り違えた失敗

overridesには、タスク全体とコンテナ単位のCpu・Memoryの2階層があります。コンテナ単位のMemoryはハードリミットで、超えたコンテナは強制終了されます。コンテナ側の値だけ上げても、タスクのサイズが同じなら確保できる量は増えません。Fargateではタスク単位の値を対応する組み合わせで上書きします。組み合わせの考え方はAWS Fargateとは?ECSのEC2起動タイプとの違い・料金・設定例を解説を参照してください。

配置失敗・非ゼロ終了・タイムアウト・中断でタスクがどうなるか

Failuresが空でないときのAmazonECS.Unknownと配置失敗の扱い

RunTaskはHTTP 200を返しつつ、応答のfailuresに配置できなかった理由を入れることがあります。ECS統合の公式ページによると、Request Responseではこの場合も成功扱いのまま進み、.syncとwaitForTaskTokenではAmazonECS.UnknownエラーでTaskが失敗します。

容量不足のような一時的な配置失敗は、時間をおけば通ることが多い失敗です。リトライ対象にしてよいのはこのエラーです。Request Responseで起動だけする設計では失敗に気づけないので、応答のFailuresをChoiceで確かめる処理を自分で足します。

コンテナの非ゼロ終了がStates.TaskFailedになる流れと確認方法

.syncで待っているタスクのコンテナが終了コード1で止まった場合、公開されている実行例ではStates.TaskFailedで失敗しています。Causeにはタスクの記述がエスケープされたJSON文字列で入り、ExitCodeとStopCode(EssentialContainerExited)の読み取りが可能です。この挙動はAWSのECS統合ページには明記されていないので、自分の環境でも一度は非ゼロ終了させて確かめる必要があります。

判定の単位はessentialなコンテナです。タスク定義パラメータでは、essentialがtrueのコンテナが止まると同じタスクの他のコンテナも止まり、falseのコンテナの失敗はタスクに影響しないとされています。ログ転送などのサイドカーをfalseにし、本体のバッチだけをtrueにしておけば、失敗の意味がぶれません。

TimeoutSecondsの既定値99,999,999秒を放置しない理由と決め方

Taskステートの仕様では、TimeoutSecondsの既定値は99,999,999秒です。約3年に相当し、実際には標準ワークフローの最大実行時間である1年が先に来ます。ハングしたバッチは、誰かが気づくまで動き続けます。

値は、過去の実行時間の最大値に余裕を足して決めます。毎晩20分で終わるバッチなら1時間が目安です。超えるとStates.Timeoutになります。States.TaskFailedはワイルドカードとして働きますがStates.Timeoutだけは拾わないため、Catchでは両方を列挙するかStates.ALLを最後に置きます。

実行停止時のキャンセルはbest-effortでありStartedByで孤児を追う

.syncの公式説明では、実行の停止、Parallelの別ブランチの失敗、Mapの反復の失敗のいずれかでTaskが中断された場合、Step Functionsはタスクのキャンセルを試みるとされています。ただしbest-effortで、実行ロールにecs:StopTaskが無いときや一時的な障害時には止まらず、追加料金が発生しうると明記されています。

TimeoutSecondsの超過でECSタスクが止まるかは、この一覧にありません。コンテナ側にも処理時間の上限を持たせ、StartedByで引いた取り残しのタスクをaws ecs stop-taskで止めます。停止時はSIGTERMの後、stopTimeout(既定30秒、最大120秒)を過ぎると強制終了されるため、後始末はこの時間内に収めます。

Retryを掛けてよいエラーと掛けてはいけないエラーの線引き

次の設定は、配置失敗だけをジッター付きで3回リトライし、アプリの失敗とタイムアウトは通知用のStateへ流します。

"Retry": [
  {
    "ErrorEquals": ["AmazonECS.Unknown"],
    "IntervalSeconds": 30,
    "MaxAttempts": 3,
    "BackoffRate": 2,
    "JitterStrategy": "FULL"
  }
],
"Catch": [
  {
    "ErrorEquals": ["States.TaskFailed", "States.Timeout"],
    "Output": "{% { 'error': $states.errorOutput.Error, 'cause': $states.errorOutput.Cause } %}",
    "Next": "NotifyFailure"
  }
]

States.TaskFailedにRetryを掛けると、途中まで書き込んだバッチがもう一度最初から走ります。エラー処理の公式ページのとおり、MaxAttemptsの既定は3回なので、冪等でない処理では二重計上が最大3回重なる可能性があるため注意が必要です。日付単位で上書き保存する作りのように再実行しても結果が変わらない場合に限り、アプリの失敗もリトライに含めます。LambdaでのRetryの組み方はStep FunctionsとLambdaの違い:役割の切り分けとlambda:invoke・リトライの実装手順と共通です。

処理結果を返すwaitForTaskTokenとSendTaskSuccessの実装

タスクトークンを環境変数で渡しコンテナからSendTaskSuccessを呼ぶ例

.syncの結果はタスクの記述で、コンテナが計算した値は入っていません。件数や出力先のキーを後続のChoiceで使いたいなら、Resourceをarn:aws:states:::ecs:runTask.waitForTaskTokenに変え、公式例と同じく$states.context.Task.Tokenを環境変数で渡します。

"Environment": [
  { "Name": "TASK_TOKEN", "Value": "{% $states.context.Task.Token %}" }
]

コンテナ側は処理の最後にトークンを返します。Pythonなら次のとおりです。

import json
import os
import boto3

sfn = boto3.client("stepfunctions")
token = os.environ["TASK_TOKEN"]
try:
    count = run_job()  # バッチ本体の関数(処理件数を返す)
    sfn.send_task_success(taskToken=token, output=json.dumps({"processed": count}))
except Exception as e:
    sfn.send_task_failure(taskToken=token, error="JobFailed", cause=str(e))
    raise

タスクロールにはstates:SendTaskSuccess・states:SendTaskFailure・states:SendTaskHeartbeatの許可が要ります。トークンが返らないままコンテナが落ちるとTaskは待ち続けるので、次の心拍の設定と組み合わせます。

HeartbeatSecondsで応答の途絶を検知する設定と256KiBの出力上限

HeartbeatSecondsを指定すると、その秒数のあいだにSendTaskHeartbeatが届かないTaskはStates.Timeoutで失敗します。10分ごとに心拍を送るなら、HeartbeatSecondsは900秒程度に設定し、TimeoutSecondsより小さい値にします。

outputに渡せる値は、Step Functionsのクォータにある入出力の上限256KiBに収める必要があります。処理結果の本体はS3へ書き、トークンで返すのは件数・キー・ステータスに留めます。

ECS直呼び・AWS Batch・Lambdaのどれでバッチを組むかの採用判断

Step Functions+ECSを採用する条件と見送るべき3つの場面

採用するのは、すでにコンテナイメージがあり、15分を超えうる処理を前後の検証・通知・分岐とまとめて管理したい場合です。実行ごとの入出力と失敗箇所が実行履歴に90日残るため、夜間バッチの障害調査が実行単位で追えます。ECSのタスク定義やECRへのpushが未整備なら、先にECSでDockerコンテナを動かす手順:ECRへのpush・タスク定義・料金とEKSとの違いで土台を作ります。

見送るのは3つの場面です。数千件のジョブを積んで空いた資源から順に流したいなら、キューと優先度を持つAWS Batchとは?ジョブキュー型マネージドバッチ処理の仕組みと採用判断の領分です。1件数秒の処理を大量に回すなら、Fargateの起動待ちが処理時間を上回るためLambdaに寄せます。失敗を検知する必要がない単発の定時起動なら、Step Functionsを挟む意味がありません。

ECSのバッチ基盤やワークフローをどう組むかの段階から相談したい場合は、一創のインフラ構築(AWS・Google Cloud・Azure)でタスク定義・IAM・監視までまとめて設計しています。

件数の多いバッチをMapでRunTaskに並べる場合とBatchへ寄せる場合

RunTaskは1回の呼び出しで最大10タスクまでで、.syncで待てるのも呼び出し単位です。日付ごと・テナントごとに数十本のタスクを並列実行するなら、Mapステートの中にrunTask.syncを置き、MaxConcurrencyで同時実行数を絞ります。Mapの書き方はStep FunctionsのMapステート:インラインと分散マップの違いとS3大量ファイルの並列処理手順を参照してください。

標準ワークフローの実行履歴は25,000イベントが上限で、超えると実行が失敗します。1つの実行で数千本のタスクを回す規模なら、分散マップで子実行に分けるか、AWS Batchに投入をまとめる設計へ切り替えます。

CDKのEcsRunTaskで定義を管理するときに指定する項目

定義をコードで管理するなら、CDKのEcsRunTaskを使います。JSONata用のEcsRunTask.jsonata()と、タスク単位のcpu・memoryMiBの上書き、taskTimeoutが用意されています。

const runBatch = tasks.EcsRunTask.jsonata(this, "RunBatch", {
  integrationPattern: sfn.IntegrationPattern.RUN_JOB,
  cluster,
  taskDefinition,
  launchTarget: new tasks.EcsFargateLaunchTarget(),
  taskTimeout: sfn.Timeout.duration(Duration.hours(1)),
  containerOverrides: [{
    containerDefinition,
    command: ["python", "main.py", "--date", "{% $states.input.date %}"],
  }],
});

実行ロールに付く権限はcdk diffで確認できます。CDK自体の導入はAWS CDK(cdk)とは?仕組み・使い方とCloudFormation・Terraformとの違いにまとめています。

よくある質問

Step FunctionsとECSの連携で詰まりやすい点を取り上げます。

Step FunctionsのExpress workflowでECSタスクの完了を待てますか?

待てません。Express workflowが対応するのはRequest Responseだけで、runTask.syncとwaitForTaskTokenは標準ワークフロー専用です。Expressの最大実行時間も5分なので、長時間のバッチを待つ用途に合いません。バッチの完了を待つなら標準ワークフローを選んでください。

ECSタスクのログはStep Functionsの実行履歴で見られますか?

見られません。実行履歴に残るのは、Taskの入力、RunTaskの引数、停止後のタスクの記述(停止コードや終了コード)までです。アプリのログは、タスク定義のlogConfigurationで送ったCloudWatch Logsで確認します。実行名をStartedByに入れておくと、実行からタスクARN、ログストリームの順にたどれます。

タスク定義のリビジョンを省略するとどうなりますか?

RunTask APIの仕様では、リビジョンを省略すると最新のACTIVEなリビジョンが使われます。デプロイ直後に新しいリビジョンで夜間バッチが走ってほしくない場合は、family:revisionの形で固定する設定が必要です。その場合、IAMのResourceもリビジョンを含むARNか、末尾を:*にしたARNで許可しておく必要があります。

Step FunctionsからECSタスクを実行すると料金はどう増えますか?

Step Functionsの標準ワークフローは状態遷移の回数で課金され、Retryも遷移として数えられます。ECS側は、Fargateならタスクが動いている間のvCPUとメモリの時間が課金の対象です。実行を止めてもタスクの停止に失敗すると、Fargateの課金は続きます。単価は公式の料金ページで最新の値を確認してください。

EventBridgeのスケジュールから直接RunTaskするのと何が違いますか?

直接起動では、RunTask APIが成功した時点で処理が終わります。EventBridgeの再試行ポリシーが見るのはRunTask APIの成否で、起動後にコンテナが失敗しても再試行されず、通知も自分で別に組む必要がある構成です。Step Functionsを挟むと、タスクの終了を待ってから終了コードに応じたRetry・Catch・通知を組めます。失敗に気づく必要があるバッチなら、EventBridge SchedulerからStep Functionsを起動する構成にします。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.16 テックブログ バーガーキング不正アクセスで約1,118万件の情報漏洩|対象確認・案内メールの真偽と運営側のログ点検
  2. 2026.10.15 テックブログ 銀のさら不正アクセス:最大650万件の対象確認と窓口、パスワード変更の手順
  3. 2026.10.16 テックブログ ソースネクスト不正アクセスで最大2,252万件流出|ログインできない理由・パスワード再設定と会員APIの点検
  4. 2025.12.02 テックブログ クーパン問題とは?3,755万人流出と過去最大6,246億ウォンの課徴金
  5. 2026.10.15 テックブログ リゾートトラスト不正アクセス:会員2.6万件・カハラ横浜3.6万件の対象確認と窓口

RELATED POSTS 関連記事

目次