Step FunctionsとAWS Batchによるバッチワークフロー構築の課題と対処例 – ENECHANGE Developer Blog

ENECHANGE所属のエンジニア id:tetsushi_fukabori こと深堀です。
弊社テックブログではAIリレーブログが大盛況のうちに終わりました。
その直後の記事が私の人手による温かみに溢れた記事で良いのか不安ではありますが、書いていこうと思います。


今回は2025年7-9月に実施した比較的大規模なアプリケーションバッチワークフローを、AWS Step Functionsへ移設を検証した際の知見の記事です。
AWS Step FunctionsはJSONで定義した順序通りに処理を実行するワークフローを実現する機能で、AWSの各種機能への統合が簡単に行える良さがあります。
今回移行するワークフローはもともとECSで稼働するアプリケーションで実装されていたため、コンテナの設定などを極力流用しつつ実現できる方法を検討しました。
また、ワークフローには大規模な並列処理も含まれているため、こちらはAWS Batchを利用しました。
Step Functions組み込みのMapではなくAWS Batchを利用した理由についても説明していきます。

この記事を届けたい人

  • AWSでバッチワークフローの実現方法を考えている
  • Step FunctionsやAWS Batchのナレッジを知りたい

今回の記事に登場する主要なAWSのサービス群について軽く説明します。
概説なので読み飛ばしていただいて構いません。

AWS Step Functions


様々な方法で実現される処理とその実行順序を定義し実行を管理できるワークフローサービスです。

aws.amazon.com

JP1(日立ソリューションズ)やSenju(野村総合研究所)のイメージで考えてくださってよいでしょう。
とは言え上記の製品と違い、Step Functionsは基本的にジョブ管理だけするものと思ってよいです。
JP1/Senjuのようなアラート発報や監視ダッシュボードなどの運用機能は持たず、それらはCloudWatch Alarms、SNS、EventBridgeなど別のAWSサービスを組み合わせて実現します。
AWSのAPI呼び出しが事前に定義された処理として多数提供されているので、AWSの機能をフルに使う場合はかなり素早くワークフローを実現できるかと思います。

Step Functionsではワークフローのことをステートマシン、呼び出される処理をステートと呼びます。
ステートマシンはJSONで実装可能です。

{
  "Comment": "A description of my state machine",
  "StartAt": "GetObject",
  "States": {
    "GetObject": {
      "Type": "Task",
      "Parameters": {
        "Bucket": "MyData",
        "Key": "MyData"
      },
      "Resource": "arn:aws:states:::aws-sdk:s3:getObject",
      "End": true
    }
  }
}

Amazon ECS


AWS上でコンテナアプリケーションを実行・管理するサービスです。弊社のアプリケーションも大半はECS上のコンテナイメージとして実装〜デプロイ〜運用されています。

aws.amazon.com

今回の話に関連する部分では、ECSのインフラ(コンテナを実行するホスト環境)にはFargateとEC2という選択肢があります。
FargateはAWS側が管理するインフラでユーザー側がインスタンスを管理しなくて良いです。
EC2はインスタンスを管理する必要がありますがFargateに比べてやや割安なのと、Fargateと異なりスケーリングの管理ができるのでECSタスク(コンテナを複数まとめたひとまとまりのアプリケーションをタスクと呼びます)の実行前にインフラを用意でき、Fargateで発生するプロビジョニング待ちを抑制できます。
また、EC2ではコンテナを管理するエージェントの設定が変更でき、挙動を調整できます。
エージェントはOSSになっています。

github.com

AWS Batch


AWS Batchはひとまとまりの処理を実行するのに適した大規模分散コンピューティング実行管理のサービスです。

aws.amazon.com

アーキテクチャ的にはジョブキューとそれに紐づくコンピューティング環境という構成で、ジョブ投入側とデカップリングされている構造です。

今回は特に配列ジョブを使っています。
配列ジョブは親ジョブとして配列サイズつきで投入すると、配列サイズ分の子ジョブが作られ各子ジョブには異なる配列インデックスが引き渡されます。
配列インデックス以外はすべて共通のパラメータが渡されるので、大体使い方としては以下のようなイメージになると思います。

  1. 処理したいデータの配列Aを用意する
  2. Aのサイズを配列サイズとして配列ジョブを投入する。この際A自身も引数として子ジョブに引き渡す。
  3. 子ジョブは A[配列インデックス] を処理対象として処理

