CLOVER🍀

That was when it all began.

AWS Step Functions LocalとAWS SAMを使って、AWS Lambdaを使ったチュートリアルを試してみる

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

こちらのエントリーでAWS Step Functions Localを試してみました。

AWS Step Functionsをローカルで試す(AWS Step Functions Localを動かしてみる) - CLOVER🍀

この時はとりあえず動かしただけなので、今度はAWS Lambdaと組み合わせてみましょう。AWS Step Functionsのチュートリアルのうち、
AWS Lambdaと組み合わせているものを少し選んでみます。

AWS Lambdaの関数はAWS SAMで作成することにします。

対象とするチュートリアル

AWS Step Functionsには慣れていないので、今回はドキュメントに沿って試してみるのがよいでしょう。

対象はこちらにします。

チュートリアル: Step Functions と を使用したワークフローのテスト AWS SAM CLI ローカル - AWS Step Functions

Lambda を使用する Step Functions ステートマシン状態の作成 - AWS Step Functions

Step Functions で Lambda 関数を使用してループを反復する - AWS Step Functions

これらをAWS Step Functions LocalとAWS SAMで試していこうと思います。

AWS Lambda関数はPythonで作成することにします。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.4 2024-07-16
OpenJDK Runtime Environment (build 21.0.4+7-Ubuntu-1ubuntu222.04)
OpenJDK 64-Bit Server VM (build 21.0.4+7-Ubuntu-1ubuntu222.04, mixed mode, sharing)


$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)


$ aws --version
aws-cli/2.19.4 Python/3.12.6 Linux/5.15.0-125-generic exe/x86_64.ubuntu.22


$ sam --version
SAM CLI, version 1.127.0

AWS CLIAWS SAM用のクレデンシャルは以下で設定。

$ export AWS_ACCESS_KEY_ID=test
$ export AWS_SECRET_ACCESS_KEY=test
$ export AWS_DEFAULT_REGION=us-east-1

AWS Step Functions Local。

$ java -jar StepFunctionsLocal.jar --version
Step Functions Local
Version: 2.0.0
Build: 2024-05-18
Step Functions Local
Version: 2.0.0
Build: 2024-05-18

クレデンシャルはこうしておきます。

$ export AWS_ACCESS_KEY_ID=test
$ export AWS_SECRET_ACCESS_KEY=test

AWS Step Functions Localのチュートリアルを試す

まずはこちらから試していこうと思います。

チュートリアル: Step Functions と を使用したワークフローのテスト AWS SAM CLI ローカル - AWS Step Functions

AWS SAMのPython 3.10向けのテンプレートを確認。

$ curl -s https://raw.githubusercontent.com/aws/aws-sam-cli-app-templates/master/manifest-v2.json | jq '."python3.10"[]'
{
  "directory": "python3.10/hello",
  "displayName": "Hello World Example",
  "dependencyManager": "pip",
  "appTemplate": "hello-world",
  "packageType": "Zip",
  "useCaseName": "Hello World Example"
}
{
  "directory": "python3.10/hello-pt",
  "displayName": "Hello World Example with Powertools for AWS Lambda",
  "dependencyManager": "pip",
  "appTemplate": "hello-world-powertools-python",
  "packageType": "Zip",
  "useCaseName": "Hello World Example with Powertools for AWS Lambda"
}
{
  "directory": "python3.10/event-bridge",
  "displayName": "EventBridge Hello World",
  "dependencyManager": "pip",
  "appTemplate": "eventBridge-hello-world",
  "packageType": "Zip",
  "useCaseName": "Infrastructure event management"
}
{
  "directory": "python3.10/event-bridge-schema",
  "displayName": "EventBridge App from scratch (100+ Event Schemas)",
  "dependencyManager": "pip",
  "appTemplate": "eventBridge-schema-app",
  "isDynamicTemplate": "True",
  "packageType": "Zip",
  "useCaseName": "Infrastructure event management"
}
{
  "directory": "python3.10/step-func",
  "displayName": "Step Functions Sample App (Stock Trader)",
  "dependencyManager": "pip",
  "appTemplate": "step-functions-sample-app",
  "packageType": "Zip",
  "useCaseName": "Multi-step workflow"
}
{
  "directory": "python3.10/efs",
  "displayName": "Elastic File System Sample App",
  "dependencyManager": "pip",
  "appTemplate": "efs-sample-app",
  "packageType": "Zip",
  "useCaseName": "Lambda EFS example"
}
{
  "directory": "python3.10/web-conn",
  "displayName": "Quick Start: Web Backend With Connectors",
  "dependencyManager": "pip",
  "appTemplate": "hello-world-connector",
  "packageType": "Zip",
  "useCaseName": "Serverless Connector Hello World Example"
}
{
  "directory": "python3.10/step-func-conn",
  "displayName": "Step Functions Sample App (Stock Trader) With Connectors",
  "dependencyManager": "pip",
  "appTemplate": "step-functions-with-connectors",
  "packageType": "Zip",
  "useCaseName": "Multi-step workflow with Connectors"
}

