PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

AWS Step Functions を使用したワークフローのレイヤー分離と外部システム連携

こんにちは、メディアプラットフォーム事業部の尾古です。

最近 AWS が提供するアプリケーション統合サービスの1種である AWS Step Functions を利用する機会がありました。その際、処理の流れを機能単位にまとめて使い回し可能な形にしたい、自前の既存システムや外部のシステムをワークフローの一部に組み込みたい、といった課題が挙げられました。これらの課題は AWS Step Functions の使い方を工夫することで解決可能となっているのですが、個人的にその使い方が面白いと感じたので、今回はタイトルに掲げたワークフローのレイヤー分離外部システム連携の2つの視点からお話を進めていきます。

AWS Step Functions とは

本記事では簡単な説明に留めますが、冒頭で記載した通り AWS アプリケーション統合サービス内でのカテゴリ「ワークフロー」に分類されているサービスのことであり、公式サイトでは以下のように紹介されています。

Coordinate multiple AWS services into serverless workflows so you can build and update apps quickly

そのままの訳ではありますが、複数の AWS サービスをサーバーレスのワークフローに整理することでアプリケーションの構築と更新を迅速に行うことができます。つまり、数多く提供されている AWS のサービスを組み合わせたワークフローを、我々ユーザが手軽に設計および実行することが可能なサービスとなっています。

AWS Step Functions ではワークフローのことを「ステートマシン」と呼称しており、ワークフローの各ステップを「ステート(状態)」として扱います。このステートにはいくつかの種別が存在し、その内の「タスク(Task)」が AWS のサービスを実行する1つの作業単位となっています。そのためタスクステートに組み合わせたい AWS のサービスを指定し、その各ステートを連ねることで1つのワークフローを設計していきます。

自由度の高いワークフローを実現可能

AWS のサービスの組み合わせによっては数多の極めて自由度の高いワークフローを設計できる AWS Step Functions ですが、組み合わせ可能なサービスの一部を抜粋すると AWS LambdaAmazon SNSAmazon Elastic Container Service (Amazon ECS) などが存在します。その内の AWS Lambda を利用すれば独自の処理を組み込むことができ、より複雑なワークフローを実現可能となっています。以降では AWS Step Functions で AWS Lambda を利用したパターンのワークフローに着目していきます。

ワークフローのレイヤー分離

独自の処理が記載された AWS Lambda を組み込めば複雑なワークフローを設計できる一方で、そのワークフローは独自処理に依存したタスク固有なものとなってしまいます。単体での使用であれば何ら問題ないと思われますが、他のワークフロー内でも使用したい場合だと同じ内容を再度組み込む必要があります。これは設計の観点から見ると無駄であり、保守・運用の観点では煩雑さを生む要因となり得ます。であればタスク固有な独自処理部分は一つの再利用可能なワークフローとして管理し、他ワークフロー内で使用することが最適であると考えられます。すなわち、タスク固有の下位レイヤーのワークフローと、それを管理する上位レイヤーのワークフローの分離を行います。

ワークフロー内でワークフローを呼び出す

当たり前ですが、ワークフローのレイヤーを分離するに当たって、上位ワークフローが下位ワークフローを呼び出す、つまり AWS Step Functions に AWS Step Functions を組み込む必要があります。AWS Step Functions はその点の自由度も高く、自身との組み合わせ、すなわち親子関係のワークフローも可能となっています。 docs.aws.amazon.com docs.aws.amazon.com

親子関係にあるワークフローの Amazon States Language

ステートマシンが親子関係にある場合における親ステートマシンの基本的な Amazon States Language*1(以降、ASL)は以下のようになります。なお、子ステートマシンは従来通りのため割愛します。

{
  "Comment": "A description of my state machine",
  "StartAt": "Step Functions StartExecution",
  "States": {
    "Step Functions StartExecution": {
      "Type": "Task",
      "Resource": "arn:aws:states:::states:startExecution",
      "Parameters": {
        "StateMachineArn": "arn:aws:states:REGION:ACCOUNT_ID:stateMachine:STATE_MACHINE_NAME",
        "Input": {
          "StatePayload": "Hello from Step Functions!",
          "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id"
        }
      },
      "End": true
    }
  }
}