こんな感じにすることで配列Aの全量を並列処理として実行できます。

AWS Batchはコンピューティング環境の実行インフラにはECSを使っており、クラスターなどは自動で作成されます。
このECSのインフラにはFargateとEC2を指定でき、メリット・デメリットはECSのそれとほぼ同じです。

その他

ECSのインフラをEC2にすると、EC2インスタンスのスケーリングにASGとキャパシティープロバイダーというものが出てきます。
基本的な考え方ではキャパシティプロバイダーはECSのメトリクス(例えばRunTaskの実行など)を監視して必要なインフラの規模を計算し、ASGに希望キャパシティという形で伝えます。
ASGは希望キャパシティを満たすために起動テンプレートにしたがってインスタンスを起動・停止し、ECSクラスターに使用可能なインフラとして登録・解除します。

この構造はAWS Batchも近い物があり、AWS Batchが作成したECSクラスターのキャパシティプロバイダーやASGは操作できません。できるのかもしれませんがしないほうが良いです。
というのもキャパシティプロバイダーの操作は基本的に人間が介入するものではないためです。
人間が介入するポイントはいくつかありますが、具体的にインフラの容量を操作したければ、ECSであればASGの最小キャパシティ/最大キャパシティを、AWS Batchであれば最小vCPU/最大vCPUを操作しましょう。
この値はスケーリングでは調整されず、この値の中で提供可能なキャパシティをASG等が提供する形になるため、ここでインフラの規模は制御可能です。

今回移行実装を検証したワークフロー特有の難所について解説します。
とはいえ、いずれも比較的一般的なポイントかと思います。

極力現実装を活かしたい

既存実装があり、アプリケーションコードも整備されているため、今回のStep Functions実装に当たって必要なアプリケーション実装を極力減らしたいです。
特に業務レベルのコードは変更することで必要になるテストが膨大になりかねないため、既存実装をそのまま流用したいです。

1500近い並列度の並列処理がある

データ量に依存しますが、高い並列度で並列処理を実現したいです。
現行のワークフローでも実現されていますが、それでもバッチワークフロー全体の処理時間が長く性能が問題になっています。
このため、極力性能を上げる意味でも高い並列度でワークフローを実現する必要があります。

アプリケーションの状態によって並列度が変わる部分がある

ワークフロー内でデータの状態やトリガーの指示により、処理対象のデータが事前に決められず、並列処理の並列度が動的にしか決定できないことがあります。
このためワークフローの並列処理の構造は、ワークフロー内で算出した値を使える構造になっている必要があります。

今回新たなStep Functionsワークフローを実現するにあたり出会った課題とその解消方法を列挙していきます。

課題: 現実装を極力活かしたい

難所として挙げた通り、既存実装を極力活かしたいです。
バッチワークフロー管理の方式をアプリからStep Functionsに移行する以上の変更はテスト範囲の増大を招きますし、コストが高くつきます。

解決: ECS RunTaskステートを使いタスクランナーを呼び出す

Step Functionsで用意されているステートには ECS RunTask というステートがあります。
これはDockerでいうところの docker run {image_name} {command} のようなもので、指定したクラスター(実行インフラ)とタスク定義(イメージ)と上書き設定(環境変数やコマンド)でコンテナを実行するものです。

{
  "Comment": "A description of my state machine",
  "StartAt": "ECS RunTask",
  "States": {
    "ECS RunTask": {
      "Type": "Task",
      "Resource": "arn:aws:states:::ecs:runTask.sync",
      "Comment": "ECSタスク実行",
      "Parameters": {
        "Cluster": "arn:aws:ecs:ap-northeast-1:999999999999:cluster/xxx",
        "TaskDefinition": "arn:aws:ecs:ap-northeast-1:999999999999:task-definition/xxx:1",
        "Overrides": {
          "ContainerOverrides": [
            {
              "Name": "app",
              "Command": [
                "bundle",
                "exec",
                "rake",
                "hoge:fuga"
              ],
              "Environment": [
                {
                  "Name": "FOO",
                  "Value.$": "BAR"
                }
              ]
            }
          ]
        }
      },
      "End": true
    }
  }
}

