LocalStackとSAM LocalでStep Functions(lambda)をローカル環境で実行してみる

# はじめに
Step Functionsの開発を行っていたときに、ローカル上で実行確認が取りたいなと思ったのですが、そのための方法がパッと出てこず少し困ったので、その方法をまとめておきます。

### 目的
ローカル環境でStep Functionsの実行確認を行う。
そのために、SAM Localで実行しているlambdaをLocalStackのStep Functionsから呼び出し実行する。

# 実行環境
### 環境
- macOS Catalina 10.15.7
- aws-cli 1.18.185
- sam-cli 1.12.0

### 事前準備
ここでは詳細に書きませんが、aws-cliとsam-cliが実行できる環境が必要です。
また、LocalStackを使用するので、そのためのcredential情報をセットしてください。

```shell
$ vi ~/.aws/credentials
```

```/.aws/credentials
[localstack]
aws_access_key_id = dummy
aws_secret_access_key = dummy
```

```shell
$ vi ~/.aws/config
```

```/.aws/credentials
[profile localstack]
region = ap-northeast-1
output = text
```

# やってみる
## LocalStackの準備
まずはLocalStackのコンテナを立てていきます。
[GitHub](https://github.com/localstack/localstack)のページに幾つかの方法が記載されていますが 今回はdocker-composeで行っていきます。

```shell
$ git clone https://github.com/atlassian/localstack.git
$ cd localstack
```

この時、後々SAMで立てたlambdaをLocalStackのStep Functionsから呼び出す必要があるので、それらを呼び出せるようにlambdaのエンドポイントを指定する環境変数をdocker-compose.ymlに追記しておきます

```docker-compose.yml
version: '2.1'

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack
    network_mode: bridge
    ports:
      - "4566:4566"
      - "4571:4571"
      - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
    environment:
      - SERVICES=${SERVICES- }
      - DEBUG=${DEBUG- }
      - DATA_DIR=${DATA_DIR- }
      - PORT_WEB_UI=${PORT_WEB_UI- }
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
      - LAMBDA_ENDPOINT=http://host.docker.internal:3001               ##追記
      - STEPFUNCTIONS_LAMBDA_ENDPOINT=http://host.docker.internal:3001 ##追記
      - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
      - DOCKER_HOST=unix:///var/run/docker.sock
      - HOST_TMP_FOLDER=${TMPDIR}
    volumes:
      - "${TMPDIR:-/tmp/localstack}:/tmp/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"
```

追記したら、コンテナを立てておきましょう

```shell
$ docker-compose up -d
```

## lambda関数の作成

今回はStep Functionsでlambdaを実行できることを確認したいだけなので、lambda関数自体は非常に単純なものにします。
ということで、以下の2つの関数を作成していきます。
- HelloWorldFunction : `{入力パラメータ}+HelloWorld!!`を出力する関数
- GoodByeFunction : `{入力パラメータ}+GoodBye!!`を出力する関数

### sam initでlambda関数を作成する
それでは適当なディレクトリにlambda関数を作成していきます。
`sam init`で作成していきます。ダイアログの通りですが、`runtime`はpython3.6、`sfn-test`というプロジェクト名で作成しています。

```shell
$ sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Which runtime would you like to use?
        1 - nodejs12.x
        2 - python3.8
        3 - ruby2.7
        4 - go1.x
        5 - java11
        6 - dotnetcore3.1
        7 - nodejs10.x
        8 - python3.7
        9 - python3.6
        10 - python2.7
        11 - ruby2.5
        12 - java8.al2
        13 - java8
        14 - dotnetcore2.1
Runtime: 9

Project name [sam-app]: sfn-test

Cloning app templates from https://github.com/awslabs/aws-sam-cli-app-templates.git

AWS quick start application templates:
        1 - Hello World Example
        2 - EventBridge Hello World
        3 - EventBridge App from scratch (100+ Event Schemas)
        4 - Step Functions Sample App (Stock Trader)
Template selection: 1

-----------------------
Generating application:
-----------------------
Name: sfn-test
Runtime: python3.6
Dependency Manager: pip
Application Template: hello-world
Output Directory: .

Next steps can be found in the README file at ./sfn-test/README.md
```
これで必要なファイル群が作成されました。
ディレクトリ構成は以下の通りです。

```shell
$ tree .
.
└── sfn-test
    ├── README.md
    ├── __init__.py
    ├── events
    │   └── event.json
    ├── hello_world
    │   ├── __init__.py
    │   ├── app.py
    │   └── requirements.txt
    ├── template.yaml
    └── tests
        ├── __init__.py
        └── unit
            ├── __init__.py
            └── test_handler.py
```

### lambdaを実行してみる
この状態で一度、lambda関数をsamで実行してみます。

```shell
$ cd sfn-test
$ sam local invoke --profile localstack
{"statusCode":200,"body":"{\"message\": \"hello world\"}"}
```
デフォルト値の出力がされました。問題なさそうです。

### lambda関数の編集
今回は、入力を受け取ってから`{入力}+HelloWorld!!`・`{入力}+GoodBye!!`を返すことが目的なので、lambda関数の中身を編集しておきます。
まずはHelloWorldFunctionを作成します。　以下がapp.py中身です。
入力された値を受け取って、HelloWorld!!と返すだけの単純なものであることが分かるかと思います。

```app.py
def lambda_handler(event, context):
    print(str(event["input"]) + " HelloWorld!!")
    return event
```

### 2つ目のlambda関数を作成
2つ目の`{入力}+GoodBye!!`の方のlambda関数も同様に作成しておきます。
今回は、同じディレクトリにもう一つ関数を作成しておきます。

```shell
~/sfn-test
$ tree .
.
├── README.md
├── __init__.py
├── events
│   └── event.json
├── good_bye        ##追加
│   ├── __init__.py ##追加
│   └── app.py      ##追加
├── hello_world
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── template.yaml
└── tests
    ├── __init__.py
    └── unit
        ├── __init__.py
        └── test_handler.py
```

こちらのlambdaも入力を受け取って、GoodBye!!と返すだけの単純なものです。

```good_bye/app.py
def lambda_handler(event, context):
    print(str(event["input"]) + " GoodBye!!")
    return event
```

### template.yamlの編集
先ほど作成した2つの関数を呼び出せるように、template.yamlを修正していきます。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sfn-test

  Sample SAM Template for sfn-test

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.6

  GoodByeFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: good_bye/
      Handler: app.lambda_handler
      Runtime: python3.6
```

`Resources`内に、`HelloWorldFunction`と`GoodByeFunction`を定義します。
`CodeUri`、`Handler`で関数のパスを指定しているので、ここを工夫すれば別ディレクトリに作成してある関数も呼び出すことができます。

### start-lambda
ここまででlambdaの準備はOKです！
Step Functionsから呼び出せるように、lambdaを実行しておきましょう。

```shell
$ sam local start-lambda --profile localstack

Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2021-01-17 14:55:11  * Running on http://127.0.0.1:3001/ (Press CTRL+C to quit)
```

エンドポイントはデフォルトで`http://127.0.0.1:3001/`なので、Step Functionsからもこのエンドポイントで呼び出すことができます。

## ステートマシンの作成
lambdaの準備ができたので、次はStep Functionsのステートマシンを作成していきます。
定義ファイルを`define.json`という名前で作成します。
lambdaを実行しているのとは別のターミナルを開いて、以下を実行していきましょう。

```shell
$ touch define.json
```

```define.json
{
    "StartAt": "HelloWorld",
    "States": {
        "HelloWorld": {
            "Type": "Task",
            "Resource": "arn:aws:lambda:us-east-1:123456789012:function:HelloWorldFunction",
            "Parameters": {
                "input": "hogehoge"
            },
        "Next": "GoodBye"
        },
        "GoodBye": {
            "Type": "Task",
            "Resource": "arn:aws:lambda:us-east-1:123456789012:function:GoodByeFunction",
            "End": true
        }
    }
}
```

ResourceのところにlambdaのARNを指定することで呼び出すことができます。ローカルで呼び出す場合はFunction名を正しくセットしておけば問題ないはずです。

定義内容は単純で、HelloWorldFunctionの後にGoodByeFunctionを呼び出すだけです。lambda内で出力することになる`input`というパラメータには`hogehoge`という文字列を渡しています。

それでは、aws-cliでLocalStackのコンテナ上にStep Functionsを作成していきます。

```shell
$ aws stepfunctions create-state-machine \
  --name TestState \
  --definition file://define.json \
  --role-arn "arn:aws:iam::000000000000:role/DummyRole" \
  --endpoint http://localhost:4566
```

`TestState`という名前のステートマシンを、先ほど作成した定義ファイル通りにLocalStackのエンドポイントである`4566`のポートで作成しています。
すると、以下のように出力されるはずです。

```shell
{
    "stateMachineArn": "arn:aws:states:us-east-1:000000000000:stateMachine:TestState",
    "creationDate": 1610863591.321
}
```

これでStep FunctionsのARNが作成されました。あとはこれを呼び出せば実行できるはずです！

### 実行してみる
今回は、aws-cliで先ほどのステートマシンを実行してみます。

```shell
$ aws stepfunctions start-execution \
  --state-machine arn:aws:states:us-east-1:000000000000:stateMachine:TestState \
  --endpoint http://localhost:4566
```

すると、lambdaを実行しているターミナルが動き、結果が出力されていくはずです。

```shell
Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2021-01-17 14:55:11  * Running on http://127.0.0.1:3001/ (Press CTRL+C to quit)
Invoking app.lambda_handler (python3.6)
Skip pulling image and use local one: amazon/aws-sam-cli-emulation-image-python3.6:rapid-1.12.0.

Mounting /sfn-test/hello_world as /var/task:ro,delegated inside runtime container
・
・
・
hogehoge HelloWorld!!
・
・
・

Mounting /sfn-test/good_bye as /var/task:ro,delegated inside runtime container
・
・
・
hogehoge GoodBye!！
・
・
・
```

無事、2つのlambdaを実行できました！

ARNを呼び出しているだけなので、アプリケーションからSDKなどで呼び出すこともできます。
実際にはこの形で、アプリケーションからStep Functionsと連携が取れるかをローカル環境で確認するために利用できそうです。

# 最後に
ということで、LocalStackとSAM Localを利用して、Step Functions(lambda)のローカル環境での実行確認が行えました。
AWS上にデプロイする前に、実行確認が取りたい場合などには有効利用できそうですね。

本記事は以上です。
どなたかの参考になれば幸いです！