🤖

Step Functions Local におけるモックサービス統合の使い方

2023/03/11に公開

これは何

  • Step Functions Local におけるモックサービス統合機能の使い方メモ
  • Step Functions Local を Docker で動かし、モックサービス統合で任意のエラーを発生させて、ステートマシンの振る舞いを確かめる
  • 以下の公式ドキュメントを読み解いていく

https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local-test-sm-exec.html

そもそも、どういう場面で便利な機能なのか

  • 実際の AWS リソースを作成せずに、ローカル環境で Step Functions のステートマシンを作成して振る舞いを確認したい場合
  • AWS 側のサービスエラーが発生した場合の振る舞いなど、実際の環境では意図的に発生させることが困難なシナリオに対して、テストや動作確認したい場合

Step Functions Local を Docker で動かして、ミニマムなステートマシンを動かしてみる

Step Functioins Local を起動

  • Docker コマンドでサクッと動かせる
  • 以降は Step Functions Local が動作する http://localhost:8083 に対して AWS CLI で Step Functions の操作を行っていく
docker run -p 8083:8083 amazon/aws-stepfunctions-local

ステートマシン定義

  • Hello World という文字列をレスポンスするステートマシンを定義
  • 以下の JSON を MyStateMachine.asl.json として保存
{
  "Comment": "A Hello World example",
  "StartAt": "HelloWorld",
  "States": {
    "HelloWorld": {
      "Type": "Pass",
      "Result": "Hello World!",
      "End": true
    }
  }
}

ステートマシンを作成

Step Functions Local にステートマシンを作成

aws stepfunctions --endpoint-url http://localhost:8083 create-state-machine --name "MyStateMachine" \
    --role-arn "arn:aws:iam::012345678901:role/DummyRole" \
    --definition file://$(pwd)/MyStateMachine.asl.json

実行結果

{
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:MyStateMachine",
    "creationDate": "2023-03-11T16:52:37.458000+09:00"
}

ステートマシンを実行

create-state-machine コマンドの結果としてレスポンスされた stateMachineArn を使用する

aws stepfunctions --endpoint http://localhost:8083 start-execution --name $(uuidgen) \
    --state-machine arn:aws:states:us-east-1:123456789012:stateMachine:MyStateMachine

実行結果

{
    "executionArn": "arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:6BCE7CC7-6D1F-4BCA-85DE-D56C0A295B14",
    "startDate": "2023-03-11T16:55:01.300000+09:00"
}

実行履歴を確認

start-execution コマンドの結果としてレスポンスされた executionArnget-execution-history コマンドで指定する

aws stepfunctions get-execution-history \
    --endpoint http://localhost:8083 \
    --execution-arn arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:6BCE7CC7-6D1F-4BCA-85DE-D56C0A295B14

実行結果

