AWS Step FunctionsでECSタスクを定期実行する仕組みをSAMでデプロイする

7 min read読了の目安(約6700字

概要

サービスのデプロイをECSで行っている場合、バッチ実行も同じコンテナ上から実行したいケースがあります。そのとき、ECSの機能として”タスクのスケジューリング”というものがあり、コンソール上からポチポチ設定するだけで定期的にタスクを起動し、外部からコマンドを注入して任意のバッチ実行が可能です。

しかし、タスクのスケジューリング機能を使っている場合、タイムアウトやリトライ等の細かい設定を行うことができません。具体的なリスクとして、想定以上にバッチ実行に時間が掛かり利用料が予想以上に掛かってしまうことが挙げられます。

本件についてAWSサポートに問い合わせしたところ、Step Functionsの利用を推奨いただきました。本記事では取り組んでみた内容のログ、および参考文献の整理をします。

Step Functionsとは

Step Functionsを利用すると、AWS独自のJSON形式の記述によって、複数のLambda関数やSNS、Batch、ECS、DynamoDBなど複数のサービスの連携を記述できます。今回はECSとの連携に焦点を当てています。

ほとんどノーコードに近い感覚です。思ったより簡単なので名前だけ聞いたことのある感じの方はコンソールで触ってみるといいでしょう。特に動かしたいLambda関数がなくてもチュートリアルの作成は可能なので、すぐに肌感覚を得ることができます。

メリットとしては実行時にリトライやタイムアウトといったアプリケーション本体とは異なる要件をJSON記法で指定できることです。これにより、ECSタスクの実行にタイムアウトを設定でき、当初の課題を解決できます。

SAMとは

SAMを利用すると、専用のSAM CLI(要はsamコマンド)を利用して、yaml形式で記述したインフラ設定をそのままデプロイできます。SAMはServerless Application Modelの略で、Lambdaを利用ケースの中心として考えているようですが、今回の要件のようにECSタスクを実行するStep Functionsにも対応しています。

sam initコマンドを実行するとサンプルアプリケーションが設定された状態でローカル上にディレクトリが展開されるので、あとは展開されたコードを読めばなんとなくわかります。

cdkとの住み分けが個人的にまだ良くわかっていないところです。長期的にはCDKのほうが普及しそうですが、今回のように少しマイナーな事例だとCDKでどうやって実現するべきか分かりにくいため、最初はSAMでやってみて余裕があればCDKにする、くらいがちょうどいいかもしれません。今回もCDKでチャレンジしましたが型定義などからどうするべきか読み取れないし、GitHubのIssueは放置されているしでちょっと大変そうでした。

Step Functionsステートマシンの記述

Step Functionsのステートマシン(ワークフロー。手順を記述したJSONのこと)は以下のように書きます。

{
    "Comment": "A Hello World example of the Amazon States Language using Pass states",
    "StartAt": "Run Fargate Task",
    "States": {
      "Run Fargate Task": {
        "Type": "Task",
        "Resource": "arn:aws:states:::ecs:runTask.sync",
        "TimeoutSeconds": 300,
        "Parameters": {
          "LaunchType": "FARGATE",
          "Cluster": "CLUSTER ARN",
          "TaskDefinition": "TASK DEFINITION ARN",
          "NetworkConfiguration": {
            "AwsvpcConfiguration": {
              "Subnets": [
                "SUBNET ID"
              ],
              "SecurityGroups": [
                "SECURITY GROUP ID"
              ],
              "AssignPublicIp": "ENABLED"
            }
          },
          "Overrides": {
            "ContainerOverrides": [
              {
                "Name": "CONTAINER NAME",
                "Command.$": "$.commands"
              }
            ],
            "TaskRoleArn": "ROLE ARN"
          }
        },
        "Next": "Notify Success",
        "Catch": [
          {
            "ErrorEquals": [
              "States.ALL"
            ],
            "Next": "Notify Failure"
          }
        ]
      },
      "Notify Success": {
        "Type": "Pass",
        "Result": "Success",
        "End": true
      },
      "Notify Failure": {
        "Type": "Pass",
        "Result": "Failure",
        "End": true
      }
    }
  }

こちらのJSON設定はいくつかの前提がありますのでご了承ください。

  • タスク定義をFARGATEで記述していること
  • 外部からバッチ実行のためのコマンドを注入すること

JSONの文法としては、Statesというオブジェクトに状態を記述していきます。Run Fargate TaskというStateを見ると、Nextというキーに次に遷移する状態名としてNotify Successと書いています。また、エラー発生時はCatchというキーに書くように決まっており、そこではNotify Failureに遷移すると書きます。

するとAWSコンソール上では以下のような状態遷移図が自動で描画されます。ここが地味に凄いところです。これを自前で組んでしまうのか、という。

Image from Gyazo

余談ですがJavaScriptのxstateの考えに近い気がしますね。(https://github.com/davidkpiano/xstate)

以下の行が、外部から入力を受け取っている部分です。

                "Command.$": "$.commands"

作成したJSONはsfn.asl.jsonという命名で、statemachineというディレクトリ以下に置きます。ここの命名はなんでも良いです。

SAMのtemplate.yaml

SAM側の設定は以下のように記述します。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-step-functions-test
  Sample SAM Template for sam-step-functions-test

Parameters:
  StateMachineName:
    Description: Please type the Step Functions StateMachine Name.
    Type: String
    Default: 'sfn-sam-app-statemachine-for-test'
  
Resources:
  SampleStateMachine:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: !Sub ${StateMachineName}
      DefinitionUri: statemachine/sfn.asl.json
      Role: ROLE_ARN
      Events:
        SampleScheduleEvents:
          Type: Schedule
          Properties:
            Input: '{"commands": ["php", "artisan", "command:artisan-command"]}'
            Schedule: 'rate(3 hours)'
      Logging:
        Level: ALL
        IncludeExecutionData: True
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StateMachineLogGroup.Arn
  StateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName : !Join [ "", [ '/aws/states/', !Sub '${StateMachineName}', '-Logs' ] ]

DefinitionUriというキーに先程作成したJSONを書いています。

DefinitionUriに指定することでStep Functionsを実行できますが、今回の目的はバッチ処理の実行のため、Eventsキーにて3時間おきにコマンドを注入して実行することを指定します。
以下のようにcommandsキーを持ったJSONをInputに指定すると、前節で作成したStep Functionsのステートマシンにおける"$.commands"と対応しており引数を受け取って実行可能です。ここでは、同じタスク定義だが自由に実行時にコマンドを指定できるような運用を想定しています。

            Input: '{"commands": ["php", "artisan", "command:artisan-command"]}'

こちらはSAM側のドキュメント(https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-resource-statemachine.html )を読み込んでいくと何を指定したら良いか分かると思います。

コマンド

SAM定義をビルドする

sam build

デプロイする

sam deploy --guided

または

sam deploy --config-file samconfig.toml --config-env {任意の環境名}

落とし穴

  • よくRoleによる権限不足で失敗するため、エラー内容を見てRoleに適宜権限を付与する。Step Functionsは特に最初は失敗するため、現状のおすすめとしては最初コンソール上でJSONを書いて実行を繰り返し、うまく行ったJSONをSAMに持ってくること
  • CloudFormationあるあると思うのだが、スタックの更新途中に再度デプロイしたり、失敗時のロールバック中にデプロイすると失敗するなどのエラーをよく踏むので焦らない
  • SAM、Step Functions、個別サービスのAPIのドキュメントをそれぞれ見に行かないといけないのでハードルが少し高い。ECSのAPIドキュメントの内容を参考にするときは、最終的にはパスカルケースでYAMLなど記載するがドキュメント内ではキャメルケースなのでそこは少しだけ注意。falseなどもFalseって書いたりする
  • スケジュール式のcronもrateも少しだけ癖があり、?を曜日のところに書いたり、rate式はunitが単数形だったり複数形になる

参考文献

20190731 Black Belt Online Seminar Amazon ECS Deep Dive

https://www.slideshare.net/AmazonWebServicesJapan/20190731-black-belt-online-seminar-amazon-ecs-deep-dive-162160987/41

Step FunctionsにおけるTaskの記述

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/amazon-states-language-task-state.html

Step Functionsにおけるコンテナタスクの管理 (Amazon ECS、Amazon SNS)

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/sample-project-container-task-notification.html

SAM CLIインストール

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install-mac.html

SAMチュートリアル

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html

SAMでのステートマシン(Step Functions)の記述

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-resource-statemachine.html

ECSに対するAPIリファレンス

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/APIReference/API_RunTask.html#API_RunTask_RequestSyntax