AWS SAM CLI for Localstackを利用してローカルでStep Functionsの動作確認をしてみました

2024.01.31

初めに

2023/11頃にAWS SAM CLIのリモート実行(remote invoke)でStep Functionsのステートマシンが実行できるようになりました。

こちらの対応はリモートだけとなり本記事執筆ではローカルモックは用意されていないためAWS SAM CLI単体ではローカルのテスト環境を用意することができません。

AWS公式のドキュメントではStep Functions Localを併用した方法が紹介されていますが、こちらはあくまでSAMは含まれるLambda関数のローカル実行を行うのみでステートマシンの実行は直接担わないものとなりステートマシンは自体はSAM外で実現する形となりそうです。

公式のツールではないですがLocalStackがオールインパッケージという感じで個人的には好んで使っていたので、どうにかしてSAM実行時のエンドポイントをこちらに向けられればやりようはあるのではないかと考えておりました。

AWS SAM CLIはAWS CLIと異なりENDPOINT_URLのような接続先を差し替えるオプションがないためその部分をどうにかしないといけないのですが、調べていたところLocalStack配下にaws-sam-cli-localというリポジトリが見つかりました。

確認してみるとAWS SAM CLIのラッパーツールで大雑把に読んでみる限り実行の向き先を自由に変更できるもののようです。
(デフォルトは本来の趣旨としてはlocalhost上のLocalStackだがAWS_ENDPOINT_URLの指定で別にもできそう)

ツールとしてはSAMの呼び出し周りをゴリゴリ書いて何とかしているものではなくでインストールされたSAMのモジュールを呼び出しているだけですので、大きく仕様が変わらない限りはこちら側のツールの対応を待つという必要もなく対応できそうというのも一つの魅力に思えます。
指定もaws-sam-cli>=1.1.0という形で緩く未指定でインストールすればその時点で最新のものが入ります。

実際にこちらを利用してローカル上でステートマシンを実行してみます。

LocalStack環境の立ち上げ

環境はdocker composeで立ち上げたいため公式のドキュメントをもとにdocker-compose.ymlを作成します。

docker-compose.yml

version: "3.8"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"
      - "127.0.0.1:4510-4559:4510-4559"
    volumes:
      # 今回特に永続化はしないので
      # - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

(しばらく前は個別に起動するサービスを指定したような気がしますがいつの間にか無くなった?)

特別な対応は必要なくdocker compose upで環境を立ち上げます。

% docker compose up -d
[+] Running 1/1
 ✔ Container localstack-main  Started

aws-sam-cli-localのインストール

pip経由でインストール可能なためそちらでインストールします。
--versionを確認してみると含まれるAWS SAM CLI側のバージョンが表示されるので本当にオプションを含めよしなに中継しているくらいになりそうです。

% pip install aws-sam-cli-local
...
% samlocal --version
SAM CLI, version 1.108.0
# aws-sam-cli-local自体はv1.68.0
% pip freeze | grep aws-sam-cli
aws-sam-cli==1.108.0
aws-sam-cli-local==1.68.0

ローカルスタック環境を対ししてオプションなしでデプロイやリモート実行コマンドを動かしてみるとデフォルトのローカルスタックのエンドポイントに繋がらないエラーが出るので、いい感じにコマンドを継承しつつ向き先だけ変わっていそうです。

% samlocal deploy
Error: Could not connect to the endpoint URL: "http://localhost:4566/"
% samlocal remote invoke
Error: Could not connect to the endpoint URL: "http://localhost:4566/"

デプロイ

今回は以下のサンプルテンプレートを利用します(sam initの4番目のテンプレートです)。

処理としては主制御をStep Functions側で行いつつLambda関数で処理し引き渡された値をDynamoDBのデータを書き込むという感じです。

趣旨としては全く別の記事ですがStep Functionsのリモート実行対応時の執筆記事でこのテンプレートを利用したリソースの動作を確認しているので一度みていただくのが処理のイメージはつきやすいかなと思います。

samlocal deployでデプロイを行います。

% samlocal deploy
	Creating the required resources...
	Successfully created!

		Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-ef0e7958
		A different default S3 bucket can be set in samconfig.toml
		Or by specifying --s3-bucket explicitly.
	Uploading to 45bd9a8dff25082b8f6d078abb1b3581  2839 / 2839  (100.00%)
	Uploading to 0966869826df4e1eca6492b5d40629b9  675 / 675  (100.00%)
	Uploading to e7a567f676ae4056e7adbd4e84881e50  913 / 913  (100.00%)
	Uploading to 992cd4f12a9f3b4c5c38e305bb4d821d  912 / 912  (100.00%)

	Deploying with following values
	===============================
	Stack name                   : sam-app-sfn
	Region                       : ap-northeast-1
	Confirm changeset            : True
	Disable rollback             : False
	Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-ef0e7958
	Capabilities                 : ["CAPABILITY_IAM"]
	Parameter overrides          : {}
	Signing Profiles             : {}