この例ではアプリケーションはRuby on Railsですが、RubyのタスクランナーであるRakeタスクとして既存のアプリケーションコードの呼び出しのラッパータスクを実装しています。
これによりインターフェイスだけRakeタスクで提供し、既存のアプリケーション実装には手を加えない方式を実現できます。

課題: ECS RunTask(on Fargate)ステートの実行時間が長い

ECS RunTaskを使うことでECSで実装されていたアプリケーションコードを流用できますが、もともとFargateで実行されていたECSタスクなので同様にFargateとしてStep Functionsから呼び出すと、Fargateのプロビジョニング待ちがかなり発生します。
バッチワークフローでは積み重ねでかなり多くのECS RunTaskが発生するためこれを削減する必要があります。

解決: ECSクラスターのインフラをEC2にする&コンテナイメージのキャッシュ設定を調整する

ECSクラスターのインフラをFargateからEC2にし、ワークフローの先頭と末尾でEC2をコントロールするASGのキャパシティを操作することでワークフロー中ずっと同じインスタンスを使い回せるようにします。
この設定であればワークフロー開始時にはインスタンスの起動待ちが発生するものの、ワークフロー実行中の大半のステートではインフラのプロビジョニング待ちは発生しません。

{
  "Comment": "A description of my state machine",
  "StartAt": "ASG_SetInitialCapacity",
  "States": {
    "ASG_SetInitialCapacity": {
      "Type": "Task",
      "Comment": "ワークフロー開始時にECS ASG MaxSizeを設定",
      "Resource": "arn:aws:states:::aws-sdk:autoscaling:updateAutoScalingGroup",
      "Parameters": {
        "AutoScalingGroupName": "ecs-asg",
        "MaxSize": 1,
        "MinSize": 1
      },
      "Next": "ECS RunTask"
    },
    "ECS RunTask": {
      ...
      "Parameters": {
        ...
        "CapacityProviderStrategy": [
          {
            "CapacityProvider": "dev2-emap-re-tepco-workflow-cp",
            "Weight": 1
          }
        ],
        "PropagateTags": "TASK_DEFINITION",
        "PlacementStrategy": [
          {
            "Type": "binpack",
            "Field": "cpu"
          }
        ]
      },
      "Next": "ASG_SetZeroCapacity"
    },
    "ASG_SetZeroCapacity": {
      "Type": "Task",
      "Comment": "ワークフロー終了時にECS ASG MaxSizeを設定",
      "Resource": "arn:aws:states:::aws-sdk:autoscaling:updateAutoScalingGroup",
      "Parameters": {
        "AutoScalingGroupName": "ecs-asg",
        "MaxSize": 0,
        "MinSize": 0
      },
      "End": true
    }
  }
}

もちろんスケーリングの設定を調整することでASGの直接操作をなくしても良いのですが、キャパシティプロバイダーが希望キャパシティを変更するまでのタイムラグがどうしても有るため、削減のために明示的にASGを操作しました。

また、ASGが使用する起動テンプレートのユーザースクリプトに以下の設定を入れることでECSクラスターへの登録とコンテナイメージのキャッシュ設定を指定できます。

cat  >> /etc/ecs/ecs.config
ECS_CLUSTER=xxx
ECS_IMAGE_PULL_BEHAVIOR=prefer-cached
EOF

prefer-cachedはEC2インスタンス上のコンテナイメージを極力使い回す指定です。
これを入れることで起動したEC2インスタンス上で稼働するECSタスクのタスク定義のイメージを揃えておけば、イメージのPullは初回だけで済むため爆速でタスクが起動できます。

課題: ECS RunTaskステートの出力にタスクランナーの結果を直接引き渡せない

ECS RunTaskステートで手に入った値をワークフローに引き回したいことがあります。
例えばあるステートで番号を生成し、その番号までワークフローでループ処理を回すような場合、現在番号や最終番号を引き回したいです。
一方で、ECS RunTaskステートではステートの出力はあくまでECS RunTask APIの実行結果であり、実行されたアプリケーションコードの結果をステートマシンに引き渡してループ変数として使うようなことはできません。

解決: タスクランナーがタスク実行結果をJSON形式でS3にPUTする