プロジェクトを作成。

$ sam init --name sfn-local-sam-getting-started --runtime python3.10 --app-template hello-world --package-type Zip --no-tracing --no-application-insights --structured-logging

プロジェクト内に移動。

$ cd sfn-local-sam-getting-started

プロジェクト内はこんな感じですね。

$ tree
.
├── README.md
├── __init__.py
├── events
│   └── event.json
├── hello_world
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── samconfig.toml
├── template.yaml
└── tests
    ├── __init__.py
    ├── integration
    │   ├── __init__.py
    │   └── test_api_gateway.py
    ├── requirements.txt
    └── unit
        ├── __init__.py
        └── test_handler.py

5 directories, 14 files

今回はあまりAWS SAMの説明はせず、AWS Lambda関数の部分だけにフォーカスしてみていきましょう。

用意されているAWS SAMのテンプレートおよびAWS Lambda関数のスケルトン実装を見てみます。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sfn-local-sam-getting-started

  Sample SAM Template for sfn-local-sam-getting-started

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

    # You can add LoggingConfig parameters such as the Logformat, Log Group, and SystemLogLevel or ApplicationLogLevel. Learn more here https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-loggingconfig.
    LoggingConfig:
      LogFormat: JSON
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.10
      Architectures:
      - x86_64
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: API Gateway endpoint URL for Prod stage for Hello World function
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: Hello World Lambda Function ARN
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: Implicit IAM Role created for Hello World function
    Value: !GetAtt HelloWorldFunctionRole.Arn

hello_world/app.py

import json

# import requests


def lambda_handler(event, context):
    """Sample pure Lambda function

    Parameters
    ----------
    event: dict, required
        API Gateway Lambda Proxy Input Format

        Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format

    context: object, required
        Lambda Context runtime methods and attributes

        Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html

    Returns
    ------
    API Gateway Lambda Proxy Output Format: dict

        Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
    """

    # try:
    #     ip = requests.get("http://checkip.amazonaws.com/")
    # except requests.RequestException as e:
    #     # Send some context about this error to Lambda Logs
    #     print(e)

    #     raise e

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
            # "location": ip.text.replace("\n", "")
        }),
    }

どう見てもAmazon API Gateway向けのものですね。

ローカルでAmazon API GatewayのエミュレーションをするHTTPサーバーを起動。

$ sam local start-api

3000ポートでリッスンします。

確認。

$ curl localhost:3000/hello
{"message": "hello world"}

1度停止して、AWS Lambdaの実行環境を起動。

$ sam local start-lambda

AWS Lambdaのエンドポイントを指定して、AWS Step Functions Localを起動。

$ java -jar StepFunctionsLocal.jar --lambda-endpoint http://localhost:3001

ログはこんな感じになりました。