...
CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 StockTradingStateMachineArn
Description         Stock Trading State machine ARN
Value               arn:aws:states:ap-northeast-1:000000000000:stateMachine:sam-app-sfn-StockTradingStateMachine-0c6f596a

Key                 StockTradingStateMachineRoleArn
Description         IAM Role created for Stock Trading State machine based on the specified SAM Policy Templates
Value               arn:aws:iam::000000000000:role/sam-app-sfn-StockTradingStateMachine-386e1e6e
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Outputsの値からローカルにデプロイされていそうな雰囲気は感じますが念の為AWS CLIで明示的にLocalStackのエンドポイントを指定してリソースのデプロイを確認してみます。

% aws stepfunctions list-state-machines --endpoint-url=http://localhost:4566/
{
    "stateMachines": [
        {
            "stateMachineArn": "arn:aws:states:ap-northeast-1:000000000000:stateMachine:sam-app-sfn-StockTradingStateMachine-0c6f596a",
            "name": "sam-app-sfn-StockTradingStateMachine-0c6f596a",
            "type": "STANDARD",
            "creationDate": "2024-01-31T14:47:37.395362+09:00"
        }
    ]
}
% aws lambda list-functions --endpoint-url=http://localhost:4566/
{
    "Functions": [
        {
            "FunctionName": "sam-app-sfn-StockCheckerFunction-1b089825",
            "FunctionArn": "arn:aws:lambda:ap-northeast-1:000000000000:function:sam-app-sfn-StockCheckerFunction-1b089825",
            "Runtime": "python3.10",
            "Role": "arn:aws:iam::000000000000:role/sam-app-sfn-StockCheckerFunctionRole-c72096c3",
            "Handler": "app.lambda_handler",
            "CodeSize": 675,
            "Description": "",
            "Timeout": 3,
            "MemorySize": 128,
            "LastModified": "2024-01-31T05:46:51.288232+0000",
            "CodeSha256": "6+F5S7OsQ0/DzPgzsv3Qq3W2gEoE1QzO4BJZEFVfgf0=",
            "Version": "$LATEST",
            "TracingConfig": {
                "Mode": "PassThrough"
            },
...
}
% aws dynamodb list-tables --endpoint-url=http://localhost:4566/
{
    "TableNames": [
        "sam-app-sfn-TransactionTable-06f89e94"
    ]
}

問題なくLocalStack環境に乗っていそうです。

実行

実行はSAM内部のものではなくLocalStack側のリソースを実行するのでremote invokeとなります。

コマンド的にはリモート実行、実際に実行されるのはローカル言葉尻で笑ってしまうだけで済めば良いですがsamsamlocalを間違えて実行すると実際の環境上のものが実行されることもありますのでご注意ください。

% samlocal remote invoke StockTradingStateMachine
Invoking Step Function StockTradingStateMachine
{}%

DyanamoDBにデータを入っていることで内部のステートマシンが正常に稼働していることを確認します。

現時点ではDynamoDBへのクエリまでSAMはサポートしていないのでここはAWS CLIで行います。

% aws dynamodb scan --table-name sam-app-sfn-TransactionTable-06f89e94 --endpoint-url=http://localhost:4566/
{
    "Items": [
        {
            "Quantity": {
                "N": "1"
            },
            "Type": {
                "S": "buy"
            },
            "Id": {
                "S": "64859cd7-2e6b-4325-82a0-86fef6773227"
            },
            "Price": {
                "N": "49"
            },
            "Timestamp": {
                "S": "2024-01-31T05:52:25.781016"
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1,
    "ConsumedCapacity": null
}

処理されたデータが格納されていることができました。

終わりに

LocalStackを併用することで実質的にSAMを使ってローカルでStep Functionsのテストを実行できることを確認することができました。

準備としても事前にdocker-compose.ymlを事前用意しておけば立ち上げもシンプルで、対応していればローカルで完結するのでCIでインテグレーションテストをしたいけどできれば実際の環境を使わず閉じた環境で実現することもできるので良さそうな雰囲気を感じます。

sam localにStep Functionsを利用できるようになってもLocalStackとしてはエミュレートしているサービスも広いことを考えると手法としては息の長いようなものではないかなと思います。

補足: LocalStack対応サービスについて

LocalStackは全てのサービスをサポートしているわけではなく、また対応していても機能によっては無料プランでは利用できないものが含まれるためご注意ください。

対応している機能やカバレッジ等は以下のページに記載がございますのでごちらもご参照ください。