タスクランナーの実装で生成した値をJSON形式でS3にPUTします。
このファイルをS3GetObjectステートで取得するとステートの出力としてファイルの内容のJSONを得られます。
二段階になってしまいますがアプリケーションコードの出力をステートマシンに引き渡せます。

{
  "Comment": "A description of my state machine",
  "StartAt": "ECS RunTask",
  "States": {
    "ECS RunTask": {
      ...
      "Parameters": {
        ...
        "Overrides": {
          "ContainerOverrides": [
            {
              ...
              "Environment": [
                {
                  "Name": "RESULT_S3_URL",
                  "Value.$": "s3://my-bucket/path/to/result.json"
                }
              ]
            }
          ]
        }
      },
      "Next": "Get Result"
    },
    "Get Result": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:s3:getObject",
      "Comment": "ECS RunTaskの結果取得",
      "Parameters": {
        "Bucket": "my-bucket",
        "Key": "path/to/result.json"
      },
      "End": true
    }
  }
}

なおs3:getObjectステートの出力のBodyキーの値としてJSONファイルの内容が入っています。

別解としてecs:runTask.syncではなくecs:runTask.waitForTaskTokenが使えます。
この方式ではStep FunctionsからECSタスクにワンタイムTokenを引き渡し、ECSタスクが受け取ったワンタイムToken付きで以下の形でSendTaskSuccess APIまたはSendTaskFailure APIを呼び出すことでステートマシンに任意のアプリケーションデータを引き渡せます。

{
  "output": "{}", # 任意のJSON文字列形式のアプリケーションデータ
  "taskToken": "TokenString"
}

この形式が使えるサービス統合は以下のページに一覧があります。

docs.aws.amazon.com

今回はS3パターンにしました。

課題: バッチ実行中に動的に算出される値で処理の並列度が変わる

バッチ中の一部の処理では並列度を動的に決定する必要があります。
この算出もアプリケーションの一部です。

解決: ワークフロー制御専用のタスクを実装しMapでECS RunTaskを呼び出す

ワークフローの制御用のタスクを新たに実装しました。
並列処理には分散Mapステートを利用することで、制御用タスクから得られる処理対象ID配列をソースに分散Mapを作り、Map内の各処理は単一の処理対象IDを受け取るECS RunTaskとすることで動的にECS RunTaskの並列度が変更可能です。
また、分散Mapであればソース(Mapの入力になる配列)が格納されているS3上のJSONファイルを直接指定できるため、前述のS3GetObjectステートも必要ありません。

{
  "Comment": "A description of my state machine",
  "StartAt": "ECS RunTask",
  "States": {
    "ECS RunTask": {
      ...
      "Parameters": {
        ...
        "Overrides": {
          "ContainerOverrides": [
            {
              ...
              "Environment": [
                {
                  "Name": "RESULT_S3_URL",
                  "Value.$": "s3://my-bucket/path/to/result.json"
                }
              ]
            }
          ]
        }
      },
      "Next": "Map"
    },
    "Map": {
      "Type": "Map",
      "ItemReader": {
        "Resource": "arn:aws:states:::s3:getObject",
        "ReaderConfig": {
          "InputType": "JSON"
        },
        "Parameters": {
          "Bucket": "my-bucket",
          "Key": "path/to/result.json"
        }
      },
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "DISTRIBUTED",
          "ExecutionType": "STANDARD"
        },
        "StartAt": "個別Map処理",
        "States": {
          "個別Map処理": {
            "Type": "Task",
            "Resource": "arn:aws:states:::ecs:runTask",
            ...,
            "End": true
          }
        }
      },
      "End": true
    }
  }
}

課題: 大規模な分散MapでECS RunTaskステートを使うとスロットリングエラーが発生する

ある程度実装してから気がつくことになると思いますが、ECS RunTask APIは秒間リクエスト数のクオータが存在し、上回るとスロットリングエラーが発生します。
以下のページのCluster resource modify actionsがそれです。

docs.aws.amazon.com

「Bucket maximum capacity (or Burst rate)」と「Bucket refill rate (or Sustained rate)」があり、高い方の前者でも100req/秒がクオータです。
このクオータはアカウントレベルかつ内部的な引き上げ上限があり引き上げても200req/秒までです。内部上限についてはサポートからその回答を受けています。
アカウントレベルなのでエラーになったらリトライ、のような実装だとアカウント内にある別プロダクトがエラーを食らうことがあります。

