LocalStack × AWS SAMで、エラーハンドリングをするAWS Step Functionsのワークフローを書いてみる - CLOVER🍀

CLOVER🍀

That was when it all began.

LocalStack × AWS SAMで、エラーハンドリングをするAWS Step Functionsのワークフローを書いてみる

これは、なにをしたくて書いたもの?

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ステートが失敗した。RetryCatchで指定すると、States.Timeout以外に対するワイルドカードとして機能する
  • States.Timeout … TaskステートがTimeoutSecondsよりも長時間実行されたか、HeartbeatSeconds値よりも長い間隔のハートビートの送信に失敗した場合

ステートは他の名前でエラーを報告することもあるようですが、States.プレフィックスで始まる名前にすることはできません。

例としては以下があります。たとえば、Lambda.ClientExecutionTimeoutExceptionなど。

Step Functions のベストプラクティス / 一時的な Lambda サービスの例外を処理する

エラーが発生した時には、リトライまたはフォールバックを行うことができます。リトライはRetry、フォールバックはCatch
表現されます。

RetryCatchも、配列で複数定義することができます。

Retryでは、リトライ対象とするエラー名、リトライ間隔やリトライ回数などを指定します。

Catchでは、ハンドリング対象とするエラー名と遷移先のステートを指定します。次のステートに対して出力をResultPathで指定することも
できます。

Catch対象となった場合はCauseというフィールドが含まれ、これをエラー出力と呼ぶそうです。遷移先のステートでは、たとえば
Parametersフィールドで$.Causeという指定でこの内容にアクセスできます。

RetryCatchの例については、このページ内の各種サンプルを見るとよいでしょう。

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度試しておこうくらいのつもりでやってみたのですが、こちらは割とすんなり確認できました。

設定についてもドキュメントを見てある程度雰囲気がわかったので、今回はこれでよいかなと思います。