これは、なにをしたくて書いたもの?
AWS Step Functionsのワークフローを見ていて、エラーハンドリングについて試しておいた方がいいかなと思いまして。
Step Functions ワークフローでのエラーの処理 - AWS Step Functions
AWS Step Functionsのエラーハンドリング
内容としては、ドキュメントのこちらのページのものです。
Step Functions ワークフローでのエラーの処理 - AWS Step Functions
ワークフローでは、Pass
ステートとWait
ステートを除くすべてのステートでランタイムエラーが発生する可能性があるとされています。
たとえば
- ステートマシンの定義が誤っている(
Choice
ステートに一致するルールがない) - AWS Lambda関数呼び出しのタスクが失敗する
- その他、ネットワークなどの一時的な問題が発生した
など。
デフォルトでは、ステートがエラーになるとワークフロー全体が失敗します。
エラーはAmazon States Language(ASL)で識別でき、以下の種類のものがあるようです。
- States.ALL … エラーに対するワイルドカード
- States.DataLimitExceeded … クォータの超過
- States.ExceedToleratedFailureThreshold …
Map
ステートで失敗したアイテム数がしきい値を超えた - States.HeartbeatTimeout …
Task
ステートに対してHeartbeatSeconds
よりも長い間隔のハートビートの送信に失敗 - States.Http.Socket … HTTPタスクのクォータ超過(60秒に1回)
- States.IntrinsicFailure … ペイロードテンプレート内での組み込み関数呼び出しの失敗
- States.ItemReaderFailed …
ItemReader
に指定されたアイテムを読み取れなかったため、Map
ステートが失敗 - States.NoChoiceMatched …
Choice
ステートに選択ルールで定義された条件と入力が一致しなかった場合で、デフォルトの遷移も設定されていない場合 - States.ParameterPathFailure … ステートの
Parameters
フィールドで名前の末尾がパスを使用する.$
になっている場合 - States.Permissions … 指定されたコードを実行する権限が足りない場合
- States.ResultPathMatchFailure … ステートの入力に、ステートの
ResultPath
を適用できなかった - States.ResultWriterFailed …
ResultWriter
フィールドの指定先に値を書き込めず、Map
ステートが失敗した - States.Runtime … 処理できない例外のため、実行に失敗した。States.ALLではキャッチできない
- States.TaskFailed …
Task
ステートが失敗した。Retry
やCatch
で指定すると、States.Timeout以外に対するワイルドカードとして機能する - States.Timeout …
Task
ステートがTimeoutSeconds
よりも長時間実行されたか、HeartbeatSeconds
値よりも長い間隔のハートビートの送信に失敗した場合
ステートは他の名前でエラーを報告することもあるようですが、States.
プレフィックスで始まる名前にすることはできません。
例としては以下があります。たとえば、Lambda.ClientExecutionTimeoutException
など。
Step Functions のベストプラクティス / 一時的な Lambda サービスの例外を処理する
エラーが発生した時には、リトライまたはフォールバックを行うことができます。リトライはRetry
、フォールバックはCatch
で
表現されます。
Retry
もCatch
も、配列で複数定義することができます。
Retry
では、リトライ対象とするエラー名、リトライ間隔やリトライ回数などを指定します。
Catch
では、ハンドリング対象とするエラー名と遷移先のステートを指定します。次のステートに対して出力をResultPath
で指定することも
できます。
Catch
対象となった場合はCause
というフィールドが含まれ、これをエラー出力と呼ぶそうです。遷移先のステートでは、たとえば
Parameters
フィールドで$.Cause
という指定でこの内容にアクセスできます。
Retry
やCatch
の例については、このページ内の各種サンプルを見るとよいでしょう。
Step Functions ワークフローでのエラーの処理 - AWS Step Functions
で終わってもなんなので、実際に自分でも動かしてみます。常に失敗するAWS Lambda関数に対してリトライを行い、最終的にCatch
で
他のステートに遷移させてみたいと思います。
環境
今回の環境はこちら。
$ python3 --version Python 3.10.12 $ pip3 --version pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10) $ awslocal --version aws-cli/2.21.3 Python/3.12.6 Linux/5.15.0-125-generic exe/x86_64.ubuntu.22 $ samlocal --version SAM CLI, version 1.129.0 $ localstack --version 3.8.1
LocalStackを起動。
$ localstack start
AWS SAMプロジェクトを作成。
$ samlocal init --name sfn-localstack-handle-error --runtime python3.10 --app-template hello-world --package-type Zip --no-tracing --no-application-insights --structured-logging $ cd sfn-localstack-handle-error
使わないファイルは削除。
$ rm -rf events hello_world tests
AWS Lambda関数は2種類作成しました。
ひとつ目は、最初に呼び出されて常に例外をスローするAWS Lambda関数。
first/app.py
def lambda_handler(event, context): print(f"first function input = {event}") raise RuntimeError("Oops!!")
2つ目は、そのまま終了するAWS Lambda関数。
catch/app.py
def lambda_handler(event, context): print(f"catch function input = {event}") return { "result": "catched!" }
どちらも入力データを標準出力に書き出しておきます。
ステートマシンの定義。
statemachine/handle-error-state-machine.asl.json
{ "Comment": "My Handle Error State Machine", "StartAt": "FirstTask", "States": { "FirstTask": { "Type": "Task", "Resource": "${FirstFunctionArn}", "Parameters": { "ExecutionId.$": "$$.Execution.Id" }, "Retry": [ { "ErrorEquals": ["States.ALL"], "IntervalSeconds": 1, "MaxAttempts": 2, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "Next": "CatchTask" } ], "End": true }, "CatchTask": { "Type": "Task", "Resource": "${CatchFunctionArn}", "End": true } } }
最初のステートはこちら。
"FirstTask": { "Type": "Task", "Resource": "${FirstFunctionArn}", "Parameters": { "ExecutionId.$": "$$.Execution.Id" }, "Retry": [ { "ErrorEquals": ["States.ALL"], "IntervalSeconds": 1, "MaxAttempts": 2, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "Next": "CatchTask" } ], "End": true },
なにも起こらなければそのまま終了するのですが、エラーが発生した場合は2回リトライ(Retry
)します(MaxAttempts
)。
最初のリトライまでは1秒(IntervalSeconds
)で、次のリトライまではIntervalSeconds
を2倍した(BackoffRate
)時間待機します。
つまり2秒後というわけですね。
それでもダメだった場合はCatch
して次のステートへ遷移します。
AWS SAMテンプレート。
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > sfn-localstack-handle-error Sample SAM Template for sfn-localstack-handle-error Globals: Function: Timeout: 3 LoggingConfig: LogFormat: JSON Resources: FirstFunction: Type: AWS::Serverless::Function Properties: FunctionName: first-function CodeUri: first/ Handler: app.lambda_handler Runtime: python3.10 Architectures: - x86_64 CatchFunction: Type: AWS::Serverless::Function Properties: FunctionName: catch-function CodeUri: catch/ Handler: app.lambda_handler Runtime: python3.10 Architectures: - x86_64 HandleErrorStateMachine: Type: AWS::Serverless::StateMachine Properties: Name: handle-error-state-machine DefinitionUri: statemachine/handle-error-state-machine.asl.json DefinitionSubstitutions: FirstFunctionArn: !GetAtt FirstFunction.Arn CatchFunctionArn: !GetAtt CatchFunction.Arn Outputs: FirstFunction: Description: First Lambda Function ARN Value: !GetAtt FirstFunction.Arn FirstFunctionIamRole: Description: Implicit IAM Role created for First function Value: !GetAtt FirstFunctionRole.Arn CatchFunction: Description: Catch Lambda Function ARN Value: !GetAtt CatchFunction.Arn CatchFunctionIamRole: Description: Implicit IAM Role created for Catch function Value: !GetAtt CatchFunctionRole.Arn HandleErrorStateMachineArn: Description: "Handle Error State machine ARN" Value: !Ref HandleErrorStateMachine HandleErrorStateMachineRoleArn: Description: "IAM Role created for Handle Error State machine based on the specified SAM Policy Templates" Value: !GetAtt HandleErrorStateMachineRole.Arn
デプロイしましょう。
$ samlocal deploy --region us-east-1
完了。
CloudFormation events from stack operations (refresh every 5.0 seconds) ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ResourceStatus ResourceType LogicalResourceId ResourceStatusReason ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- CREATE_IN_PROGRESS AWS::CloudFormation::Stack sfn-localstack-handle-error - CREATE_IN_PROGRESS AWS::IAM::Role FirstFunctionRole - CREATE_COMPLETE AWS::IAM::Role FirstFunctionRole - CREATE_IN_PROGRESS AWS::IAM::Role CatchFunctionRole - CREATE_COMPLETE AWS::IAM::Role CatchFunctionRole - CREATE_IN_PROGRESS AWS::IAM::Role HandleErrorStateMachineRole - CREATE_COMPLETE AWS::IAM::Role HandleErrorStateMachineRole - CREATE_IN_PROGRESS AWS::Lambda::Function FirstFunction - CREATE_COMPLETE AWS::Lambda::Function FirstFunction - CREATE_IN_PROGRESS AWS::Lambda::Function CatchFunction - CREATE_COMPLETE AWS::Lambda::Function CatchFunction - CREATE_IN_PROGRESS AWS::StepFunctions::StateMachine HandleErrorStateMachine - CREATE_COMPLETE AWS::StepFunctions::StateMachine HandleErrorStateMachine - CREATE_COMPLETE AWS::CloudFormation::Stack sfn-localstack-handle-error - ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- CloudFormation outputs from deployed stack -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Outputs -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Key FirstFunction Description First Lambda Function ARN Value arn:aws:lambda:us-east-1:000000000000:function:first-function Key FirstFunctionIamRole Description Implicit IAM Role created for First function Value arn:aws:iam::000000000000:role/sfn-localstack-handle-error-FirstFunctionRole-eb2b1157 Key CatchFunction Description Catch Lambda Function ARN Value arn:aws:lambda:us-east-1:000000000000:function:catch-function Key CatchFunctionIamRole Description Implicit IAM Role created for Catch function Value arn:aws:iam::000000000000:role/sfn-localstack-handle-error-CatchFunctionRole-03f7be79 Key HandleErrorStateMachineArn Description Handle Error State machine ARN Value arn:aws:states:us-east-1:000000000000:stateMachine:handle-error-state-machine Key HandleErrorStateMachineRoleArn Description IAM Role created for Handle Error State machine based on the specified SAM Policy Templates Value arn:aws:iam::000000000000:role/sfn-localstack-handle-error-HandleErrorStateMachineR-caa73133 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Successfully created/updated stack - sfn-localstack-handle-error in us-east-1
動かしてみます。
$ awslocal stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:000000000000:stateMachine:handle-error-state-machine --name run { "executionArn": "arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run", "startDate": "2024-11-16T15:18:58.454010+09:00" }
結果の確認。
$ awslocal stepfunctions describe-execution --execution-arn arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run { "executionArn": "arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run", "stateMachineArn": "arn:aws:states:us-east-1:000000000000:stateMachine:handle-error-state-machine", "name": "run", "status": "SUCCEEDED", "startDate": "2024-11-16T15:18:58.454010+09:00", "stopDate": "2024-11-16T15:19:08.121066+09:00", "input": "{}", "inputDetails": { "included": true }, "output": "{\"result\":\"catched!\"}", "outputDetails": { "included": true } }
output
は2つ目の関数の結果になっていますね。
AWS Lambda関数のログを見てみましょう。
最初のAWS Lambda関数のログ。
$ awslocal logs tail /aws/lambda/first-function 2024-11-16T06:19:03.218000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba START RequestId: f7584963-af3f-4ea6-bb9c-f19856130b43 Version: $LATEST 2024-11-16T06:19:03.244000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba first function input = {'ExecutionId': 'arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run'} 2024-11-16T06:19:03.271000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba LAMBDA_WARNING: Unhandled exception. The most likely cause is an issue in the function code. However, in rare cases, a Lambda runtime update can cause unexpected function behavior. For functions using managed runtimes, runtime updates can be triggered by a function change, or can be applied automatically. To determine if the runtime has been updated, check the runtime version in the INIT_START log entry. If this error correlates with a change in the runtime version, you may be able raise RuntimeError("Oops!!")4, in lambda_handlero the previous runtime version. For more information, see https://docs.aws.amazon.com/lambda/latest/dg/runtimes-update.html 2024-11-16T06:19:03.297000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba END RequestId: f7584963-af3f-4ea6-bb9c-f19856130b43 2024-11-16T06:19:03.324000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba REPORT RequestId: f7584963-af3f-4ea6-bb9c-f19856130b43 Duration: 29.45 ms Billed Duration: 30 msMemory Size: 128 MB Max Memory Used: 128 MB 2024-11-16T06:19:04.272000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba START RequestId: 0d8378da-e74e-41dc-a4a3-95d2b9d5eb8f Version: $LATEST 2024-11-16T06:19:04.275000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba first function input = {'ExecutionId': 'arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run'} 2024-11-16T06:19:04.278000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba LAMBDA_WARNING: Unhandled exception. The most likely cause is an issue in the function code. However, in rare cases, a Lambda runtime update can cause unexpected function behavior. For functions using managed runtimes, runtime updates can be triggered by a function change, or can be applied automatically. To determine if the runtime has been updated, check the runtime version in the INIT_START log entry. If this error correlates with a change in the runtime version, you may be able raise RuntimeError("Oops!!")4, in lambda_handlero the previous runtime version. For more information, see https://docs.aws.amazon.com/lambda/latest/dg/runtimes-update.html 2024-11-16T06:19:04.282000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba END RequestId: 0d8378da-e74e-41dc-a4a3-95d2b9d5eb8f 2024-11-16T06:19:04.285000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba REPORT RequestId: 0d8378da-e74e-41dc-a4a3-95d2b9d5eb8f Duration: 1.18 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 128 MB 2024-11-16T06:19:06.340000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba START RequestId: 1c27110e-c161-4bbd-bc1f-0000c452141b Version: $LATEST 2024-11-16T06:19:06.342000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba first function input = {'ExecutionId': 'arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run'} 2024-11-16T06:19:06.345000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba LAMBDA_WARNING: Unhandled exception. The most likely cause is an issue in the function code. However, in rare cases, a Lambda runtime update can cause unexpected function behavior. For functions using managed runtimes, runtime updates can be triggered by a function change, or can be applied automatically. To determine if the runtime has been updated, check the runtime version in the INIT_START log entry. If this error correlates with a change in the runtime version, you may be able raise RuntimeError("Oops!!")4, in lambda_handlero the previous runtime version. For more information, see https://docs.aws.amazon.com/lambda/latest/dg/runtimes-update.html 2024-11-16T06:19:06.348000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba END RequestId: 1c27110e-c161-4bbd-bc1f-0000c452141b 2024-11-16T06:19:06.350000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba REPORT RequestId: 1c27110e-c161-4bbd-bc1f-0000c452141b Duration: 1.18 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 128 MB
わかりにくいですが、3回実行されていることが確認できます。こちらは入力データに関するログを抜粋したものです。
2024-11-16T06:19:03.244000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba first function input = {'ExecutionId': 'arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run'} 2024-11-16T06:19:04.275000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba first function input = {'ExecutionId': 'arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run'} 2024-11-16T06:19:06.342000+00:00 2024/11/16/[$LATEST]7e9c103b236c39a7cb9ce1a9d42606ba first function input = {'ExecutionId': 'arn:aws:states:us-east-1:000000000000:execution:handle-error-state-machine:run'}
最初のリトライまで1秒、2回目のリトライは2秒後になっていることが確認できます。
2つ目のAWS Lambda関数のログを見てみましょう。
$ awslocal logs tail /aws/lambda/catch-function 2024-11-16T06:19:08.049000+00:00 2024/11/16/[$LATEST]b28967ff032a814b6ddd17f1624ca6e0 START RequestId: b2990806-5529-4a59-a96c-2f3234ef930b Version: $LATEST 2024-11-16T06:19:08.055000+00:00 2024/11/16/[$LATEST]b28967ff032a814b6ddd17f1624ca6e0 catch function input = {'Error': 'Exception', 'Cause': '{"errorMessage":"Oops!!","errorType":"RuntimeError","requestId":"1c27110e-c161-4bbd-bc1f-0000c452141b","stackTrace":[" File \\"/var/task/app.py\\", line 4, in lambda_handler\\n raise RuntimeError(\\"Oops!!\\")\\n"]}'} 2024-11-16T06:19:08.061000+00:00 2024/11/16/[$LATEST]b28967ff032a814b6ddd17f1624ca6e0 END RequestId: b2990806-5529-4a59-a96c-2f3234ef930b 2024-11-16T06:19:08.067000+00:00 2024/11/16/[$LATEST]b28967ff032a814b6ddd17f1624ca6e0 REPORT RequestId: b2990806-5529-4a59-a96c-2f3234ef930b Duration: 1.29 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 128 MB
Error
というフィールドにException
という値が、Cause
にひとつ前のAWS Lambda関数で発生したエラーの内容が含まれていますね。
2024-11-16T06:19:08.055000+00:00 2024/11/16/[$LATEST]b28967ff032a814b6ddd17f1624ca6e0 catch function input = {'Error': 'Exception', 'Cause': '{"errorMessage":"Oops!!","errorType":"RuntimeError","requestId":"1c27110e-c161-4bbd-bc1f-0000c452141b","stackTrace":[" File \\"/var/task/app.py\\", line 4, in lambda_handler\\n raise RuntimeError(\\"Oops!!\\")\\n"]}'}
これで、簡単ですがエラーハンドリングを確認できました。
おわりに
AWS Step Functionsのワークフローで、エラーハンドリングを試してみました。
とりあえず1度試しておこうくらいのつもりでやってみたのですが、こちらは割とすんなり確認できました。
設定についてもドキュメントを見てある程度雰囲気がわかったので、今回はこれでよいかなと思います。