Step FunctionsのMapはAPIの呼び出しが結構早く、並列度が高い場合はすぐにクオータに達します。
もちろんMapには同時実行数を制限するオプションはありますが、それはつまり並列度が下がるので実行時間に悪影響があります。

解決: AWS Batchの配列ジョブを使用する

前述のAWS Batchの配列ジョブを使い、MapのソースになるJSON配列をパラメータの一つとして渡しつつ配列サイズをBatchのArrayサイズとしてArray Indexで配列の要素を処理するようなアプリケーションコードにする実装です。
AWS Batchを呼び出す場合、実体はECSタスクを並列数分実行するのではあるのですが、API呼び出しとしてはBatchSubmitJobの呼び出し1回扱いになります。
(ちょっと納得がいかない感じはあります)

{
  "Comment": "A description of my state machine",
  "StartAt": "ECS RunTask",
  "States": {
    "ECS RunTask": {
      "Type": "Task",
      "Resource": "arn:aws:states:::ecs:runTask.sync",
      "Comment": "ECSタスク実行",
      "Parameters": {
        "Cluster": "arn:aws:ecs:ap-northeast-1:999999999999:cluster/xxx",
        "TaskDefinition": "arn:aws:ecs:ap-northeast-1:999999999999:task-definition/xxx:1",
        "Overrides": {
          "ContainerOverrides": [
            {
              "Name": "app",
              "Command": [
                "bundle",
                "exec",
                "rake",
                "hoge:fuga"
              ],
              "Environment": [
                {
                  "Name": "RESULT_S3_URL",
                  "Value.$": "s3://my-bucket/path/to/result.json"
                }
              ]
            }
          ]
        }
      },
      "Next": "Get Result"
    },
    "Get Result": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:s3:getObject",
      "Comment": "ECS RunTaskの結果取得",
      "Parameters": {
        "Bucket": "my-bucket",
        "Key": "path/to/result.json"
      },
      "Next": "並列Batch"
    },
    "並列Batch": {
      "Type": "Task",
      "Resource": "arn:aws:states:::batch:submitJob.sync",
      "Parameters": {
        "JobName": "xxxx",
        "JobDefinition": "arn:aws:batch:ap-northeast-1:999999999999:job-definition/abcd:10",
        "JobQueue": "arn:aws:batch:ap-northeast-1:999999999999:job-queue/efgh",
        "ArrayProperties": {
          "Size.$": "States.ArrayLength($.Body)"
        },
        "ContainerOverrides": {
          "Command": [
            "bundle",
            "exec",
            "rake",
            "hoo:batch"
          ],
          "Environment": [
            {
              "Name": "INPUT_ARRAY_JSON",
              "Value.$": "States.JsonToString($.Body)"
            }
          ]
        }
      },
      "End": true
    }
  }
}

この方式であれば並列度については調整せずにECSタスクをBatch経由で大規模並列で実行できます。

課題: Step Functionsからの自動リトライだとAWS Batchの配列ジョブでは成功していたものも含め全子ジョブが再実行される

Step Functionsにはリトライ機構があり、エラーの自動回復ができます。
ただ、これをAWS Batchの配列ジョブで行うと、配列全体の再投入となります。
成功している子ジョブまで再投入されるので、不要な処理時間が発生します。

解決: AWS Batchのリトライ戦略を設定し、配列ジョブの子ジョブ単位で自動リトライする

AWSのドキュメントからはうまく見つけられなかったのですが、AWSブログを参照すると以下の記事に記載があります。

aws.amazon.com

How job retries work with Array jobs

Before we dive into the timeouts and retries in case of Array jobs, let us understand what Array jobs are. An array job is a type of Batch job that shares common parameters, such as the job definition, vCPUs, and memory. It runs as a collection of related, yet separate, basic jobs that may be distributed across multiple hosts and may run concurrently. When you have a requirement to run parallel jobs like Monte Carlo simulations, you should consider using Array jobs.

