🪐

Step FunctionsとECS fargateを使ってバッチ処理を構成した

2022/12/08に公開

ポートのSREを担当している @taiki.noda です。

少し前にJenkinsやEC2上で実行していたcronを別環境に移行しました。

バッチ処理の構成パターンも様々あると思いますが、今回は

EventBridge + Step Functionsステートマシン + ECS Fargate

で定期実行・タスクの実行環境を構築したので、解説していこうと思います。

選定理由

まず、移行するにあたってなぜこの構成を選定したのかについて。

以下のようないくつかの要件がありました。

[MUST]

  • cron 的な構文で、定期実行が可能である
  • タスクランナーとして利用できる
  • 実行結果のログが cloudwatch に残る
  • タスクの成功と失敗がわかる
  • 実行時間に制約がない
  • メモリや CPU 等のリソースを柔軟に設定できる

[SHOULD]

  • 新規で作成する時に周辺リソースや追加の設定が多くない
  • シェルスクリプト等から任意のコマンドを実行できる
  • 失敗時に再実行できる
  • ジョブの並列化が行える
  • 既存のジョブを一覧で確認できる

[WANT]

  • タスクの依存関係が定義できる
  • Web 上の UI が用意されている
  • 実行結果が slack に通知できる

ECS Fargateであれば、Lambdaのように実行時間の制約はありませんし、
Step Functionsを組み込むことで失敗時のリトライも可能です。

詳細は省きますが、おおよそ要件を満たすことができたのでこの構成を採用しました。

構成の解説

構成について解説していこうと思います。
大きく分けて以下の3つのフローがあります。

  • タスクの単発実行、定時処理の登録
  • 定時処理の実行
  • 実行結果の通知

構成図

タスクの単発実行、定時処理の登録

それぞれコンポーネントで表すと、以下のような構成です。

  • Engineer → GitHub Actions → Step Functionsステートマシン → ECS Fargate
  • Engineer → GitHub Actions → EventBridge

GitHub Actionsからタスクの実行、定時処理を登録できるようにしています。

もちろんAWSのコンソール上からも実行できるのですが、アプリケーションエンジニアの実行し易さ、権限管理上の都合からこの構成にしています。

具体的にみていきます。

ステートマシンの定義は以下のようになっていて、実行したいコマンドをECS taskの実行時にinputでoverrideできるようにしています。

step_functions.tf
resource "aws_sfn_state_machine" "prod_batch" {
・
・
・
  definition = <<EOF
{
  "Comment": "A description of my State Machine",
  "StartAt": "ECS RunTask",
  "States": {
    "ECS RunTask": {
・
・
・
        "Overrides": {
          "ContainerOverrides": [
            {
              "Name": "rails",
              "Command.$": "$.commands"
            }
          ]
        }
・
・
・

冪等性が考慮されたバッチ処理であれば、以下のようにRetryを定義に入れることで、失敗時に再実行してくれるようになります。

      "Retry": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "BackoffRate": 1,
          "IntervalSeconds": 10,
          "MaxAttempts": 2
        }
      ],

ステートマシン定義でワークフローを定義すると、AWSコンソール上から可視化することができます。

タスクの単発実行

run_task.yml
      - name: Execute Command
        env:
          ENVIRONMENT: ${{ github.event.inputs.environment }}
          NAME: ${{ github.event.inputs.name }}
          COMMAND: ${{ github.event.inputs.command }}
        run: |
          ./run_task.sh "$ENVIRONMENT" "$NAME" "$COMMAND"
        working-directory: scripts/batch
run_task.sh
  aws stepfunctions start-execution \
    --state-machine-arn "$STATE_MACHINE_ARN" \
    --name "$NAME" \
    --input "$INPUT_JSON" \
・
・

workflow上で、shellscriptを実行します。

AWS CLIを叩いて、Step Functionsに実行したいコマンドを渡しています。

定時処理の登録

add_scheduled_task.yml
      - name: Add Event and Target to EventBridge
        env:
          ENVIRONMENT: ${{ github.event.inputs.environment }}
          NAME: ${{ github.event.inputs.name }}
          SCHEDULE: ${{ github.event.inputs.schedule }}
          COMMAND: ${{ github.event.inputs.command }}
        run: |
          ./add_scheduled_task.sh "$ENVIRONMENT" "$NAME" "$SCHEDULE" "$COMMAND"
        working-directory: scripts/batch
add_scheduled_task.sh
aws events put-targets \
  --rule "$RULE_NAME" \
  --targets "Id"="$TARGET_NAME","Arn"="$STATE_MACHINE_ARN","Input"="$INPUT_JSON","RoleArn"="$EVENTBRIDGE_ROLE_ARN"

こちらも同様にAWS CLIからEventBridgeルールを追加できるようになっています。

GitHub Actions上からは以下のように、情報を入力して実行できるような形となっています。

定時処理の実行

  • EventBridge → Step Functionsステートマシン → ECS Fargate

登録されたEventBridgeルールは、スケジュールでStep Functionsステートマシンを実行し、
実行時にJSONでコマンドを渡すようにしています。

{
  "commands": ["bash", "-c", "bundle exec rake xxx"]
}

ここのEventBridgeルールは追加したりdisabledにしたりすることが多いため、terraformとの相性が悪いと考え、管理外にしています。

実行結果の通知

  • EventBridge → SNS → ChatBot → Slack

Step Functionsステートマシンの実行ステータスの変化をトリガーに、SNSでChatBotに知らせてSlackに通知を送るようにしています。

eventbridge.tf
resource "aws_cloudwatch_event_rule" "prod_batch_statemachine_succeed_rule" {
・
・
  event_pattern = <<EOF
{
  "source": ["aws.states"],
  "detail-type": ["Step Functions Execution Status Change"],
  "detail": {
    "status": ["SUCCEEDED"],
    "stateMachineArn": ["${aws_sfn_state_machine.prod_batch.arn}"]
  }
}
EOF
}

resource "aws_cloudwatch_event_rule" "prod_batch_statemachine_failure_rule" {
・
・
  event_pattern = <<EOF
{
  "source": ["aws.states"],
  "detail-type": ["Step Functions Execution Status Change"],
  "detail": {
    "status": ["FAILED", "TIMED_OUT", "ABORTED"],
    "stateMachineArn": ["${aws_sfn_state_machine.prod_batch.arn}"]
  }
}
EOF
}

成功時と失敗時でルールを別々で作成して、別々のチャンネルに通知をするようにしています。

終わりに

参考になれば幸いです。

参考

Discussion