注目したいのは次の3つのフィールドです。

  • Resource
  • StateMachineArn
  • AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$

1つ目の Resource は実行するタスクを一意に特定するURIを指定するフィールドで、ステートマシンを実行するARN(Step Functions API における StartExecution の呼び出し)を指定しています。 2つ目の StateMachineArn は実行対象となるステートマシンのARNを指定するフィールドで、呼び出したい子ステートマシンのARNを指定します。 3つ目の AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$ は子ステートマシンの実行結果を親ステートマシンに紐付けるフィールドで、親ステートマシンのコンテキストオブジェクトから親の実行IDを渡しています。必須のフィールドではありませんが、AWS コンソールにて親の実行結果に子の実行結果へのリンクが貼られるため運用時の確認が容易に行えます。

親子関係にあるワークフローの IAM 権限

注意すべき点として、親ステートマシンの IAM 権限に子ステートマシンの StartExecution の呼び出しと、親ステートマシンに対して実行の停止がリクエストされた際に、子ステートマシンの実行も停止する必要があるため StopExecution の呼び出しを許可する必要があります。加えて、子ステートマシンの実行が完了したか否かを親ステートマシンはポーリングとイベントによって判断するため、以下4つの許可も必要です。

  • states:DescribeExecution
  • event:PutTargets
  • event:PutRule
  • event:DescribeRule

1つ目の states:DescribeExecution は子ステートマシンの実行詳細をポーリングするための権限であり、それ以外の event:PutTargetsevent:PutRuleevent:DescribeRuleAmazon EventBridge を介して AWS Step Functions に送られるイベントに必要な権限となります。

まとめると、親ステートマシンの基本的な IAM 権限は次の通りです。

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "states:StartExecution"
    ],
    "Resource": [
      "arn:aws:states:REGION:ACCOUNT_ID:stateMachine:STATE_MACHINE_NAME"
    ]
  }, {
      "Effect": "Allow",
      "Action": [
        "states:StopExecution",
        "states:DescribeExecution"
      ],
      "Resource": [
        "arn:aws:states:REGION:ACCOUNT_ID:execution:STATE_MACHINE_NAME:*"
      ]
  }, {
      "Effect": "Allow",
      "Action": [
        "events:PutTargets",
        "events:PutRule",
        "events:DescribeRule"
      ],
      "Resource": [
        "arn:aws:events:REGION:ACCOUNT_ID:rule/RULE_NAME"
      ]
  }]
}

docs.aws.amazon.com

外部システム連携

前節で触れた下位レイヤーのワークフロー、すなわちタスク固有の独自処理を行うワークフローには、その内部処理で完結するものだけでなく外部システムとの連携が要求されるものも多く存在すると思います。この場合において外部システムのコールバックを受けた上で次のステートへと遷移させるためには、 AWS Step Functions のサービス統合パターンの「タスクトークンを用いたコールバックの待機」を使用する必要があります。

このコールバックを待機するタスクステートでは、SendTaskSuccess もしくは SendTaskFailure の呼び出しによってステートマシンが生成するタスクトークンを受け取るまで、そのステートを一時停止させます。そのため、外部システムのコールバックを受けた際にタスクトークンを該当タスクステートに送信するアーキテクチャを設計すれば実現可能となります。

アーキテクチャ設計

一例を下図に示します。

図中の上部にある AWS Lambda に対してステートマシンが生成するタスクトークンを渡します。 受け取った AWS Lambda はタスクトークンと、そのインデックスとなるタスク ID を例えば Amazon DynamoDB に保存し、そのタスク ID を外部システムへのペイロードに含めてリクエストを行います*2。 外部システムがリクエストを処理している間、タスクステートは一時停止しコールバックを待機します。 そして外部システムの処理完了後、タスク ID と処理の出力をパラメータとして Amazon API Gateway に対してコールバックしてもらい、図中の下部にある AWS Lambda にてタスク ID を基にタスクトークンを取得し、コールバックを待機しているタスクステートに対してタスクトークンの送信を行います。 タスクトークンを受け取ったタスクステートは処理が完了となり次のステートへと遷移します。