As far as the job submission is concerned, these jobs are submitted like the usual AWS Batch jobs with a specification of Array size. For example, if you submit a job with an array size of 100, one single job will run and also spawn 100 child jobs. The submitted array job becomes the reference or pointer to manage all the child jobs. This helps you to submit large parallel workload jobs with a single query.

When an array job is submitted, the parent array job gets a normal AWS Batch job ID. Every child job has the same base ID, but the array index for the child job is appended to the end of the parent ID, such as example_job_ID:0 for the first child job of the array. If an array job fails, Batch will:

  • Retry the child jobs individually.
  • The entire parent is not retried.
  • Jobs that have been cancelled or terminated are not retried. Also, jobs that fail due to an invalid job definition are not retried.

自動翻訳:

## 配列ジョブでのジョブ再試行の仕組み

アレイジョブのタイムアウトと再試行について詳しく説明する前に、アレイジョブとは何かを理解しておきましょう。アレイジョブは、ジョブ定義、vCPU、メモリなどの共通パラメータを共有するバッチジョブの一種です。関連性がありながらも独立した基本ジョブの集合として実行され、複数のホストに分散して同時に実行されることもあります。モンテカルロシミュレーションなどの並列ジョブを実行する必要がある場合は、アレイジョブの使用を検討してください。

ジョブの送信に関しては、これらのジョブは通常のAWS Batchジョブと同様に、配列サイズを指定して送信されます。例えば、配列サイズが100のジョブを送信すると、1つのジョブが実行され、同時に100個の子ジョブが生成されます。送信された配列ジョブは、すべての子ジョブを管理するための参照またはポインタとなります。これにより、大規模な並列ワークロードジョブを単一のクエリで送信できるようになります。

配列ジョブが送信されると、親配列ジョブは通常のAWS BatchジョブIDを取得します。すべての子ジョブは同じベースIDを持ちますが、子ジョブの配列インデックスは親IDの末尾に追加されます。例えば、配列の最初の子ジョブの場合はexample_job_ID:0となります。配列ジョブが失敗した場合、Batchは次の処理を行います。

- 子ジョブを個別に再試行します。
- 親全体は再試行されません。
- キャンセルまたは終了したジョブは再試行されません。また、無効なジョブ定義のために失敗したジョブも再試行されません。

つまりStep FunctionsではなくAWS Batchのリトライ機構を使うことで個別の子ジョブの自動リトライが可能になります。

なお、Batchで実行される個々の処理で以下のような性質を実現していればStep Functionsのリトライ機構でリトライしても良いでしょう。

  • 冪等である
  • 一度でも成功したら以降は重複実行が自動的にスキップされる

この状態であれば親ジョブ全体が再実行されても、個々の子ジョブは「すでに成功済みの場合は早期終了」「エラー終了していた場合はリトライ」が実現可能になります。

課題: AWS Batchのコンピューティング環境のスケーリングを制御したい

AWS Batchを使い始めると処理の実行環境であるコンピューティング環境のスケーリングの遅さが気になることがあります。
コンピューティング環境の最小vCPUを固定値で設定しておくとインスタンスが常に確保されているのですぐにタスクが実行できますが、ワークフローが動いていない間は無駄なリソースになります。

解決: コンピューティング環境の最小vCPU/最大vCPUをワークフローから動的に変更する

ECS RunTaskと同様に、Batchのコンピューティング環境をEC2にしたうえでワークフローの冒頭と最後でコンピューティング環境の最小vCPU/最大vCPUを変更してあげることで緩和可能です。
先に起動を始めてしまって、必要になる前に立ち上げる形式です。

{
  "Comment": "A description of my state machine",
  "StartAt": "SetupBatchComputeEnvironment",
  "States": {
    "SetupBatchComputeEnvironment": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:batch:updateComputeEnvironment",
      "Parameters": {
        "ComputeEnvironment": "batch-compute-env",
        "ComputeResources": {
          "MinvCpus": 1000,
          "MaxvCpus": 1000
        }
      },
      "Next": "並列Batch"
    },
    "並列Batch": {
      ...
      "Next": "ZeroBatchComputeEnvironment"
    },
    "ZeroBatchComputeEnvironment": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:batch:updateComputeEnvironment",
      "Parameters": {
        "ComputeEnvironment": "batch-compute-env",
        "ComputeResources": {
          "MinvCpus": 0,
          "MaxvCpus": 0
        }
      },
      "End": true
    }
  }
}