{
    "events": [
        {
            "timestamp": "2023-03-11T16:55:01.336000+09:00",
            "type": "ExecutionStarted",
            "id": 1,
            "previousEventId": 0,
            "executionStartedEventDetails": {
                "input": "{}",
                "inputDetails": {
                    "truncated": false
                },
                "roleArn": "arn:aws:iam::012345678901:role/DummyRole"
            }
        },
        {
            "timestamp": "2023-03-11T16:55:01.338000+09:00",
            "type": "PassStateEntered",
            "id": 2,
            "previousEventId": 0,
            "stateEnteredEventDetails": {
                "name": "HelloWorld",
                "input": "{}",
                "inputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T16:55:01.354000+09:00",
            "type": "PassStateExited",
            "id": 3,
            "previousEventId": 2,
            "stateExitedEventDetails": {
                "name": "HelloWorld",
                "output": "\"Hello World!\"",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T16:55:01.358000+09:00",
            "type": "ExecutionSucceeded",
            "id": 4,
            "previousEventId": 3,
            "executionSucceededEventDetails": {
                "output": "\"Hello World!\"",
                "outputDetails": {
                    "truncated": false
                }
            }
        }
    ]
}

一連のコマンドをサクッと実行できるように整理

# シェル変数セットアップ
StateMachineName="MyStateMachine"
DummyIamRoleArn="arn:aws:iam::012345678901:role/DummyRole"
StateMachineDefinition="file://$(pwd)/MyStateMachine.asl.json"
EndpointUrl="http://localhost:8083"

# ステートマシン作成
StateMachineArn=$(aws stepfunctions --endpoint-url ${EndpointUrl} create-state-machine --name ${StateMachineName} \
    --role-arn ${DummyIamRoleArn} \
    --definition ${StateMachineDefinition} | jq -r ".stateMachineArn")

# ステートマシン実行
ExecutionArn=$(aws stepfunctions --endpoint ${EndpointUrl} start-execution --name $(uuidgen) \
    --state-machine ${StateMachineArn} | jq -r ".executionArn")

# 実行結果を確認
aws stepfunctions get-execution-history \
    --endpoint ${EndpointUrl} \
    --execution-arn ${ExecutionArn}

モックサービス統合を動かす

以上の前提を整理した上で、公式ドキュメントのサンプルを読み解く

https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local-test-sm-exec.html

Lambda 関数を起動してレスポンスを待ってから、 SQS キューにメッセージを送信するというステートマシン

msi-graph

ステートマシン定義

MyStateMachine.asl.json

{
    "Comment": "This state machine is called: LambdaSQSIntegration",
    "StartAt": "LambdaState",
    "States": {
        "LambdaState": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "Parameters": {
                "Payload.$": "$",
                "FunctionName": "HelloWorldFunction"
            },
            "Retry": [{
                "ErrorEquals": [
                    "States.ALL"
                ],
                "IntervalSeconds": 2,
                "MaxAttempts": 3,
                "BackoffRate": 2
            }],
            "Next": "SQSState"
        },
        "SQSState": {
            "Type": "Task",
            "Resource": "arn:aws:states:::sqs:sendMessage",
            "Parameters": {
                "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/myQueue",
                "MessageBody.$": "$"
            },
            "End": true
        }
    }
}

モック設定ファイルを作成

モック設定ファイルとして MockConfigFile.json を作成

{
    "StateMachines": {
        "LambdaSQSIntegration": {
            "TestCases": {
                "HappyPath": {
                    "LambdaState": "MockedLambdaSuccess",
                    "SQSState": "MockedSQSSuccess"
                },
                "RetryPath": {
                    "LambdaState": "MockedLambdaRetry",
                    "SQSState": "MockedSQSSuccess"
                },
                "HybridPath": {
                    "LambdaState": "MockedLambdaSuccess"
                }
            }
        }
    },
    "MockedResponses": {
        "MockedLambdaSuccess": {
            "0": {
                "Return": {
                    "StatusCode": 200,
                    "Payload": {
                        "StatusCode": 200,
                        "body": "Hello from Lambda!"
                    }
                }
            }
        },
        "LambdaMockedResourceNotReady": {
            "0": {
                "Throw": {
                    "Error": "Lambda.ResourceNotReadyException",
                    "Cause": "Lambda resource is not ready."
                }
            }
        },
        "MockedSQSSuccess": {
            "0": {
                "Return": {
                    "MD5OfMessageBody": "3bcb6e8e-7h85-4375-b0bc-1a59812c6e51",
                    "MessageId": "3bcb6e8e-8b51-4375-b0bc-1a59812c6e51"
                }
            }
        },
        "MockedLambdaRetry": {
            "0": {
                "Throw": {
                    "Error": "Lambda.ResourceNotReadyException",
                    "Cause": "Lambda resource is not ready."
                }
            },
            "1-2": {
                "Throw": {
                    "Error": "Lambda.TimeoutException",
                    "Cause": "Lambda timed out."
                }
            },
            "3": {
                "Return": {
                    "StatusCode": 200,
                    "Payload": {
                        "StatusCode": 200,
                        "body": "Hello from Lambda!"
                    }
                }
            }
        }
    }
}

モック設定ファイルの構成について

  • StateMachines: モックを使用するステートマシン名を指定
    • LambdaSQSIntegration は後ほどステートマシンを作成時に指定
  • TestCases: ステートマシン毎に定義するテストケース群
    • ドキュメントの例では LambdaSQSIntegration ステートマシンに対して HappyPath, RetryPath, HybridPath という 3つ のテストケースを定義
    • LambdaState, SQSState: ステートマシンで定義されている各ステートに対して、どのモックレスポンスを使用するかの対応付け
  • MockedResponses: 各種モックレスポンスの定義
  • 各種モックレスポンスに含まれる "0", "1-2", "3" などの数字: Retry が発生するステートにおいて、何回目のリクエストごとにモックレスポンスを変更するという設定
    • 例えば MockedLambdaRetry においては、リトライを含めたリクエスト全体で、以下のようなモックレスポンスが発生する
      • 0: Lambda.ResourceNotReadyException が throw される
      • 1-2: Lambda.TimeoutException が throw される
      • 3: 正常に動作する

モック構成ファイルを指定して Step Functions Local を起動する

使用するリージョンや、実際に AWS サービスの API を呼び出す場合にはクレデンシャルの設定が必要であり、それらは aws-stepfunctions-local-credentials.txt に記載することで指定できる

https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local-config-options.html

aws-stepfunctions-local-credentials.txt とモック構成ファイルを指定して Step Functions Local を起動するコマンド例

docker run -p 8083:8083 \
    --env-file aws-stepfunctions-local-credentials.txt \
    --mount type=bind,readonly,source=$(pwd)/MockConfigFile.json,destination=/home/StepFunctionsLocal/MockConfigFile.json \
    -e SFN_MOCK_CONFIG="/home/StepFunctionsLocal/MockConfigFile.json" \
    amazon/aws-stepfunctions-local

ステートマシンの作成

# シェル変数セットアップ
StateMachineName="LambdaSQSIntegration" # モック構成ファイルで指定しているステートマシン名に変更
DummyIamRoleArn="arn:aws:iam::012345678901:role/DummyRole"
StateMachineDefinition="file://$(pwd)/MyStateMachine.asl.json"
EndpointUrl="http://localhost:8083"

# ステートマシン作成
StateMachineArn=$(aws stepfunctions --endpoint-url ${EndpointUrl} create-state-machine --name ${StateMachineName} \
    --role-arn ${DummyIamRoleArn} \
    --definition ${StateMachineDefinition} | jq -r ".stateMachineArn")

ステートマシンの実行と確認

  • ステートマシンを実行する際に、テストケースを指定する必要がある
  • 具体的には arn:aws:states:us-east-1:123456789012:stateMachine:LambdaSQSIntegration#HappyPath のように ステートマシンの ARN + #テストケース名 というフォーマットで指定
  • サンプルでは HappyPath, RetryPath, HybridPath という 3つ のテストケースがあるため、これら全てのテストケースを実行して結果を確認してみる

HappyPath

  • Lambda も SQS も成功した場合のモックがレスポンスがされるので、何事もなく正常終了する振る舞いが確認できる
# テストケース名
TestCase="HappyPath"

# ステートマシン実行
ExecutionArn=$(aws stepfunctions --endpoint ${EndpointUrl} start-execution --name $(uuidgen) \
    --state-machine "${StateMachineArn}#${TestCase}" | jq -r ".executionArn")

# 実行結果を確認
aws stepfunctions get-execution-history \
    --endpoint ${EndpointUrl} \
    --execution-arn ${ExecutionArn}
実行結果の全文
{
    "events": [
        {
            "timestamp": "2023-03-11T18:12:01.460000+09:00",
            "type": "ExecutionStarted",
            "id": 1,
            "previousEventId": 0,
            "executionStartedEventDetails": {
                "input": "{}",
                "inputDetails": {
                    "truncated": false
                },
                "roleArn": "arn:aws:iam::012345678901:role/DummyRole"
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.460000+09:00",
            "type": "TaskStateEntered",
            "id": 2,
            "previousEventId": 0,
            "stateEnteredEventDetails": {
                "name": "LambdaState",
                "input": "{}",
                "inputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.465000+09:00",
            "type": "TaskScheduled",
            "id": 3,
            "previousEventId": 2,
            "taskScheduledEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "region": "us-east-1",
                "parameters": "{\"FunctionName\":\"HelloWorldFunction\",\"Payload\":{}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.466000+09:00",
            "type": "TaskStarted",
            "id": 4,
            "previousEventId": 3,
            "taskStartedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke"
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.471000+09:00",
            "type": "TaskSucceeded",
            "id": 5,
            "previousEventId": 4,
            "taskSucceededEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "output": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.472000+09:00",
            "type": "TaskStateExited",
            "id": 6,
            "previousEventId": 5,
            "stateExitedEventDetails": {
                "name": "LambdaState",
                "output": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.472000+09:00",
            "type": "TaskStateEntered",
            "id": 7,
            "previousEventId": 6,
            "stateEnteredEventDetails": {
                "name": "SQSState",
                "input": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "inputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.473000+09:00",
            "type": "TaskScheduled",
            "id": 8,
            "previousEventId": 7,
            "taskScheduledEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage",
                "region": "us-east-1",
                "parameters": "{\"QueueUrl\":\"https://sqs.us-east-1.amazonaws.com/123456789012/myQueue\",\"MessageBody\":{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.474000+09:00",
            "type": "TaskStarted",
            "id": 9,
            "previousEventId": 8,
            "taskStartedEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage"
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.474000+09:00",
            "type": "TaskSucceeded",
            "id": 10,
            "previousEventId": 9,
            "taskSucceededEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage",
                "output": "{\"MD5OfMessageBody\":\"3bcb6e8e-7h85-4375-b0bc-1a59812c6e51\",\"MessageId\":\"3bcb6e8e-8b51-4375-b0bc-1a59812c6e51\"}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.475000+09:00",
            "type": "TaskStateExited",
            "id": 11,
            "previousEventId": 10,
            "stateExitedEventDetails": {
                "name": "SQSState",
                "output": "{\"MD5OfMessageBody\":\"3bcb6e8e-7h85-4375-b0bc-1a59812c6e51\",\"MessageId\":\"3bcb6e8e-8b51-4375-b0bc-1a59812c6e51\"}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:12:01.475000+09:00",
            "type": "ExecutionSucceeded",
            "id": 12,
            "previousEventId": 11,
            "executionSucceededEventDetails": {
                "output": "{\"MD5OfMessageBody\":\"3bcb6e8e-7h85-4375-b0bc-1a59812c6e51\",\"MessageId\":\"3bcb6e8e-8b51-4375-b0bc-1a59812c6e51\"}",
                "outputDetails": {
                    "truncated": false
                }
            }
        }
    ]
}

RetryPath

  • リトライが走るテストケースのため、実行終了まで少々時間がかかるものの、正常に終了する振る舞いが確認できる
    • Lambda ステートにてリトライが発生した振る舞いがモックされるものの 4回目 のリトライで正常終了する
    • SQS ステートは正常なレスポンスがモックされる
# テストケース名
TestCase="RetryPath"

# ステートマシン実行
ExecutionArn=$(aws stepfunctions --endpoint ${EndpointUrl} start-execution --name $(uuidgen) \
    --state-machine "${StateMachineArn}#${TestCase}" | jq -r ".executionArn")

# 実行結果を確認
aws stepfunctions get-execution-history \
    --endpoint ${EndpointUrl} \
    --execution-arn ${ExecutionArn}
実行結果の全文
{
    "events": [
        {
            "timestamp": "2023-03-11T18:14:09.620000+09:00",
            "type": "ExecutionStarted",
            "id": 1,
            "previousEventId": 0,
            "executionStartedEventDetails": {
                "input": "{}",
                "inputDetails": {
                    "truncated": false
                },
                "roleArn": "arn:aws:iam::012345678901:role/DummyRole"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:09.620000+09:00",
            "type": "TaskStateEntered",
            "id": 2,
            "previousEventId": 0,
            "stateEnteredEventDetails": {
                "name": "LambdaState",
                "input": "{}",
                "inputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:14:09.621000+09:00",
            "type": "TaskScheduled",
            "id": 3,
            "previousEventId": 2,
            "taskScheduledEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "region": "us-east-1",
                "parameters": "{\"FunctionName\":\"HelloWorldFunction\",\"Payload\":{}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:09.622000+09:00",
            "type": "TaskStarted",
            "id": 4,
            "previousEventId": 3,
            "taskStartedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:09.630000+09:00",
            "type": "TaskFailed",
            "id": 5,
            "previousEventId": 4,
            "taskFailedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "error": "Lambda.ResourceNotReadyException",
                "cause": "Lambda resource is not ready."
            }
        },
        {
            "timestamp": "2023-03-11T18:14:11.633000+09:00",
            "type": "TaskScheduled",
            "id": 6,
            "previousEventId": 5,
            "taskScheduledEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "region": "us-east-1",
                "parameters": "{\"FunctionName\":\"HelloWorldFunction\",\"Payload\":{}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:11.633000+09:00",
            "type": "TaskStarted",
            "id": 7,
            "previousEventId": 6,
            "taskStartedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:11.634000+09:00",
            "type": "TaskFailed",
            "id": 8,
            "previousEventId": 7,
            "taskFailedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "error": "Lambda.TimeoutException",
                "cause": "Lambda timed out."
            }
        },
        {
            "timestamp": "2023-03-11T18:14:15.635000+09:00",
            "type": "TaskScheduled",
            "id": 9,
            "previousEventId": 8,
            "taskScheduledEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "region": "us-east-1",
                "parameters": "{\"FunctionName\":\"HelloWorldFunction\",\"Payload\":{}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:15.636000+09:00",
            "type": "TaskStarted",
            "id": 10,
            "previousEventId": 9,
            "taskStartedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:15.636000+09:00",
            "type": "TaskFailed",
            "id": 11,
            "previousEventId": 10,
            "taskFailedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "error": "Lambda.TimeoutException",
                "cause": "Lambda timed out."
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.640000+09:00",
            "type": "TaskScheduled",
            "id": 12,
            "previousEventId": 11,
            "taskScheduledEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "region": "us-east-1",
                "parameters": "{\"FunctionName\":\"HelloWorldFunction\",\"Payload\":{}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.641000+09:00",
            "type": "TaskStarted",
            "id": 13,
            "previousEventId": 12,
            "taskStartedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.643000+09:00",
            "type": "TaskSucceeded",
            "id": 14,
            "previousEventId": 13,
            "taskSucceededEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "output": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.643000+09:00",
            "type": "TaskStateExited",
            "id": 15,
            "previousEventId": 14,
            "stateExitedEventDetails": {
                "name": "LambdaState",
                "output": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.643000+09:00",
            "type": "TaskStateEntered",
            "id": 16,
            "previousEventId": 15,
            "stateEnteredEventDetails": {
                "name": "SQSState",
                "input": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "inputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.644000+09:00",
            "type": "TaskScheduled",
            "id": 17,
            "previousEventId": 16,
            "taskScheduledEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage",
                "region": "us-east-1",
                "parameters": "{\"QueueUrl\":\"https://sqs.us-east-1.amazonaws.com/123456789012/myQueue\",\"MessageBody\":{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.645000+09:00",
            "type": "TaskStarted",
            "id": 18,
            "previousEventId": 17,
            "taskStartedEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage"
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.645000+09:00",
            "type": "TaskSucceeded",
            "id": 19,
            "previousEventId": 18,
            "taskSucceededEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage",
                "output": "{\"MD5OfMessageBody\":\"3bcb6e8e-7h85-4375-b0bc-1a59812c6e51\",\"MessageId\":\"3bcb6e8e-8b51-4375-b0bc-1a59812c6e51\"}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.645000+09:00",
            "type": "TaskStateExited",
            "id": 20,
            "previousEventId": 19,
            "stateExitedEventDetails": {
                "name": "SQSState",
                "output": "{\"MD5OfMessageBody\":\"3bcb6e8e-7h85-4375-b0bc-1a59812c6e51\",\"MessageId\":\"3bcb6e8e-8b51-4375-b0bc-1a59812c6e51\"}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:14:23.646000+09:00",
            "type": "ExecutionSucceeded",
            "id": 21,
            "previousEventId": 20,
            "executionSucceededEventDetails": {
                "output": "{\"MD5OfMessageBody\":\"3bcb6e8e-7h85-4375-b0bc-1a59812c6e51\",\"MessageId\":\"3bcb6e8e-8b51-4375-b0bc-1a59812c6e51\"}",
                "outputDetails": {
                    "truncated": false
                }
            }
        }
    ]
}

HybridPath

  • Lambda ステートとしては正常なレスポンスがモックされる
  • SQS ステートはモックレスポンスが指定されていないため、実際に SendMessage API の呼び出しが行われるが、ステートマシン定義で指定している https://sqs.us-east-1.amazonaws.com/123456789012/myQueue というリソースは存在しないためエラーで終了する
# テストケース名
TestCase="HybridPath"

# ステートマシン実行
ExecutionArn=$(aws stepfunctions --endpoint ${EndpointUrl} start-execution --name $(uuidgen) \
    --state-machine "${StateMachineArn}#${TestCase}" | jq -r ".executionArn")

# 実行結果を確認
aws stepfunctions get-execution-history \
    --endpoint ${EndpointUrl} \
    --execution-arn ${ExecutionArn}
実行結果の全文
{
    "events": [
        {
            "timestamp": "2023-03-11T18:17:43.773000+09:00",
            "type": "ExecutionStarted",
            "id": 1,
            "previousEventId": 0,
            "executionStartedEventDetails": {
                "input": "{}",
                "inputDetails": {
                    "truncated": false
                },
                "roleArn": "arn:aws:iam::012345678901:role/DummyRole"
            }
        },
        {
            "timestamp": "2023-03-11T18:17:43.774000+09:00",
            "type": "TaskStateEntered",
            "id": 2,
            "previousEventId": 0,
            "stateEnteredEventDetails": {
                "name": "LambdaState",
                "input": "{}",
                "inputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:17:43.882000+09:00",
            "type": "TaskScheduled",
            "id": 3,
            "previousEventId": 2,
            "taskScheduledEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "region": "ap-northeast-1",
                "parameters": "{\"FunctionName\":\"HelloWorldFunction\",\"Payload\":{}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:17:43.884000+09:00",
            "type": "TaskStarted",
            "id": 4,
            "previousEventId": 3,
            "taskStartedEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke"
            }
        },
        {
            "timestamp": "2023-03-11T18:17:43.899000+09:00",
            "type": "TaskSucceeded",
            "id": 5,
            "previousEventId": 4,
            "taskSucceededEventDetails": {
                "resourceType": "lambda",
                "resource": "invoke",
                "output": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:17:43.901000+09:00",
            "type": "TaskStateExited",
            "id": 6,
            "previousEventId": 5,
            "stateExitedEventDetails": {
                "name": "LambdaState",
                "output": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "outputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:17:43.903000+09:00",
            "type": "TaskStateEntered",
            "id": 7,
            "previousEventId": 6,
            "stateEnteredEventDetails": {
                "name": "SQSState",
                "input": "{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}",
                "inputDetails": {
                    "truncated": false
                }
            }
        },
        {
            "timestamp": "2023-03-11T18:17:43.907000+09:00",
            "type": "TaskScheduled",
            "id": 8,
            "previousEventId": 7,
            "taskScheduledEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage",
                "region": "ap-northeast-1",
                "parameters": "{\"QueueUrl\":\"https://sqs.us-east-1.amazonaws.com/123456789012/myQueue\",\"MessageBody\":{\"StatusCode\":200,\"Payload\":{\"StatusCode\":200,\"body\":\"Hello from Lambda!\"}}}"
            }
        },
        {
            "timestamp": "2023-03-11T18:17:43.907000+09:00",
            "type": "TaskStarted",
            "id": 9,
            "previousEventId": 8,
            "taskStartedEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage"
            }
        },
        {
            "timestamp": "2023-03-11T18:17:45.054000+09:00",
            "type": "TaskFailed",
            "id": 10,
            "previousEventId": 9,
            "taskFailedEventDetails": {
                "resourceType": "sqs",
                "resource": "sendMessage",
                "error": "SQS.AmazonSQSException",
                "cause": "The address https://sqs.us-east-1.amazonaws.com/123456789012/myQueue is not valid for this endpoint. (Service: AmazonSQS; Status Code: 404; Error Code: InvalidAddress; Request ID: 8cb32d05-344d-51fa-bc7f-71837d35a611; Proxy: null)"
            }
        },
        {
            "timestamp": "2023-03-11T18:17:45.058000+09:00",
            "type": "ExecutionFailed",
            "id": 11,
            "previousEventId": 10,
            "executionFailedEventDetails": {
                "error": "SQS.AmazonSQSException",
                "cause": "The address https://sqs.us-east-1.amazonaws.com/123456789012/myQueue is not valid for this endpoint. (Service: AmazonSQS; Status Code: 404; Error Code: InvalidAddress; Request ID: 8cb32d05-344d-51fa-bc7f-71837d35a611; Proxy: null)"
            }
        }
    ]
}

感想

  • Mock ファイルの定義方法や、テストケースを指定した実行など、ドキュメントを読み解くのが難しかった・・・
  • 地味に Step Functions Local のコードは公開されていないようなので、この振る舞いを読み解くのも難しいポイント
  • Step Functions を使用する上では結構汎用的なテクニックな気がするので、使ってみたいけどドキュメントが難しい・・・という方に刺されば幸い

試した環境

% sw_vers
ProductName:            macOS
ProductVersion:         13.2.1
BuildVersion:           22D68
% docker -v
Docker version 20.10.22, build 3a2c30b
% docker image inspect amazon/aws-stepfunctions-local
[
    {
        "Id": "sha256:a30ba5c83abb66eaacd7155540f408bb5328813166ad98f6456e5a76ad0ebbd1",
        "RepoTags": [
            "amazon/aws-stepfunctions-local:latest"
        ],
        "RepoDigests": [
            "amazon/aws-stepfunctions-local@sha256:6672e363b0c8fe7fcbab3d9b7eaf268d89b7e89f0c1479a98cb72dde30d1fcfb"
        ],
        "Parent": "",
        "Comment": "buildkit.dockerfile.v0",
        "Created": "2022-11-21T22:41:28.892652055Z",
        "Container": "",
        "ContainerConfig": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": null,
            "Cmd": null,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "DockerVersion": "",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "stepfunctionslocal",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "8083/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "-jar",
                "StepFunctionsLocal.jar"
            ],
            "ArgsEscaped": true,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "/home/stepfunctionslocal",
            "Entrypoint": [
                "java"
            ],
            "OnBuild": null,
            "Labels": {
                "aws.java.sdk.version": ""
            }
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 863241167,
        "VirtualSize": 863241167,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/641a933b708ca12dd48bd8defad15133d2d936d2b8e4ff724a339bc813479726/diff:/var/lib/docker/overlay2/b3032bbfc2363fcdbcd90f7092b4281cad63e756015ac319375858cbc417d5fd/diff:/var/lib/docker/overlay2/47ed62d52307875a196ffa06130ff25b1ff3a445256ed916eff70655a58fdeec/diff",
                "MergedDir": "/var/lib/docker/overlay2/4b7705b7ce50e3b95c249b5b806f580798ec2e5af8a3ee4a7bcd0435ee55ee85/merged",
                "UpperDir": "/var/lib/docker/overlay2/4b7705b7ce50e3b95c249b5b806f580798ec2e5af8a3ee4a7bcd0435ee55ee85/diff",
                "WorkDir": "/var/lib/docker/overlay2/4b7705b7ce50e3b95c249b5b806f580798ec2e5af8a3ee4a7bcd0435ee55ee85/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:4635ea08a5587840b3141185b30f1c667584b11e9ec96c47152563f52d7882e4",
                "sha256:b7791268875e7be2efa508fa6e39490602defb06f84b2659a59f15fbb7c4ab63",
                "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
                "sha256:5f63578b3562be1e9c688557e3797244bf86ea9d2dcd67de95697907fcc47475"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

Discussion