Step Functions Local
Version: 2.0.0
Build: 2024-05-18
2024-11-09 18:31:04.541: Configure [Lambda Endpoint] to [http://localhost:3001]
2024-11-09 18:31:04.545: Loaded credentials from environment
2024-11-09 18:31:05.221: Starting server on port 8083 with account 123456789012, region us-east-1
SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.

ステートマシンを作成します。

$ aws --endpoint http://localhost:8083 stepfunctions create-state-machine --definition "{\
  \"Comment\": \"A Hello World example of the Amazon States Language using an AWS Lambda Local function\",\
  \"StartAt\": \"HelloWorld\",\
  \"States\": {\
    \"HelloWorld\": {\
      \"Type\": \"Task\",\
      \"Resource\": \"arn:aws:lambda:us-east-1:123456789012:function:HelloWorldFunction\",\
      \"End\": true\
    }\
  }\
}" --name 'HelloWorld' --role-arn 'arn:aws:iam::012345678901:role/DummyRole'

作成できました。

{
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld",
    "creationDate": "2024-11-09T18:33:46.330000+09:00"
}

文字列ですが、ステートマシンの定義を見てみましょう。

{\
  \"Comment\": \"A Hello World example of the Amazon States Language using an AWS Lambda Local function\",\
  \"StartAt\": \"HelloWorld\",\
  \"States\": {\
    \"HelloWorld\": {\
      \"Type\": \"Task\",\
      \"Resource\": \"arn:aws:lambda:us-east-1:123456789012:function:HelloWorldFunction\",\
      \"End\": true\
    }\
  }\
}

Amazon States Language(ASL)のリファレンスはこちら。

Step Functions ワークフローの Amazon States Language のステートマシン構造 - AWS Step Functions

Commentは文字通りコメント、StartAtはステートマシンが開始した際に最初に実行するステートを指定します。

ステートはStatesで定義されていて、「HelloWorld」というひとつのステートがあります。

  \"States\": {\
    \"HelloWorld\": {\
      \"Type\": \"Task\",\
      \"Resource\": \"arn:aws:lambda:us-east-1:123456789012:function:HelloWorldFunction\",\
      \"End\": true\
    }\
  }\

これはAWS Lambdaなどを呼び出せるステートですね。

タスクワークフローの状態 - AWS Step Functions

Endtrueになっているので、このステートが終了するとこのステートマシンが終了します。

Resourceは呼び出すAWSリソースのARNを指定します。今回はAWS Lambda関数ですね。

今回はAWS Lambda関数をデプロイしているわけではありませんが、sam local start-lambdaで起動したAWS Lambda関数環境内に含まれる
リソース名と一致しているので、これで呼び出せるんだと思います。

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction

つまり、こういう構図になるわけですね。

flowchart LR
    A[AWS CLI] --> |start-execution| B[AWS Step Functions Local]
    B --> |invoke| C[AWS SAM start-lambda]
    C --> B
    B --> A

では、ステートマシンを実行してみましょう。

$ aws --endpoint http://localhost:8083 stepfunctions start-execution --state-machine arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld --name test

AWS Step Functions Localにはこんなログが出力されました。

2024-11-09 18:43:54.121: StartExecution => {"requestClientOptions":{"readLimit":131073,"skipAppendUriPath":false},"requestMetricCollector":null,"customRequestHeaders":null,"customQueryParameters":null,"cloneSource":null,"sdkRequestTimeout":null,"sdkClientExecutionTimeout":null,"stateMachineArn":"arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld","name":"test","input":null,"traceHeader":null,"requestCredentialsProvider":null,"requestCredentials":null,"generalProgressListener":{"syncCallSafe":true},"readLimit":131073,"cloneRoot":null}
2024-11-09 18:43:54.470: arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test : {"Type":"ExecutionStarted","PreviousEventId":0,"ExecutionStartedEventDetails":{"Input":"{}","RoleArn":"arn:aws:iam::012345678901:role/DummyRole"}}
2024-11-09 18:43:54.471: arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test : {"Type":"TaskStateEntered","PreviousEventId":0,"StateEnteredEventDetails":{"Name":"HelloWorld","Input":"{}"}}
2024-11-09 18:43:54.475: [200] StartExecution <= {"sdkResponseMetadata":null,"sdkHttpMetadata":null,"executionArn":"arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test","startDate":1731145434441}
2024-11-09 18:43:54.507: arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test : {"Type":"LambdaFunctionScheduled","PreviousEventId":2,"LambdaFunctionScheduledEventDetails":{"Resource":"arn:aws:lambda:us-east-1:123456789012:function:HelloWorldFunction","Input":"{}"}}
2024-11-09 18:43:54.508: arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test : {"Type":"LambdaFunctionStarted","PreviousEventId":3}
2024-11-09 18:43:55.919: arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test : {"Type":"LambdaFunctionSucceeded","PreviousEventId":4,"LambdaFunctionSucceededEventDetails":{"Output":"{\"statusCode\": 200, \"body\": \"{\\\"message\\\": \\\"hello world\\\"}\"}"}}
2024-11-09 18:43:55.920: arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test : {"Type":"TaskStateExited","PreviousEventId":5,"StateExitedEventDetails":{"Name":"HelloWorld","Output":"{\"statusCode\": 200, \"body\": \"{\\\"message\\\": \\\"hello world\\\"}\"}"}}
2024-11-09 18:43:55.922: arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test : {"Type":"ExecutionSucceeded","PreviousEventId":6,"ExecutionSucceededEventDetails":{"Output":"{\"statusCode\": 200, \"body\": \"{\\\"message\\\": \\\"hello world\\\"}\"}"}}

AWS SAM側にはこんなログが出力されました。

2024-11-09 18:43:55 127.0.0.1 - - [09/Nov/2024 18:43:55] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -

動いたようです。

結果を確認。

$ aws --endpoint http://localhost:8083 stepfunctions describe-execution --execution-arn arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test

OKですね。

{
    "executionArn": "arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test",
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld",
    "name": "test",
    "status": "SUCCEEDED",
    "startDate": "2024-11-09T18:43:54.441000+09:00",
    "stopDate": "2024-11-09T18:43:55.923000+09:00",
    "input": "{}",
    "inputDetails": {
        "included": true
    },
    "output": "{\"statusCode\": 200, \"body\": \"{\\\"message\\\": \\\"hello world\\\"}\"}",
    "outputDetails": {
        "included": true
    }
}

outputを見ると、AWS Lambda関数の戻り値が含まれていることが確認できます。

これで最初のチュートリアルは確認できました。

AWS Step FunctionsのAWS Lambdaのチュートリアルを試す

次は、こちらのチュートリアルを試してみます。

Lambda を使用する Step Functions ステートマシン状態の作成 - AWS Step Functions

パッと見、先程のAWS Lambdaの例と変わらないのではと思いましたが、呼び出し時にペイロードを取るパターンですね。
簡単に試しておきましょう。

先程のAWS Step Functions LocalとAWS SAMのプロセスは終了し、新しく作り直します。ここから先は、すべて別々にアプリケーションを
作成して行うことにします。

AWS SAMプロジェクトを作成。

$ sam init --name sfn-local-invoke-lambda --runtime python3.10 --app-template hello-world --package-type Zip --no-tracing --no-application-insights --structured-logging
$ cd sfn-local-invoke-lambda

AWS Lambda関数は、こんな感じに修正。

hello_world/app.py

def lambda_handler(event, context):
    return f'Hello from {event["who"]}'

AWS SAMでAWS Lambda環境を、そしてAWS Step Functions Localを起動します。

$ sam local start-lambda


$ java -jar StepFunctionsLocal.jar --lambda-endpoint http://localhost:3001

ステートマシンを作成します。定義自体は、先ほどと同じですね。

$ aws --endpoint http://localhost:8083 stepfunctions create-state-machine --definition "{\
  \"Comment\": \"A Hello World example of the Amazon States Language using an AWS Lambda Local function\",\
  \"StartAt\": \"HelloWorld\",\
  \"States\": {\
    \"HelloWorld\": {\
      \"Type\": \"Task\",\
      \"Resource\": \"arn:aws:lambda:us-east-1:123456789012:function:HelloWorldFunction\",\
      \"End\": true\
    }\
  }\
}" --name 'HelloWorld' --role-arn 'arn:aws:iam::012345678901:role/DummyRole'

ステートマシンを実行します。

$ aws --endpoint http://localhost:8083 stepfunctions start-execution --state-machine arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld --name test --input '{"who": "AWS Step Functions"}'
{
    "executionArn": "arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test",
    "startDate": "2024-11-09T19:05:56.723000+09:00"
}

この時、--input '{"who": "AWS Step Functions"}'で入力を与えます。

実行結果を確認。

$ aws --endpoint http://localhost:8083 stepfunctions describe-execution --execution-arn arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test
{
    "executionArn": "arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test",
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld",
    "name": "test",
    "status": "SUCCEEDED",
    "startDate": "2024-11-09T19:05:56.723000+09:00",
    "stopDate": "2024-11-09T19:05:57.284000+09:00",
    "input": "{\"who\": \"AWS Step Functions\"}",
    "inputDetails": {
        "included": true
    },
    "output": "\"Hello from AWS Step Functions\"",
    "outputDetails": {
        "included": true
    }
}

outputを見ると、AWS Lambdaに--inputで渡した値が引き継がれていることがわかります。

--inputを変えて実行してみましょう。

$ aws --endpoint http://localhost:8083 stepfunctions start-execution --state-machine arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld --name test-name --input '{"who": "AWS Lambda"}'
{
    "executionArn": "arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test-name",
    "startDate": "2024-11-09T19:09:53.368000+09:00"
}

この時、--nameの値を変えてあげないとすでに同じ実行が存在するのでエラーになります。

$ aws --endpoint http://localhost:8083 stepfunctions start-execution --state-machine arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld --name test --input '{"who": "AWS Lambda"}'

An error occurred (ExecutionAlreadyExists) when calling the StartExecution operation: Execution Already Exists: 'arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test'

結果の確認。

$ aws --endpoint http://localhost:8083 stepfunctions describe-execution --execution-arn arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test-name
{
    "executionArn": "arn:aws:states:us-east-1:123456789012:execution:HelloWorld:test-name",
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld",
    "name": "test-name",
    "status": "SUCCEEDED",
    "startDate": "2024-11-09T19:09:53.368000+09:00",
    "stopDate": "2024-11-09T19:09:53.399000+09:00",
    "input": "{\"who\": \"AWS Lambda\"}",
    "inputDetails": {
        "included": true
    },
    "output": "\"Hello from AWS Lambda\"",
    "outputDetails": {
        "included": true
    }
}

OKですね。

AWS Step FunctionsのAWS Lambdaのループのチュートリアルを試す

次は、AWS Step FunctionsとAWS Lambda関数でループを試してみます。

Step Functions で Lambda 関数を使用してループを反復する - AWS Step Functions

まずはAWS SAMプロジェクトを作成。

$ sam init --name sfn-local-loop-lambda --runtime python3.10 --app-template hello-world --package-type Zip --no-tracing --no-application-insights --structured-logging
$ cd sfn-local-loop-lambda

AWS Lambda関数は、こうしました。

hello_world/app.py

def lambda_handler(event, context):
    iterator = event["iterator"]
    index = iterator["index"]
    step = iterator["step"]
    count = iterator["count"]

    print(f"index = {index}, step = {step}, count = {count}, event = {event}")

    index = index + step

    return {
        "index": index,
        "step": step,
        "count": count,
        "continue": count > index
    }

AWS SAMのAWS Lambda環境と、AWS Step Functions Localを起動。

$ sam local start-lambda


$ java -jar StepFunctionsLocal.jar --lambda-endpoint http://localhost:3001

ステートマシンを作成します。定義はこちらです。

{
    "Comment": "Iterator State Machine Example",
    "StartAt": "ConfigureCount",
    "States": {
        
        "ConfigureCount": {
            "Type": "Pass",
            "Result": {
                "count": 10,
                "index": 0,
                "step": 1
            },
            "ResultPath": "$.iterator",
            "Next": "Iterator"
        },
        "Iterator": {
            "Type": "Task",
            "Resource": "arn:aws:lambda:us-east-1:123456789012:function:Iterate",
            "ResultPath": "$.iterator",
            "Next": "IsCountReached"
        },
        "IsCountReached": {
            "Type": "Choice",
            "Choices": [
                {
                    "Variable": "$.iterator.continue",
                    "BooleanEquals": true,
                    "Next": "ExampleWork"
                }
            ],
            "Default": "Done"
        },
        "ExampleWork": {
            "Comment": "Your application logic, to run a specific number of times",
            "Type": "Pass",
            "Result": {
              "success": true
            },
            "ResultPath": "$.result",
            "Next": "Iterator"
        },
        "Done": {
            "Type": "Pass",
            "End": true
          
        }
    }
}

ここまでとだいぶ定義が違います。

スタートになるステートは「ConfigureCount」のようです。

    "StartAt": "ConfigureCount",

こちらですね。

        "ConfigureCount": {
            "Type": "Pass",
            "Result": {
                "count": 10,
                "index": 0,
                "step": 1
            },
            "ResultPath": "$.iterator",
            "Next": "Iterator"
        },

TypePassのステートは、なにもせずに入力を出力に渡すステートのようです。

ワークフロー状態を渡す - AWS Step Functions

Resultはこのステートの出力です。

            "Result": {
                "count": 10,
                "index": 0,
                "step": 1
            },

これをResultPathで出力位置を指定します。

            "ResultPath": "$.iterator",

つまり、このステートの出力は以下になるというわけですね。作成したAWS Lambda関数が入力として期待している構造になっています。

{
  "iterator": {
    "count": 10,
    "index": 0,
    "step": 1
  }
}

Step Functions での入力と出力の処理 - AWS Step Functions

NextIteratorなので、次はIteratorのステート定義を見てみましょう。

こちらはTaskステートです。先ほど作成したAWS Lambdaを呼び出すようにARNを調整すればよさそうですね。

        "Iterator": {
            "Type": "Task",
            "Resource": "arn:aws:lambda:us-east-1:123456789012:function:Iterate",
            "ResultPath": "$.iterator",
            "Next": "IsCountReached"
        },

ResultPathでは、関数の結果を指定のパスに入れます。つまり、こうなるわけですね。

{
  "iterator": {
    "count": 10,
    "index": 0,
    "step": 1
  }
}

やっぱり作成したAWS Lambda関数が期待する構造になっています。

NextIsCountReachedです。

        "IsCountReached": {
            "Type": "Choice",
            "Choices": [
                {
                    "Variable": "$.iterator.continue",
                    "BooleanEquals": true,
                    "Next": "ExampleWork"
                }
            ],
            "Default": "Done"
        },

今度はChoiceというステートになりました。これは、条件によって分岐を行うステートのようですね。

選択ワークフローの状態 - AWS Step Functions

こちらを見ると、$.iterator.continueの値がtrueであればExampleWorkステートに進むようです。

            "Choices": [
                {
                    "Variable": "$.iterator.continue",
                    "BooleanEquals": true,
                    "Next": "ExampleWork"
                }
            ],

DefaultDoneになっていて、Choicesの条件に一致しなかった場合はこちらの実行に移ります。

        "Done": {
            "Type": "Pass",
            "End": true
          
        }

こちらはPassステートで終了ですね。

ExampleWorkステートはこちらもPassステートで、Iteratorステートに遷移します。

        "ExampleWork": {
            "Comment": "Your application logic, to run a specific number of times",
            "Type": "Pass",
            "Result": {
              "success": true
            },
            "ResultPath": "$.result",
            "Next": "Iterator"
        },

つまり、これでループを行っているわけですね。

では、AWS SAMでAWS Lambda環境と、AWS Step Functions Localを起動します。

$ sam local start-lambda


$ java -jar StepFunctionsLocal.jar --lambda-endpoint http://localhost:3001

ステートマシンを作成。

$ aws --endpoint http://localhost:8083 stepfunctions create-state-machine --definition "{\
    \"Comment\": \"Iterator State Machine Example\",\
    \"StartAt\": \"ConfigureCount\",\
    \"States\": {\
        \
        \"ConfigureCount\": {\
            \"Type\": \"Pass\",\
            \"Result\": {\
                \"count\": 10,\
                \"index\": 0,\
                \"step\": 1\
            },\
            \"ResultPath\": \"$.iterator\",\
            \"Next\": \"Iterator\"\
        },\
        \"Iterator\": {\
            \"Type\": \"Task\",\
            \"Resource\": \"arn:aws:lambda:us-east-1:123456789012:function:HelloWorldFunction\",\
            \"ResultPath\": \"$.iterator\",\
            \"Next\": \"IsCountReached\"\
        },\
        \"IsCountReached\": {\
            \"Type\": \"Choice\",\
            \"Choices\": [\
                {\
                    \"Variable\": \"$.iterator.continue\",\
                    \"BooleanEquals\": true,\
                    \"Next\": \"ExampleWork\"\
                }\
            ],\
            \"Default\": \"Done\"\
        },\
        \"ExampleWork\": {\
            \"Comment\": \"Your application logic, to run a specific number of times\",\
            \"Type\": \"Pass\",\
            \"Result\": {\
              \"success\": true\
            },\
            \"ResultPath\": \"$.result\",\
            \"Next\": \"Iterator\"\
        },\
        \"Done\": {\
            \"Type\": \"Pass\",\
            \"End\": true\
          
        }\
    }\
}" --name "Loop" --role-arn "arn:aws:iam::012345678901:role/DummyRole"

