はじめに
初投稿します。食パンです!
参画したプロジェクトの中でStep Functionsの実装で、
エラーハンドリングに困ったので他に困っている人がいるときに助けとなることと備忘録を兼ねて書きます。
今回試したエラーハンドリング
今回、Step Functions で試したエラーハンドリングは、
主に以下の2つのアプローチをしました。
- 1.組み込みのRetryとCatch
- ●よくあるリトライの実装(記事も出てくる)
- ●タスクの失敗時に自動的にリトライと分岐を行う組み込み機能
- 2.Choice ステート
- ●今回プロジェクトで利用したエラーハンドリングの方法
- ●レスポンスの内容に基づいてリトライを判断するカスタム実装
- ●状態の遷移とカウンターを使用して制御
具体的な実装を以下で解説します。
1. 組み込み Retry を使ったパターン
実装例
{
"Comment": "組み込みRetryパターン",
"StartAt": "処理実行",
"States": {
"処理実行": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "my-function",
"Payload.$": "$"
},
"ResultPath": "$.taskResult",
"Retry": [
{
"ErrorEquals": [
"Lambda.ServiceException",
"Lambda.AWSLambdaException",
"Lambda.SdkClientException",
"Lambda.TooManyRequestsException"
],
"IntervalSeconds": 1,
"MaxAttempts": 3,
"BackoffRate": 2,
"JitterStrategy": "FULL"
},
{
"ErrorEquals": ["States.TaskFailed"],
"IntervalSeconds": 5,
"MaxAttempts": 3,
"BackoffRate": 2
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error",
"Next": "エラー処理"
}
],
"Next": "成功処理"
},
"成功処理": {
"Type": "Pass",
"Parameters": {
"result": "処理に成功しました",
"id.$": "$.taskResult.Payload.id"
},
"End": true
},
"エラー処理": {
"Type": "Pass",
"Parameters": {
"error": "処理に失敗しました",
"error_details.$": "$.error"
},
"End": true
}
}
}
Retry
AWS StepFunctionにはタスクが失敗した場合に自動的にリトライする機能があります。
タスクの失敗時に再試行(リトライ)を行う際、以下のパラメータで細かく挙動を制御できます。
ErrorEquals
リトライ対象とするエラータイプを指定します。
例えば ["States.Timeout", "States.TaskFailed"] と設定すれば、それらのエラーが発生した場合にリトライを行います。
States.ALL を指定すれば、すべてのエラーがリトライ対象になります。
IntervalSeconds
リトライ間隔を秒単位で指定します。
たとえば 3 を設定すると、最初のリトライは3秒後に実行されます。
MaxAttempts
最大リトライ回数を指定します。
デフォルトは3回ですが、例えば 5 に設定すると最大5回までリトライされます。
無限ループを避けるため、適切な回数を設定することが重要です。
BackoffRate
リトライ間隔の増加率を指定します。
2.0 に設定した場合、リトライ間隔がリトライごとに倍々に伸びます。
例えば、IntervalSeconds: 3、BackoffRate: 2.0 なら、
1回目は3秒後 → 2回目は6秒後 → 3回目は12秒後… という形で間隔が長くなります。
これにより、一時的な負荷や障害からの回復を待つことができます。
Catch
リトライが最大回数に達した場合や、リトライ対象外のエラーが発生した場合は、
Catchを利用して代替フローに遷移できます。
ErrorEquals
捕捉するエラータイプを指定します。Lambdaで throw したエラー名もそのまま使用できる。
ResultPath
エラー情報の格納場所を指定します(例: $.error)。
Next
エラー発生時の遷移先ステートを指定します。
通常であればRetryとCatchを使えばエラーハンドリングができます。
今回のプロジェクトでは以下の実装でエラーハンドリングを行いました。
※正直catchで問題ないです。
2. Choice ステートを利用したエラーハンドリング
Lambdaの実行結果を受け取ってchoice stateを利用して、
statusCodeが200以外の時にリトライとリトライ回数が一定を超えたときにエラーとして
終了させる実装になっています。

{
"Comment": "Choiceによる手動リトライパターン",
"StartAt": "リトライカウンタ初期化",
"States": {
"リトライカウンタ初期化": {
"Type": "Pass",
"Result": {
"count": 0
},
"ResultPath": "$.retryCount",
"Next": "処理実行"
},
"処理実行": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "my-function",
"Payload.$": "$"
},
"ResultPath": "$.taskResult",
"Retry": [
{
"ErrorEquals": [
"Lambda.ServiceException",
"Lambda.AWSLambdaException",
"Lambda.SdkClientException",
"Lambda.TooManyRequestsException"
],
"IntervalSeconds": 1,
"MaxAttempts": 3,
"BackoffRate": 2,
"JitterStrategy": "FULL"
}
],
"Next": "ステータスコード確認"
},
"ステータスコード確認": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.taskResult.Payload.statusCode",
"NumericEquals": 200,
"Next": "成功処理"
}
],
"Default": "リトライカウンタ加算"
},
"リトライカウンタ加算": {
"Type": "Pass",
"Parameters": {
"count.$": "States.MathAdd($.retryCount.count, 1)",
"last_error.$": "$.taskResult.Payload.status",
"timestamp.$": "$$.State.EnteredTime"
},
"ResultPath": "$.retryCount",
"Next": "リトライ回数確認"
},
"リトライ回数確認": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.retryCount.count",
"NumericLessThan": 3,
"Next": "リトライ待機"
}
],
"Default": "エラー処理"
},
"リトライ待機": {
"Type": "Wait",
"Seconds": 5,
"Next": "処理実行"
},
"成功処理": {
"Type": "Pass",
"Parameters": {
"result": "処理に成功しました",
"id.$": "$.taskResult.Payload.id"
},
"End": true
},
"エラー処理": {
"Type": "Pass",
"Parameters": {
"error": "処理に失敗しました",
"last_attempt.$": "$.taskResult.Payload",
"retry_history.$": "$.retryCount"
},
"End": true
}
}
}
Choice ステートを選択した理由
理由①
設計上Lambda 関数が、エラーレスポンス型の実装を採用しており、
この実装では、Lambda 関数内で発生した例外はキャッチされ、HTTP ステータスコード 500 を含むレスポンスとして返されます。
Retry・Catch は成功扱いのエラーを扱えませんが、今回のような実装になっているとLambda 関数自体は正常終了するため、Step Functions の Retry が発動しない。
try:
# 処理
return {"statusCode": 200, "body": "success"}
except Exception as e:
# エラーをレスポンスとして返す
return {"statusCode": 500, "body": "failed"}
理由②
Payloadの中身を見て、フローの分岐を行う必要のあるステートマシンがあった。
今回のプロジェクトでは複数のステートマシンを作成しており、その中でCatchではできない分岐があったため、共通化を兼ねてすべてChoiceステートを利用してエラーハンドリングを実装した。
結論
Lambda 関数が例外をスローせず、エラーステータスコード(500)をレスポンスとして返す設計の場合、組み込み Retry では適切にエラーを検知してリトライすることができません。このような場合、Choice ステートを使用した手動リトライの実装が必要となります。
Lambda 関数の実装を変更できない制約がある場合は特に、Choice ステートによるリトライ実装が最適な選択となります。これにより、Lambda のレスポンス内容に基づいた柔軟なエラーハンドリングとリトライ戦略を実現できますが、基本的には RetryとCatchでいいと思います。