タスクトークンを用いたコールバックを待機するワークフローの ASL

コールバックを待機するタスクステートを持つステートマシンの基本的な ASL は以下のようになります。なお、実行するタスクとしてステートマシンの実行を指定していますが、こちらは関係ありません。

{
  "Comment": "A description of my state machine",
  "StartAt": "Step Functions StartExecution WaitForTaskToken",
  "States": {
    "Step Functions StartExecution WaitForTaskToken": {
      "Type": "Task",
      "Resource": "arn:aws:states:::states:startExecution.waitForTaskToken",
      "Parameters": {
        "StateMachineArn": "arn:aws:states:REGION:ACCOUNT_ID:stateMachine:STATE_MACHINE_NAME",
        "Input": {
          "StatePayload": "Hello from Step Functions!",
          "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id",
          "TaskToken.$": "$$.Task.Token"
        }
      },
      "End": true
    }
  }
}

注目したいのは次の2つのフィールドです。

  • Resource
  • TaskToken.$

1つ目の Resource では末尾に追加されている .waitForTaskToken が重要であり、これがこのタスクステートでコールバックを待機することを意味しています。 2つ目の TaskToken.$ はステートマシンが生成するタスクトークンをタスクステートへ渡すためのフィールドで、ステートマシンのコンテキストオブジェクトからトークンを渡しています。この値がなければタスクステートは処理を完了することができないため重要なフィールドとなっています。

弊社での実用例

最後に前節までで触れてきた内容を基にした弊社での実用例を簡単にご紹介させていただきます。

弊社は動画配信技術を基にソリューションを提供しており、常日頃から動画ファイルを取り扱っています。私がアサインされているプロダクトにおいても例に漏れず、動画ファイルに対して、映像・音声の詳細情報を得るためのアナライズ処理や、エンドユーザへ提供可能なファイルを作成するための動画編集処理およびエンコード処理を行なっています。これらの処理を一連のワークフローとし、お客様からの入稿をトリガーに処理の自動化を図っています。

そのために各処理を機能単位での再利用可能な下位レイヤーのワークフローとして管理し、これらを上位レイヤーのワークフローにて制御しています。先ほど列挙したもので言えば、アナライズ処理、(編集ルールに基づいた)動画編集処理、エンコード処理を順に実行していきます。 また一例としてエンコード処理を挙げますが、こういった処理は外部システム(例えば AWS Elemental MediaConvert など)と連携して行うパターンもあるため、コールバックを受けるまでステートを待機させるようにし、エンコード処理が完了するまで次のステートに遷移しないように制御しています。

最後に

本記事では AWS Step Functions をテーマにワークフローのレイヤー分離外部システム連携の2つの視点から基本的な ASL やアーキテクチャ設計をお話しさせていただきました。ワークフローのレイヤー分離で記載した ASL は親ステートマシンが1つの子ステートマシンを実行するという非常に単純なワークフローを例に挙げました。単純ではあるものの、この基本構造を押さえておけば上位レイヤーにおいて「並列(Parallel)」や「選択(Choice)」といったステートを用いて下位レイヤーをより柔軟に制御することができ、様々なユースケースに応じたワークフローの設計が可能だと考えています。

*1:AWS Step Functions のステートマシンにおける各ステートの連なりを定義する JSON ベースの構造化言語

*2:SendTaskSuccess および SendTaskFailure はタスクトークンがあれば呼び出し可能となっているため、そのままの値ではなくタスク ID に置き換えて外部システムへリクエストを行うように設計し、(AWS 認証情報がないと実行できないが念のため)悪用される可能性をなるべく排除する対策を図っています