課題: AWS Batchでのジョブ起動時間を短くしたい

配列ジョブで大量の子ジョブを実行する場合、すべての子ジョブは実体としてECSタスクとして起動されます。
このため、ECS RunTaskのときと同様にイメージのPullやインフラのプロビジョニング待ち問題があります。

解決: EC2をインフラに指定し、各種設定をチューニングする

AWS Batchはコンピューティング環境として以下を指定できます。

  • Fargate
  • EC2(UNMANAGED)
  • EC2(MANAGED)
  • EKS

インフラ側を制御したいのでEC2で考えます。
MANAGEDとUNMANAGEDの差はAWS Batchにスケーリングやインスタンス起動停止などの管理をさせるかユーザ自身が行うかです。

EC2インスタンスをインフラにするにあたり、AWS Batchで実行するジョブ定義(ほぼECSのタスク定義と同じJSON形式の定義リソース)のvCPU数を考慮して複数のジョブが1台のEC2インスタンスで実行できる用に設定すると、ジョブ実行のたびにインフラをプロビジョニングすることがなくなります。
もちろん最小vCPU/最大vCPUでインフラを起こしきっていてもいいですが、そうであってもEC2インスタンスに相乗りさせる嬉しさは同一ジョブなら同一イメージなのでEC2インスタンス上のイメージキャッシュがとても良く効いてくれるためです。
ECS同様にEC2インスタンスをインフラとする場合、その起動テンプレートでイメージのキャッシュが効くように設定することが可能です。
ただしユーザーデータはMIMEマルチパートアーカイブ形式で作成する必要があります。内容はキャッシュ設定だけ指定しておけばよいです。

docs.aws.amazon.com

MIMEマルチパートアーカイブ形式で書かせることでAWS Batchが制御用に挿入したい情報とマージできるようです。
おそらくクラスター名などはここで挿入されているのだと思います。

この状態であればイメージのPullとプロビジョニング両方が削減できるので速度向上が見込めます。

ただし、1台で大量のECSタスクを起動すると、(おそらく)EBSのファイルシステムのI/Oが耐えきれずにタスク起動に失敗します。
c8g.48xlarge(192vCPU/384GiBメモリ)で192タスクを起動しようとしましたがエラーで失敗しました。
このためバランスは要調整です。

DockerTimeoutError: Could not transition to created; timed out after waiting 4m0s

以降では課題としてありつつも解消できなかったものや、仕様理解を間違ったために苦労することになった部分などを列挙しています。
記事のおまけですね。

Step FunctionsのJSON関数の出力操作関数が貧弱

JSOPathを最初に使い始めてしまったのでそれが良くないと言えばそうなのですが、JSONPathだと使える組み込み関数が貧弱です。

docs.aws.amazon.com

JSONataはだいぶリッチな機能を簡素なクエリ言語で表現してくれるので、こちらにするのが良さそうです。
というか、JSONataがステートマシンをマネコンのUIで作成したときのデフォルトなので、こちらが推奨なんでしょうね…。
デフォルトでは以下のステートマシン設定になっています。

{
  "Comment": "A description of my state machine",
  "StartAt": "",
  "States": {},
  "QueryLanguage": "JSONata"
}

AWS Batchの配列ジョブではFast Failができない

これは制約のようです。
配列ジョブで一部のジョブが失敗してもFast Failできず、すべての子ジョブが終了するまで親ジョブは実行中状態のままです。

docs.aws.amazon.com

個々の子ジョブを、他の子ジョブに影響を与えずにキャンセルまたは終了できます (FAILED ステータスに移動させる)。ただし、子配列ジョブが失敗した場合 (それ自身の失敗または手動でキャンセルもしくは終了した場合)、親ジョブも失敗します。このシナリオでは、すべての子ジョブが完了すると、親ジョブは FAILED に移行します。

どうしてもFast FailしたければEventBridgeなどで子ジョブのFailを捕まえて親ジョブをキャンセルするなどの対応が必要かもしれません。

今回は私がStep Functionsでバッチワークフローを移行する際にぶつかった課題と解決について列挙してみました。
同じ様な課題に突き当たっている方の力になれれば幸いです。


元の記事を確認する

関連記事