AWS Lambda関数のARNはHelloWorldFunctionに合わせています。

        \"Iterator\": {\
            \"Type\": \"Task\",\
            \"Resource\": \"arn:aws:lambda:us-east-1:123456789012:function:HelloWorldFunction\",\
            \"ResultPath\": \"$.iterator\",\
            \"Next\": \"IsCountReached\"\
        },\

作成したステートマシンのARN。

{
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:Loop",
    "creationDate": "2024-11-09T20:49:08.180000+09:00"
}

ステートマシンを実行します。

$ aws --endpoint http://localhost:8083 stepfunctions start-execution --state-machine arn:aws:states:us-east-1:123456789012:stateMachine:Loop --name loop
{
    "executionArn": "arn:aws:states:us-east-1:123456789012:execution:Loop:loop",
    "startDate": "2024-11-09T20:51:18.239000+09:00"
}

この時のAWS SAM側のログ。AWS Lambdaの実行ログですね。

Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
index = 0, step = 1, count = 10, event = {'iterator': {'count': 10, 'index': 0, 'step': 1}}
END RequestId: eca096a4-56e0-4f08-a14b-b1b54c814ee2
REPORT RequestId: eca096a4-56e0-4f08-a14b-b1b54c814ee2  Init Duration: 0.05 ms  Duration: 52.59 ms      Billed Duration: 53 ms  Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: d4fb2a1b-6e96-4464-9f23-854dbe5ec213 Version: $LATEST
index = 1, step = 1, count = 10, event = {'iterator': {'index': 1, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: 35f21479-50c7-4329-b446-565303fb48a0
REPORT RequestId: 35f21479-50c7-4329-b446-565303fb48a0  Duration: 1.33 ms       Billed Duration: 2 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: 1d3a97af-6df5-4693-b97b-666e461c4183 Version: $LATEST
index = 2, step = 1, count = 10, event = {'iterator': {'index': 2, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: d79f784a-e872-40ff-9a46-49e281dc53c3
REPORT RequestId: d79f784a-e872-40ff-9a46-49e281dc53c3  Duration: 1.27 ms       Billed Duration: 2 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: 53d19d9c-9dae-41e5-9243-84a3c96cd418 Version: $LATEST
index = 3, step = 1, count = 10, event = {'iterator': {'index': 3, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: c117cc83-dfb7-4bb6-8092-776ac05ab64c
REPORT RequestId: c117cc83-dfb7-4bb6-8092-776ac05ab64c  Duration: 0.93 ms       Billed Duration: 1 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: 4be613d6-1ffd-400c-951d-00a32603dbcf Version: $LATEST
index = 4, step = 1, count = 10, event = {'iterator': {'index': 4, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: 140797b6-8754-42e1-be5d-81b049f1f34a
REPORT RequestId: 140797b6-8754-42e1-be5d-81b049f1f34a  Duration: 0.94 ms       Billed Duration: 1 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: b96f049a-360d-4382-a2b9-9415f5423e02 Version: $LATEST
index = 5, step = 1, count = 10, event = {'iterator': {'index': 5, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: 634cf741-c6d9-4908-b79d-36661b0a4563
REPORT RequestId: 634cf741-c6d9-4908-b79d-36661b0a4563  Duration: 0.90 ms       Billed Duration: 1 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: 93121bb9-6d50-429c-9483-cb05041aca1e Version: $LATEST
index = 6, step = 1, count = 10, event = {'iterator': {'index': 6, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: e7434c8b-efd8-4b45-b987-34c1f77b0b08
REPORT RequestId: e7434c8b-efd8-4b45-b987-34c1f77b0b08  Duration: 0.86 ms       Billed Duration: 1 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: c84f02a3-a8a1-47e8-b64a-b164e731d9d7 Version: $LATEST
index = 7, step = 1, count = 10, event = {'iterator': {'index': 7, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: bea5aae8-8018-4047-9895-6b44c34c6ae7
REPORT RequestId: bea5aae8-8018-4047-9895-6b44c34c6ae7  Duration: 1.07 ms       Billed Duration: 2 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: dbbd2fcb-37fa-4ff3-b241-283a22289a8b Version: $LATEST
index = 8, step = 1, count = 10, event = {'iterator': {'index': 8, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: c83ef130-f1f8-4713-be6d-1f5a86ac7721
REPORT RequestId: c83ef130-f1f8-4713-be6d-1f5a86ac7721  Duration: 0.86 ms       Billed Duration: 1 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.10)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: 7b47866f-4986-41b1-8cfc-b143f17c6d49 Version: $LATEST
index = 9, step = 1, count = 10, event = {'iterator': {'index': 9, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}
END RequestId: 27fb39f7-169e-4256-9ff9-9a4ecbd99584
REPORT RequestId: 27fb39f7-169e-4256-9ff9-9a4ecbd99584  Duration: 0.92 ms       Billed Duration: 1 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

2024-11-09 20:51:18 127.0.0.1 - - [09/Nov/2024 20:51:18] "POST /2015-03-31/functions/HelloWorldFunction/invocations HTTP/1.1" 200 -

このあたりを見ると、実行の様子やExampleWorkステートのResult.successが一緒に入力に渡ってきていることが確認できますね。

# 1回目
index = 0, step = 1, count = 10, event = {'iterator': {'count': 10, 'index': 0, 'step': 1}}


# 9回目
index = 8, step = 1, count = 10, event = {'iterator': {'index': 8, 'step': 1, 'count': 10, 'continue': True}, 'result': {'success': True}}

実行結果の確認。

$ aws --endpoint http://localhost:8083 stepfunctions describe-execution --execution-arn arn:aws:states:us-east-1:123456789012:execution:Loop:loop
{
    "executionArn": "arn:aws:states:us-east-1:123456789012:execution:Loop:loop",
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:Loop",
    "name": "loop",
    "status": "SUCCEEDED",
    "startDate": "2024-11-09T20:51:18.239000+09:00",
    "stopDate": "2024-11-09T20:51:18.821000+09:00",
    "input": "{}",
    "inputDetails": {
        "included": true
    },
    "output": "{\"iterator\":{\"index\":10,\"step\":1,\"count\":10,\"continue\":false},\"result\":{\"success\":true}}",
    "outputDetails": {
        "included": true
    }
}

OKですね。

おわりに

AWS Step Functions LocalとAWS SAMを使って、AWS Lambdaを使ったチュートリアルを動かしてみました。

前回はただ実行しただけだったので、ステートマシンの定義の基礎みたいなところは学べたかなと思います。