こんにちは!
サーバーレスの導入相談が増えている昨今、Step Functions に関する開発相談が増えてきています。本記事では Step Functions について、概要からその用途や使い方、メリットやデメリットをまとめてご紹介します。
Step Functions は、Finite-State Machine (FSM) モデルを使用する AWS 管理下のサービスです。
Finite State Machines は、理論計算機科学のモデルを元にしたソフトウェアシステムのワークフローをモデル化(可視化)する方法です。ステップファンクションの実行内容には DynamoDB、Lambda 関数などの AWS サーバーレス製品を指定することが可能で、サーバーレスの製品を組み合わせることでプログラミングなしで機能(ユースケース)を解決することができます。
Step Functions を使えば、Amazon ECS や AWS Lambda、DynamoDB などの様々なサーバーレスサービスをオーケストレーション(機能豊富なアプリケーションにするワークフローを設計・実行)できるようになります。
また、任意のパラメーターや外部 API のリクエストパラメーターを Step Functions に設定することが可能なため、Step Functions は様々な AWS サービス・外部システムとシームレスに結合が可能です。そして何より、Step Functions 自体もサーバーレスであるため、運用コストを抑えることができます。
もちろん EventBridge との連携も可能なため、サードパーティと AWS をより高度に連携することが可能になります。
Step Functions では、与えられたタスクを実行する単位を「ステート」、ステートを束ねる単位をステートマシンと呼称します。このステートを複数つなぎ合わせたステートマシンを実行することで、ユースケースを解決することが可能となり、これをステートマシン・モデルといいます。
ここで覚えておきたいキーワードは「State」と「Transition」です。
ステートマシンモデルは、「状態」とその間の「遷移」と呼ばれる関係のみによって定義されます。
下の図1は、ステートマシンモデルにおける「状態」と「遷移」の関係を表現した例です。ドアが開いているか閉じているか、2つの状態があり、切り替えるためには何らかのインプットを行わなければなりません。ドアが空いている状態からドアを閉めるというインプットをして、閉じたドアを開けるまでの一連の動作を状態の切り替え(トランジション)と呼びます。
この考え方は日常生活でも例えられます。「仕事、家、寝る」を状態として捉えてみましょう。仕事(状態)からバス(入力)に乗って家に帰り(状態)、家に着いたら寝る(別の状態につながる別の入力)。明日の朝、目が覚めてベッドから出ると、前回いた状態から前の状態に遷移し、さらにまた、家から会社までバスで移動するのも、また1つの遷移です。
その他にも、状態、入力、それらの間の遷移が多数ある、より複雑な例もあり、状態を追加すればするほど、ステートマシンモデルはより複雑になります。
要するにステートマシンモデルとは、状態とその状態間の遷移を定義することにより、システムをモデル化する方法ということです。
Step Functions は前述の通り、ステートマシン(ワークフロー)とステート(タスク)で構成されています。ステートは、個々の状態や作業の一単位です。
下の図2の例では、長方形が状態を表しています。前回のステート実行結果によって次のステートへ処理がつながり、図2の例では最終的にはシステムの通知先をメールまたは SMS に設定しています。緑色のステートは正常に実行され、白色のステートは実行されないことを表しています。
このステートマシンを表すグラフ全体を「ワークフロー=ステートマシン」とも呼び、Step Functions には2種類のワークフローが用意されています。
ワークフローはスタンダードとエクスプレスの2つのグループに分けられます。エクスプレスワークフローは、昨年から提供されている比較的新しいオプションです。下の表は、この2つのワークフローの違いを表しています。
ワークフロー | スタンダード | エクスプレス |
---|---|---|
最長持続時間 | 1年 | 5分 |
実行率/毎秒 | 2,000+ | 100,000+ |
状態の遷移 | 4,000+ | 無限 |
価格 | 状態が遷移するごと | 実行回数、実行時間、メモリ |
実行回数 | 確実に1回 | 少なくとも1回 |
実行履歴 | API、AWSコンソールまたはCloudWatch | CloudWatch |
エクスプレスワークフローの価格は、実行時間や実行に使用されたメモリを含む実行回数に対してかかるため、より詳細に決定されています。標準的なワークフローの価格設定では、ユーザーは発生した各状態遷移に対してのみ支払う必要があります。
ここで覚えておきたいのは、スタンダードワークフローは、耐久性が高く審査可能である長時間稼働のワークフローであるということです。これに対し、エクスプレスワークフロータイプは、より頻度が高くイベント処理量が多い場合に適しています。
これらは、SQS の標準キュー、FIFO キューの関係とその点で似ています。Step Functions でファンアウトさせたいときにはエクスプレスワークフロータイプを使用しましょう。
ここまで Step Functions のワークフローの種類をご紹介してきました。次は実行の方法です。主な実行方法は以下となります。
ステートには数多くの種類があり、そのすべてがワークフロー全体の中で役割を担っています。Cloud Formation 等で Step Functions を作成するときはこれらを理解した上で作成する必要があります。
Pass | 入力を出力に切り替える |
Task | 入力を受けて出力を行う |
Choice | 入力に応じた分岐ロジックを使用できるようにする |
Wait | State Machine の実行に遅延が発生する |
Success | 実行を正常に停止させるデッドエンドを予測する |
Fail | 失敗と実行停止するデッドエンドを予測する |
Parallel | 実行時の並列分岐を実現し、一度に複数の状態を開始できるようにする |
Mapping | 入力項目ごとに一連のステップを実行 |
以下は Serverless Framework で Choice を含む Step Functions を作成する例です。
functions:
hello1:
handler: handler.hello1
hello2:
handler: handler.hello2
hello3:
handler: handler.hello3
hello4:
handler: handler.hello4
stepFunctions:
stateMachines:
yourChoiceMachine:
definition:
StartAt: FirstState
States:
FirstState:
Type: Task
Resource:
Fn::GetAtt: [hello, Arn]
Next: ChoiceState
ChoiceState:
Type: Choice
Choices:
- Variable: "$.foo"
NumericEquals: 1
Next: FirstMatchState
- Variable: "$.foo"
NumericEquals: 2
Next: SecondMatchState
Default: DefaultState
FirstMatchState:
Type: Task
Resource:
Fn::GetAtt: [hello2, Arn]
Next: NextState
SecondMatchState:
Type: Task
Resource:
Fn::GetAtt: [hello3, Arn]
Next: NextState
DefaultState:
Type: Fail
Cause: "No Matches!"
NextState:
Type: Task
Resource:
Fn::GetAtt: [hello4, Arn]
End: true
タスクは一連の動作を実行する上で中心となる State Type です。タスクは アクティビティを呼び出すことができます。
また、Step Functions Tasks が持つ別の構成要素により、AWS側から働きかけることを可能とします。
エラー処理には、リトライとキャッチがあります。図3 はステップファンクションがどのようにエラーハンドリングを行うのかを示した例です。
例では、並列に分かれたタスクがあります。このタスクにおいて、1つの状態がエラーに遭遇した場合、実行全体が失敗することを意味しています。
Step Functions を作成する際、開発者は Step Functions サービスが提供する Amazon State Language を使用し JSON 構文で Step Functions を作成することが可能です。
Amazon State Language では、コメントを配置し、いつ状態を開始するかといった状態やタスクに対する定義をすることができます。エラー名から再試行を指定するだけでなく、再試行間隔、再試行の最大試行回数、バックオフ率を指定することができます。
{
"Comment": "A Hello World example",
"StartAt": "HelloWorld",
"States": {
"HelloWorld": {
"Type": "Task"
"Resource": "arn:aws:Lambda:...",
"Retry": [
{
"ErrorEquals": ["HandledError"],
"IntervalSeconds": 1,
"MaxAttempts": 2,
"BackoffRate": 2.0
}
],
"End": true
}
}
}
エラーをキャッチしたい場合は、ある状態がなぜ実行されなかったのか、どのタスクが失敗したのかを確認することができます。エラーをキャッチする方法は、以下の例をご覧ください。
{
"Comment": "A Hello World example",
"StartAt": "HelloWorld",
"States": {
"HelloWorld": {
"Type": "Task",
"Resource": "arn:aws:Lambda:...",
"Catch": [
{
"ErrorEquals": ["States. TaskFailed"],
"Next": "fallback",
"End": true
}
],
"fallback": {
"Type": "Pass",
"Result": "Hello, AWS Step Functions!",
"End": true
}
}
}
}
最初の再試行は、あらかじめ設定した間隔で開始され、設定したバックオフ率で乗算されます。
また、正しくエラー処理を記述することで、詳細なエラー内容をログに記録することが可能となります。エラーが発生した理由をログから解析し、問題の原因を調査することが可能となります。
続いて、Step Functions の例をいくつか見てみましょう。これらは AWS CDK を利用して構築されています。
ここではまず、関数に優先番号を入力する必要があります。例えば、10という数字を選び、顧客が10個以上の商品を購入した場合 Step Functions は好ましい選択肢に従って正常に実行されます。また、顧客が10個未満の商品を購入した場合、実行は成功しますが、事前設定された別の選択肢の下で実行されます。
const success = new stepFunsfnctions.Succeed(this, "Success!");
const moreTask = new stepFunsfnctions.Pass(this, "MORE");
moreTask.next(success);
const lessTask = new stepFunsfnctions.Pass(this, "LESS");
lessTask.next(success);
const desiredAmountChoice = new stepFunsfnctions.Choice(
this,
"More than desired amount?"
);
desiredAmountChoice.when(
stepFunsfnctions.Condition.numberGreaterThanJsonPath(
"$.itemAmount",
"$.desiredAmount"
),
moreTask
);
desiredAmountChoice.when(
stepFunsfnctions.Condition.numberLessThanEqualsJsonPath(
"$.itemAmount",
"$.desiredAmount"
),
lessTask
);
new stepFunsfnctions.StateMachine(this, "StateMachine", {
definition: desiredAmountChoice,
});
desired Amount Choice の状態において、item Amount と different Amount の入力を比較し、それに応じて分岐します。この入力は、ステートマシンの新しい実行が作成されるときに供給されます。
そして、desired Amount Choice はmoreTask と lessTask という2つの異なる状態を導きます。この例においてはどちらも単なるパス型のステートですが、Lambda Functions を実行するタスク型のステートに切り替えることもできます。
ステートマシーンが以下のインプットをどのように処理するか確認することできます。
{
"itemAmount": 23,
"desiredAmount": 10
}
Lambda 関数 がエラーを投げると、その関数が所属するタスクは失敗します。次の例では、存在しないイベント属性にアクセスしようとしています。この方法では、Lambda 関数 は常にクラッシュし、何度か挑戦した後、エラー処理の代替物としてパス型の状態に逆戻りします。
Step Functions において、エラーをキャッチし処理することは、関数の実行を成功させ、エラーをなくすために不可欠です。リトライとキャッチの例に対するコードは、以下のようになります。
const brokenTask = new stepFunsfnctionsTasks.LambdaInvoke(this, "BrokenTask", {
lambdaFunction: new lambda.Function(this, "BrokenFunction", {
runtime: lambda.Runtime.NODEJS_12_X,
handler: "index.handler",
code: new lambda.InlineCode(`
exports.handler = async (event) => {
const error = event.x.y;
return {Payload: "result text"};
}
`),
}),
outputPath: "$.Payload",
});
brokenTask.addRetry({ maxAttempts: 5 });
const handleFail = new stepFunsfnctions.Pass(this, "HandleFail");
const success = new stepFunsfnctions.Succeed(this, "Success!");
handleFail.next(success);
brokenTask.addCatch(handleFail);
brokenTask.next(success);
new stepFunsfnctions.StateMachine(this, "StateMachine", {
definition: brokenTask,
});
broken Task は broken Function の起動を試みますが、成功することはありません。max Attempts の5回だけ再試行し、add Catch で追加した handle Fail の状態を実行します。
下の図4では、ステートマシンが broken Task の実行を試みているのが分かります。また、再試行のためにデフォルトの backoff Rate が2の乗算器であるため、失敗したステップの間隔が長くなっていることが確認できます。
下の図5では、最終的にステートマシーンがどのように機能するのか確認できます。
Step Functions のスタンダードワークフローは、ビジネスにおけるワークフローに最適で、多くの利点があります。
Step Functions はLambda 関数 よりもはるかに優れたエラー処理ロジックを提供しており、エラーハンドリングを機能へ統合するのも比較的容易です。
一方、エクスプレスワークフローと比較するとかなり高価なため、ビジネスにおいて、よりクリティカルなワークフロー向けと言えます。スタンダードワークフローの価格は、100万回実行あたり25ドルで、メモリと使用期間のコストが追加されます。AWS Step Functions のコスト削減についてもっと知りたい方は、エンタープライズスケールのワークフローで Step Functionsのコストを削減する方法についての記事をご覧ください。
また、Step Functions は、長期間稼働するワークフローや遅延しているワークフローにも有効です。待機状態も実装しながら、最長で1年間のワークフローを持つことができます。(一年後まで処理を待機することが可能)
Step Functions では、S3に設定したファイルをペイロードに指定することができます。
そのため、Step Functions の最もよい使い方の1つは、ペイロードサイズが大きい処理の実行です。(SQSでいうところ、メッセージサイズが大きい的な意味)
S3 にファイルを設置すれば、Step Functions 実行に直接指定しインポートが可能です。下記のコード例のように、S3 の場所を「arn」で指定することで簡単に行えます。
{
"StartAt": "Invoke Lambda function",
"States": {
"Invoke Lambda function": {
"Type": "Task",
"Resource": "arn:aws: states:::lambda:invoke",
"Parameters": {
"FunctionName": "arn:aws:Lambda:...",
"Payload": {
"Data": "arn:aws:53:::MyBucket/data.json"
}
},
"End": true
}
}
}
以下の例のように、Lambda 例外エラーを評価したエラーハンドリングが可能です。例外のインスタンスをカスタムしている場合など大変有効ですね。
"Retry": [
{
"ErrorEquals": [
"Lambda.ServiceException",
"Lambda.AWSLambdaException",
"Lambda.SdkClientException"
],
"IntervalSeconds": 2,
"MaxAttempts": 6,
"BackoffRate": 2
}
]
エラーによって、回復を試みる、管理者へ通知するなどの条件分岐も可能となります。
Step Functions の統合に使える AWS サービスは十数種類用意されており、マネージドで簡単に統合することができます。
AWS CDK には Step Functions モジュールがあり、CDK スタックで直接ワークフローを定義することができ、静的な型チェックも含め実現できます。
更に、Step Functions を .jar ファイルや Docker イメージとしてダウンロードし、自分のローカル環境で実行することも可能です。
また、Step Functions のパフォーマンスを常に把握することも欠かせません。そこで、Dashbird のようなサーバーレス監視ツールが活躍します。Step Functions は Cloud Trail と Cloud Watch にイベントとメトリクスを公開し、Dashbird はそれを監視することができます。
Dashbird のインサイトエンジンは、ステートマシンの定義やタスク実行の失敗に関連するエラーをリアルタイムに検出します。
ワークフロー内の何かが壊れたり、上手くいかなくなりそうになると、Slack やメールで即座に通知します。インサイトエンジンは、AWS Well-Architected のベストプラクティスに基づいており、サーバーレスインフラ全体のデータを常にルールに従って実行することで、どのような規模でもアプリを最適化し、信頼性を確保できるようにしてくれます。
Step Functions について、基本的な情報から具体的な使い方まで整理してみました。
エクスプレスワークフローは標準ワークフローよりはるかに安価ですが、Cloud Watch のログに情報をプッシュするため、実行を監視する視覚的な支援はありません。また、非常に優れた洞察を提供してくれますが、視覚的な補助がないため、特に手元にある実行回数が多すぎる場合は、難しく感じることもあるでしょう。何が失敗で何が失敗でないかを復元するのは、難しいタスクに思えるかもしれません。
Step Functions は AWS の比較的新しい製品で、アプリケーションを AWS サービスで疎結合に構成することができるため、間違いなく開発効率が上がるでしょう。
Step Functions をはじめ AWS に関する開発相談はお気軽にお問合せください。
スモールスタート開発支援、サーバーレス・NoSQLのことなら
ラーゲイトまでご相談ください
低コスト、サーバーレスの
モダナイズ開発をご検討なら
下請け対応可能
Sler企業様からの依頼も歓迎