0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS Emulator】AWS公式のローカルエミュレーターで、本格的なサーバーレスはどこまで動く?motoやFlociなど5つのOSSとも比べてみた

0
Posted at

はじめに

こんばんは、mirukyです。

2026年11月11日、AWSから公式のローカルエミュレーター、AWS Emulatorが出ました。AWSが自分で作っていて、アカウントも料金もいらないとなると、さすがに試したくなります。

今回は、API GatewayからStep Functions、S3まで経由する注文処理をAWS Emulatorに載せ、motoやFlociなど5つのOSSと、どこまで同じように動くかを比べます。検証はすべて手元のDockerで行ったので、AWSの料金は0円です。

目次

  1. AWS Emulatorって、どんなもの?
  2. LocalStackの今と、5つのOSS
  3. 注文処理を、手元で通しで動かしたい
  4. AWS Emulatorを動かしましょう
  5. 注文処理をAWS Emulatorに載せてみた
  6. 5つのOSSと比べる
  7. 結局、どれを選ぶ?

1. AWS Emulatorって、どんなもの?

1-1. AWSのAPIを、手元の1つのプロセスで受ける

AWS CLIで aws s3 mb を打つと、普段はリクエストがAWSのS3へ飛んでいきます。この行き先を環境変数で手元に向け、AWSの代わりに応答を返すプログラムが、AWS Emulatorです。作ったバケットやテーブルは手元のメモリに置かれ、エミュレーターを止めるとすべて消えます。

中では1つのプロセスが署名の確認からLambdaの実行までを受け持っています。

AWS CLIやSDKが署名して送ったリクエストを、AWS Emulatorの署名の確認が受け、APIモデルから生成されたサービスの処理へ渡す。サービスの処理はメモリ上のリソースを作成・更新し、LambdaのInvokeでは同じプロセスの中のRustPythonとQuickJSで関数を実行する。関数の中のSDK呼び出しも同じエミュレーターのリソースへ届く

リクエストは自己署名の証明書を使った https://127.0.0.1:9297 で受け、起動のたびに作られる専用の鍵で署名していないものは断ります。サービスごとの処理はSDKと同じAWSのAPIモデルから作られていて、リポジトリにはサービスごとのコードが400以上並んでいます。まだ作られていない操作を呼ぶと、返ってくるのはHTTP 501の NotImplemented です。

項目 内容
提供開始 2026年11月11日(一般提供)
ライセンス CC0 1.0(vendor/ 以下はApache 2.0)
実装 Rust
動かし方 LinuxはRPMとDebianのパッケージ。macOSとWindowsは公式のDockerfileで自分でイメージを作る
使用状況の送信 既定でオン。--telemetry off か EMULATOR_TELEMETRY=off で止まる

ドキュメントの制約の節にはIAMのポリシーをAWSと同じようには評価しない、と書かれています。どのアカウントIDとリージョンで送っても受け付けるものの、リソースをアカウントやリージョンごとに区別しないので、1つのエミュレーターには1つのアカウントとリージョンで送る決まりです。AWSとの答え合わせはグローバルなサービスを除いて米国西部(オレゴン)リージョンで行われていて、IAMの扱いがどこまでAWSに近いかは5章で確かめます。

1-2. コードを書いているのは、AIのエージェント

AWS Emulatorのコードは、AIのエージェントが書いて保守しています。AWSのブログによると、Amazon Bedrock AgentCore、AWS CodePipeline、AWS CodeCommitの上でエージェントが作り続けているそうです。GitHubのリポジトリはプルリクエストを受け付けておらず、違いを見つけたらIssueで知らせます。

リポジトリを開くと、issues/ の下に、まだ動かないもの(things-that-do-not-work/)と直ったもの(things-that-now-work/)が1件ずつファイルになって並んでいて、なかなか生々しいです。数えてみると、まだ動かないものが6,500件あまり、直ったものはその倍近くありました。リリースも1日に何度も出る日があり、今回試したのは11月11日に出たv1.0.322です。

AWSとの違いの大きさを測っているのが、再現度(fidelity)のテストです。AWSで正しさを確かめた小さなプログラムを流し、同じ結果になるかを数える仕組みで、合格数はリリースごとに公開されます。v1.0.322に添付された scores.json を集計すると、通信の形式を確かめるテストと振る舞いのテストを合わせて、4万件あまりのうち合格は半分強でした。今回使うサービスの振る舞いのテストを、そのサービス単独のものと、ほかのサービスとまたがるものとで比べてみます。

AWS Emulator v1.0.322の振る舞いのテストの合格率。そのサービスだけのテストでは、DynamoDB 82%、SQS 94%、S3 83%、EventBridge 83%、SNS 75%、Step Functions 73%、Lambda 48%、API Gateway v2 75%、CloudFormation 78%。ほかのサービスとまたがるテストでは、DynamoDB 54%、SQS 62%、S3 51%、EventBridge 67%、SNS 66%、Step Functions 66%、Lambda 54%、API Gateway v2 32%、CloudFormation 24%

単独のテストでは多くのサービスが7割を超えているのに、ほかのサービスとまたがると、ほとんどが半分から6割台に下がります。いちばん下がったのはCloudFormationで、78%から24%まで落ちていました。ドキュメントでも、リソースを作る・取得する・消す側の操作(コントロールプレーン)がいちばんAWSに近く、データを流す側の操作やCloudFormationの対応はサービスによって差がある、と説明されています。サービスをいくつも経由する構成ほど、この差が結果に出そうです。

2. LocalStackの今と、5つのOSS

2-1. LocalStackは認証トークンが必須に

ローカルでAWSを動かすOSSといえば、長くLocalStackでした。そのLocalStackは2026年3月23日の2026.03.0で無料版と有料版のイメージを1つにまとめ、起動に認証トークンを求めるようになりました。GitHubの localstack/localstack も同じ日にアーカイブされています。非商用なら無料のHobbyプランがありますが、アカウントの登録は必要です。

手元で localstack/localstack:latest(2026.10.7)をトークンなしで起動すると、資格情報が見つからないと言って終了しました。

LocalStackの最新イメージをトークンなしで起動した端末。No credentials were found in the environment. Please set the LOCALSTACK_AUTH_TOKEN variable と表示されて終了している

今回はアカウントを作らずに比べたかったので、LocalStackは外し、アカウントなしで動く5つのOSSを相手にしました。

2-2. 比べる5つのOSS

LocalStackの変更の前後から、置き換えをうたうOSSがいくつも公開されています。motoだけは2013年からある古株で、テストのプロセスの中でAWSを差し替える使い方が中心です。ほかの4つは2026年に入ってから公開されたもので、LocalStackと同じ4566番で待ち受けます。

ライセンス 実装 既定のポート 版
AWS Emulator CC0-1.0 Rust 9297(HTTPS) 1.0.322
Floci MIT Java(Quarkus) 4566 2.4.0
MiniStack MIT Python 4566 1.5.40
fakecloud AGPL-3.0 Rust 4566 0.51.0
moto Apache-2.0 Python 5000 5.2.6.dev(Docker の latest)
kumo MIT Go 4566 0.33.1

表の中でいちばん違いが出たのは、Lambdaのコードをどこで動かすかでした。同じ python3.13 の関数でも、エミュレーターによって動く場所が変わります。

AWS Emulatorは同じプロセスの中のRustPythonとQuickJSで関数を実行し、代わりのboto3でエミュレーターを直接呼ぶ。Floci、fakecloud、motoはdocker.sock経由でLambdaのイメージのコンテナを起動し、関数の中のSDK呼び出しがエミュレーターへ戻る。kumoは関数を実行せず、手元で起動した実行役がRuntime APIの次の呼び出しを取りに行き、結果を返す

FlociとfakecloudはDockerのソケットを借りて、AWS公式のLambdaのイメージでコンテナを起動します。motoも同じ仕組みで、使うイメージはコミュニティ製の ghcr.io/shogo82148/lambda-python でした。本物に近い反面、Dockerが必須です。AWS EmulatorはDockerを使わず、自分のプロセスの中で関数を動かします。kumoは関数を動かさず、Runtime APIで呼び出しを渡すだけなので、受け取る側は自分で用意します。MiniStackは既定では自分のコンテナの中のPythonで動かし、設定でDockerにも切り替えられる作りです。

3. 注文処理を、手元で通しで動かしたい

3-1. 注文を受けてから、領収書を残すまで

ネットショップの注文処理を、デプロイする前に手元で通しで確かめたいとします。API Gatewayで注文を受け、DynamoDB Streams、EventBridge、Step Functionsを経て在庫の確保と決済をし、確定した注文には領収書をS3に保存する構成です。在庫切れや決済の失敗はStep FunctionsのCatchで拾い、注文を FAILED にして失敗用のキューへ送ります。

注文する画面からAPI Gateway、注文の受付のLambda、DynamoDBの注文テーブル、変更の通知のLambda、EventBridgeのカスタムバスへ進む。ルールでStep Functionsのステートマシンを起動し、在庫の確保、決済、CONFIRMEDへの更新、SNSの通知へ進む。在庫の確保と決済の失敗はCatchでFAILEDへの更新と失敗キューへ送る。SNSからSQSの領収書キュー、領収書の保存のLambda、S3の領収書バケットへ進む構成図

サービスからサービスへの受け渡しを多めにしてあります。1-2節のスコアでも、エミュレーターの差が出やすいのはそこだったからです。

構成はCloudFormationのテンプレート1本(30リソース)で書き、実際のAWSにもそのままデプロイできる書き方であることを cfn-lint で確かめました。DynamoDBのテーブルと、Step Functionsの在庫の確保のステートを抜き出します。

  OrdersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
      - AttributeName: orderId
        AttributeType: S
      KeySchema:
      - AttributeName: orderId
        KeyType: HASH
      # 追加された注文をストリームで変更の通知の関数へ渡す
      StreamSpecification:
        StreamViewType: NEW_IMAGE
"ReserveStock": {
  "Type": "Task",
  "Resource": "arn:aws:states:::lambda:invoke",
  "Parameters": { "FunctionName": "${ReserveStockFunction}", "Payload.$": "$" },
  "ResultSelector": { "result.$": "$.Payload" },
  "ResultPath": "$.reservestock",
  "Retry": [{ "ErrorEquals": ["TransientError"], "IntervalSeconds": 1, "MaxAttempts": 2, "BackoffRate": 2.0 }],
  "Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "MarkFailed" }],
  "Next": "ChargePayment"
}

注文の状態を CONFIRMED や FAILED に変えるステートは、Lambdaを挟まず、Step FunctionsからDynamoDBを直接更新する連携(arn:aws:states:::dynamodb:updateItem)にしました。このステートには "ResultPath": null を付けて、DynamoDBの応答を捨て、受け取った注文の情報をそのまま次のSNSの通知へ渡します。6章では、この1行で結果が変わるエミュレーターが出てきます。

3-2. 流す注文は4種類

成功の流れだけでなく、RetryとCatchまで動くかを確かめたいので、商品コードと数量を変えた4種類の注文を流します。

注文 在庫の確保 決済 期待する結果
BOOK-1を2個 成功 成功 CONFIRMED、S3に領収書
FLAKY-1を1個 1回目だけ一時的なエラー 成功 Retryで CONFIRMED、試行回数は2
SOLD-OUT-1を1個 在庫切れのエラー 呼ばれない Catchで FAILED、失敗用のキューへ
BOOK-2を20個 成功 数量の上限を超えてエラー Catchで FAILED、失敗用のキューへ

在庫の確保の関数は、試行回数を注文の項目に数えながら、商品コードに応じてエラーを出します。

import os, boto3
ddb = boto3.client("dynamodb", endpoint_url=os.environ.get("EMULATOR_ENDPOINT") or None)

class OutOfStock(Exception): pass
class TransientError(Exception): pass

def handler(event, context):
    # 試行回数を注文の項目に数え、FLAKY- の商品は1回目だけ一時的なエラーにする
    n = ddb.update_item(TableName=os.environ["TABLE"], Key={"orderId": {"S": event["orderId"]}},
                        UpdateExpression="ADD attempts :one", ExpressionAttributeValues={":one": {"N": "1"}},
                        ReturnValues="UPDATED_NEW")["Attributes"]["attempts"]["N"]
    if event["sku"].startswith("SOLD-OUT"):
        raise OutOfStock(event["sku"])
    if event["sku"].startswith("FLAKY") and int(n) < 2:
        raise TransientError(f"attempt {n}")
    return {"reserved": event["qty"], "attempts": int(n)}

EMULATOR_ENDPOINT は、関数の中の接続先をエミュレーターが差し替えてくれないときのための逃げ道です。結局、今回はどのエミュレーターでも空のままで動きました。

4. AWS Emulatorを動かしましょう

4-1. イメージのビルドと起動

macOS(Apple silicon)とcolimaのDocker 29.5.2で動かします。ビルド済みのイメージは配られていないので、公式のDockerfileから作ります。中身はAmazon Linux 2023の上にRPMを入れるだけの短いものです。Apple siliconではRPMのファイル名を aarch64 のものに替えます。

FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal
# Apple silicon では aarch64 の RPM を使う(Intel なら x86_64)
ADD https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator-aarch64.rpm /tmp/aws-emulator.rpm
RUN rpm -i /tmp/aws-emulator.rpm && rm /tmp/aws-emulator.rpm \
    && mkdir -p /home/emulator && chown 1000 /home/emulator
USER 1000
ENV HOME=/home/emulator
EXPOSE 9297
ENTRYPOINT ["aws-emulator", "-b", "0.0.0.0"]
# イメージを作る(2026-11-12 時点で展開後 1.63GB)
docker build -t aws-emulator .
# 鍵と証明書を手元と共有するディレクトリを作ってから起動する
mkdir -p ~/.aws/emulator
docker run --rm -d --name aws-emulator --memory=2g --memory-swap=2g -p 127.0.0.1:9297:9297 \
  -e HOME="$HOME" -v "$HOME/.aws/emulator:$HOME/.aws/emulator" \
  aws-emulator -p 9297 --telemetry off

ポートは -p 127.0.0.1:9297:9297 のように手元のアドレスに絞ります。-p 9297:9297 にすると、同じネットワークのほかのマシンからも呼べてしまうので、ドキュメントでもはっきり止められています。使用状況の送信は、今回は --telemetry off で止めました。

4-2. AWS CLIをエミュレーターへ向けましょう

起動すると、~/.aws/emulator/9297/ に、その回だけ使える鍵、SDK用の設定ファイル、証明書ができます。ドキュメントの手順どおりに環境変数を並べ、本物の資格情報は消しておきます。

# AWS CLI と SDK の接続先を AWS Emulator に向ける
export AWS_ENDPOINT_URL=https://127.0.0.1:9297
export AWS_SHARED_CREDENTIALS_FILE=~/.aws/emulator/9297/emulator_credentials
export AWS_CONFIG_FILE=~/.aws/emulator/9297/config
export AWS_CA_BUNDLE=~/.aws/emulator/9297/ca.pem
export AWS_EC2_METADATA_DISABLED=true
export AWS_DISABLE_HOST_PREFIX_INJECTION=true
export AWS_REGION=us-east-1
export AWS_DEFAULT_REGION=us-east-1
# 本物の資格情報が残っていると、エミュレーターに拒否される
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE

見慣れないのは AWS_DISABLE_HOST_PREFIX_INJECTION ですかね。一部の操作で、SDKは接続先のホスト名に data- などを付け足すので、そのままだと data-127.0.0.1 のような存在しないホストへ送られてしまいます。それを止める設定です。準備ができたら、ヘルスチェックと get-caller-identity で行き先を確かめます。

AWS Emulatorのコンテナを起動し、ヘルスチェックがokを返し、aws sts get-caller-identityのAccountが000000000000、Arnがarn:aws:iam::000000000000:user/emulatorになっている端末

アカウントIDが 000000000000 なら、リクエストはエミュレーターに届いています。ここに自分のアカウントIDが出たら、本物のAWSに送っているので、環境変数を見直してください。

5. 注文処理をAWS Emulatorに載せてみた

5-1. CloudFormationは、DynamoDBのテーブルでロールバック

まずは、実際のAWSと同じ手順で、テンプレートを aws cloudformation deploy に渡します。

AWS Emulatorでaws cloudformation deployを実行するとスタックの作成に失敗し、describe-stack-eventsでOrdersTableが、streamSpecification.streamEnabledがnullだという検証エラーで失敗している端末

注文テーブルの作成は streamSpecification.streamEnabled がnullだという検証エラーで失敗し、スタックはロールバックしました。CloudFormationの AWS::DynamoDB::Table の StreamSpecification に書けるのは StreamViewType などで、StreamEnabled はそもそも存在しないプロパティです。一方、DynamoDBの CreateTable のAPIでは StreamEnabled が必須です。ソースを確かめると、AWS Emulatorはテンプレートの StreamSpecification をそのまま CreateTable に渡していて、ここで値が欠けていました。

Streamsの設定を外したテンプレートでも試すと、今度はEventBridgeのルールが、バスが存在しないという理由で失敗しました。バスだけのテンプレートを流すと、スタックは CREATE_COMPLETE になるのに、list-event-buses にそのバスは出てきません。こちらは、CloudFormationで作ったイベントバスが見えない問題として、2026年10月25日に issues/ へ登録済みでした。

それでも、CloudFormationそのものは動きます。テーブルのストリームを外し、ルールを既定のイベントバスに付け替えると、Lambda、Step Functions、HTTP API、SNS、SQS、IAMロールなど残りの28リソースは CREATE_COMPLETE になりました。今回の構成で止まったのは、ストリームを有効にしたテーブルと、カスタムのイベントバスの2か所です。

5-2. SDKで組むなら、作成直後の状態を待ちましょう

CloudFormationの代わりに、同じ構成をboto3で1つずつ作るスクリプト(deploy.py)を用意しました。テンプレートと同じリソースを、テンプレートと同じ権限で作るだけのものです。

AWS Emulatorでpython3 deploy.pyを実行し、テーブル名、バケット名、失敗キューのURL、ステートマシンのARN、APIのIDとともに、ApiEndpointがhttps://127.0.0.1:9297/_aws/execute-api/から始まるURLで返っている端末

HTTP APIの呼び出し先は ApiEndpoint に https://127.0.0.1:9297/_aws/execute-api/<API ID> の形でそのまま返ってきます。実はこのスクリプト、ほかのOSSでは通っていた手抜きが、AWS Emulatorでは4つ通りませんでした。

作った直後のLambdaの関数は Pending で、呼べるのは Active になってからです。DynamoDBのテーブルも作った直後は CREATING で、書き込むと失敗します。イベントソースマッピングを作るときには、関数の実行ロールにストリームやSQSからデータを受け取る権限があるかを確かめられ、SNSからSQSへの配信もキューのポリシーでSNSからの送信を許可するまで届きませんでした。

4つとも、実際のAWSと同じ振る舞いです。ウェイターで待ち、ロールとキューに権限を付ければ通ります。言い換えると、ほかのOSSで通っていた書き方のほうが、本番では落ちる書き方だった、みたいな感じです。

5-3. 注文がStep Functionsの手前で止まる理由

準備ができたので、BOOK-1の注文を送ります。

AWS EmulatorのAPIへ注文をPOSTすると注文IDが返り、その注文をGETするとstatusがPENDINGのまま。自動で起動されたステートマシンの実行は、States.Runtimeで、in-processの呼び出しの連鎖がすでに3段なので何も実行しなかったという理由で失敗している端末

API GatewayからLambda、DynamoDBへの書き込み、Streamsから変更の通知のLambda、EventBridgeのルールまでは動き、ステートマシンの実行も自動で始まりました。ところが注文は PENDING のままで、実行は States.Runtime で失敗しています。エラーの文は、呼び出しの連鎖がすでに3段の深さなので、在庫の確保のLambdaを呼ばなかった、という内容でした。

注文のPOSTからAPI Gateway、注文の受付のLambda、DynamoDB、Streams、変更の通知のLambda、EventBridge、Step Functionsまでは動き、Step Functionsが在庫の確保のLambdaを呼ぶところで止まる。States.Runtimeで失敗し、呼び出しの連鎖がすでに3段の深さなのでLambdaを呼ばない、ソースのMAX_CALL_CHAIN_DEPTHは3でエミュレーター側の制限、CatchのStates.ALLでは拾えず注文はPENDINGのまま、と注記している

ソースを確かめると、Step Functionsの実装に MAX_CALL_CHAIN_DEPTH = 3 という定数があり、コメントにはエミュレーター側の制限でAWSの制限ではない、とはっきり書かれていました。AWS Emulatorは呼び出しを自分のプロセスの中で処理するので、Streams、EventBridge、Step Functionsと連なるたびに、待っている処理が1段ずつ重なっていきます。それを抑えるための定数です。

States.Runtime は、実際のAWSでもCatchの States.ALL で拾えない種類のエラーです。そのため注文は PENDING のまま残り、失敗用のキューも空のままでした。

5-4. 手で起動して、続きを確かめましょう

連鎖の途中から始めればどうなるかを確かめるため、同じ注文で、ステートマシンを start-execution から直接起動しました。

AWS Emulatorでstart-executionを直接実行すると、注文のstatusがCONFIRMEDになったが、実行はNotifyConfirmedのステートで、sns:publishの連携がこのエミュレーターの対応サービスにないという理由で失敗している端末

今度は在庫の確保と決済が動き、注文は CONFIRMED になりました。止まったのは、確定のあとのSNSへの通知です。4種類の注文を同じように流すと、確定する注文はSNSの通知で止まり、失敗する注文はCatchの先の最後のステートまで進みました。

AWS Emulatorでstart-executionから起動したステートマシン。ReserveStock、ChargePayment、MarkConfirmedまでは動き、NotifyConfirmedでSNSの連携が未対応のため失敗する。Catchの側は、在庫切れと決済の失敗からMarkFailed、SendToFailedQueue、OrderFailedまで動き、SOLD-OUT-1とBOOK-2は失敗キューに2件届いた

FLAKY-1はRetryで2回目に成功し、SOLD-OUT-1とBOOK-2はCatchで FAILED になって、失敗用のキューに2件届きました。DynamoDBやSQSを直接呼ぶ連携も含めて、ステートマシンの解釈そのものは実際のAWSと同じ結果です。

SNSが止まったのは、Step Functionsから直接呼べるサービスの一覧に入っていないからでした。ソースで確かめた連携先は、Lambda、DynamoDB、SQS、EventBridgeなどの12個でした。SNSへの連携は2026年10月27日に issues/ へ登録されています。SNSに手で発行すると、SQSからLambda、S3への領収書の保存までは1秒ほどで動いたので、止まるのはStep FunctionsとSNSのあいだだけです。

5-5. Lambdaの中身はRustPython

SDKで組んでいる途中、関数のエラーのログに、ZIPに入れた覚えのない /var/task/botocore/client.py が出てきました。気になってソースを確かめると、AWS EmulatorはPythonのLambdaをRustPython(Rustで書かれたPythonの処理系)で、Node.jsのLambdaをQuickJSで、自分のプロセスの中で動かしていました。boto3も、AWS Emulatorが用意した、同じ使い方ができる代わりのモジュールです。

実行環境を調べる小さな関数を python3.13 で作り、AWS EmulatorとFlociで呼んでみます。

AWS Emulatorで実行環境を調べる関数を呼んだ結果。pythonが3.14.0.alpha、implementationがRustPython、外部への通信がエミュレーターの提供するサービス以外には届かないというエラー、sqlite3とsslがModuleNotFoundError、paginatorがない、と表示されている端末

python3.13 を指定しても、中身はRustPythonの 3.14.0.alpha でした。外部へのHTTP通信は届かず、sqlite3 と ssl は import で失敗し、boto3のページネーターも用意されていません。同じ関数をFlociで呼ぶと、AWS公式のLambdaのイメージの上で動きます。

Flociで同じ関数を呼んだ結果。pythonが3.13.18、implementationがCPythonで、外部への通信、sqlite3、ssl、paginatorがすべてok

FlociはCPython 3.13.18で、どの項目もそのまま動きました。MiniStack、fakecloud、motoもCPythonです。AWS Emulatorで関数まで試すなら、外部のAPIを呼ぶ処理とページネーターを使う処理は、動かない前提で組んでおくのが良いかと思います。ソースではネイティブのライブラリをプロセスに取り込む仕組みも外されているので、C拡張を含むライブラリ(numpyやpydantic-coreなど)も動かないはずです(ここは試していません)。その代わり、Dockerがなくても関数を実行でき、起動も一瞬です。

5-6. 実行ロールの権限は、ちゃんと拒否された

1-1節で触れたとおり、ドキュメントにはIAMのポリシーをAWSと同じようには評価しないとあります。そこで、在庫の確保の関数の実行ロールから dynamodb:UpdateItem を外して、ステートマシンを起動しました。

AWS Emulatorで実行ロールのポリシーからUpdateItemを外し、ステートマシンを起動すると、注文のstatusがFAILEDになり、失敗用のキューのエラーに、assumed-role/shop-lambda/reserve_stockはdynamodb:UpdateItemを実行する権限がない、どのアイデンティティベースのポリシーも許可していない、と書かれている端末

注文は FAILED になり、assumed-role/shop-lambda/reserve_stock には dynamodb:UpdateItem の権限がない、と実際のAWSと同じ形のエラーが返ってきました。ソースによると、関数の中からの呼び出しは、実行ロールのセッションとして評価されています。変更の通知の関数から events:PutEvents を外したときも、同じ形の AccessDeniedException でイベントの送信が断られ、ステートマシンは起動しませんでした。

すべてのポリシーの評価がAWSと同じになるとは限らないので、ドキュメントのとおり、セキュリティの検証には使わないほうが安心です。それでも、実行ロールの権限の付け忘れのような、ありがちな失敗は手元で拾えました。ドキュメントの注意書きより、一歩踏み込んだ振る舞いです。

6. 5つのOSSと比べる

6-1. 区間ごとの結果

同じテンプレートと同じ4種類の注文を、5つのOSSにも流しました。CloudFormationが通らないmotoとkumoは、AWS Emulatorと同じくSDKで構成を組みました。自動で起動しない区間は手でステートマシンを起動して、その先の区間も確かめています。motoのStep Functionsは既定ではステートを実行しないので、moto-api/config で execute_state_machine を有効にしました。

6つのエミュレーターで、CloudFormation、HTTP APIからLambda、LambdaからDynamoDB、StreamsからStep Functionsの起動、Step FunctionsからLambda、Catch、DynamoDBの直接更新、SNSからS3までの区間を比べた一覧。FlociとMiniStackはすべて通る。AWS EmulatorはCloudFormationとSNSの連携で止まり、Step Functionsは連鎖3段で停止し手で起動なら通る。fakecloudはS3のZIPならCloudFormationが通るがResultPathの差で最後に止まる。motoはCloudFormationとHTTP APIが500エラーで、Streamsからは起動しない。kumoはCloudFormationが成功表示だけで、ほぼ止まる

手を加えずに全区間が通ったのは、FlociとMiniStackの2つです。どちらもテンプレートのコードを1行も変えずに、HTTP APIからS3の領収書まで動きました。AWS Emulatorはステートマシンの解釈やIAM、作成直後の状態の扱いでは実際のAWSに近い一方で、サービスをいくつも経由させると、連鎖の深さや未対応の連携に当たります。1-2節のスコアの傾向がそのまま出た形です。

6-2. 全区間が動いたFloci

Flociは起動にDockerのソケットを渡すだけで、Lambdaのコンテナまで自分で用意してくれます。APIの呼び出し先はLocalStackと同じ形の http://<API ID>.execute-api.localhost.localstack.cloud:4566 です。

Flociを起動し、aws cloudformation deployでスタックを作り、注文をPOSTしてGETすると、statusがCONFIRMEDでattemptsが1になり、aws s3 lsで領収書のJSONが保存されている端末

注文から2秒ほどで CONFIRMED になり、S3に領収書が保存されました。成功の流れが通ったので、次はCatchの側です。在庫切れのSOLD-OUT-1を流します。

Flociで在庫切れの注文をPOSTすると、注文のstatusがFAILEDになり、失敗用のキューのメッセージのerrorがOutOfStockになっている端末

Catchで FAILED になり、失敗用のキューには OutOfStock のエラーが届いています。1つだけ気をつけたいのが、CloudFormationの出力の ApiEndpoint です。実際のAWSと同じ https://<API ID>.execute-api.us-east-1.amazonaws.com の形で返るので、手元から呼ぶときは上の形に置き換えます。

6-3. CREATE_COMPLETEなのに、何も作られていない

kumoのCloudFormationは、もっと注意が要りました。

kumoでcloudformation create-stackを実行するとCREATE_COMPLETEになったが、dynamodb list-tablesのTableNamesが空、Lambdaの関数とステートマシンの一覧も空の端末

スタックは CREATE_COMPLETE なのに、テーブルも関数もステートマシンも作られていません。CIでデプロイの成否だけを確認していると、成功したと思い込んでしまいます。Lambdaも2-2節の図のとおり自分では動かさないので、今回は標準ライブラリだけで小さな実行役を書き、AWS_LAMBDA_RUNTIME_API に関数ごとの宛先を渡して起動しました。

fakecloudはテンプレートに直接書いたコード(Code.ZipFile)を、ZIPファイルとして展開しようとして失敗しました。

fakecloudでaws cloudformation deployは成功したが、aws lambda invokeでLambda execution failed、ZIP extraction failed、invalid Zip archiveというエラーが返っている端末

コードをZIPにしてS3に置く形(sam package と同じ形)に変えると、デプロイから関数の実行までは通りました。ところが、SNSの通知の中身から注文IDが消えていて、領収書の保存で止まります。原因は3-1節で付けた "ResultPath": null でした。

ResultPathがnullのDynamoDB連携のタスクに同じ入力を流すと、Floci、MiniStack、motoはAWSのドキュメントどおり入力のorderIdとskuをそのまま出力し、fakecloudはDynamoDBの応答のSdkHttpMetadataで出力を置き換え、orderIdがなくなる

実際のAWSでは、ResultPath を null にするとタスクの結果は捨てられ、入力がそのまま出力になります。最小のステートマシンに同じ入力を流して、4つで比べました。

ResultPathがnullのDynamoDB連携のタスクに同じ入力を流した結果。floci、ministack、motoは入力のorderIdとskuがそのまま出力されているが、fakecloudだけDynamoDBの応答のSdkHttpMetadataで出力が置き換わっている端末

Floci、MiniStack、motoは入力をそのまま返し、fakecloudだけがDynamoDBの応答で置き換えていました。fakecloudで ResultPath: null を使うステートマシンを試すときは、この1点だけ実際のAWSと結果が変わります。

6-4. OSSのIAMの評価は、既定でオフ

5-6節と同じく、在庫の確保の関数から dynamodb:UpdateItem を外したテンプレート(template-broken.yaml)を、Flociに流しました。

Flociを既定の設定で起動すると、権限を外したのに注文がCONFIRMEDになる。FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED=trueを付けて起動し直すと、同じ注文がFAILEDになり、User is not authorized to perform dynamodb:UpdateItemのエラーが出ている端末

既定の設定では、本来は拒否されるはずの注文が CONFIRMED で通ってしまいます。FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED=true を付けて起動し直すと、同じ注文が FAILED になりました。fakecloudの FAKECLOUD_IAM=strict、MiniStackの AUTH=true でも同じく FAILED になります。権限を戻した正しいテンプレートでは、評価をオンにしてもFlociとMiniStackは全区間が通りました。

在庫の確保の関数の実行ロールからUpdateItemを外した結果。AWS Emulatorは既定の設定で拒否し注文はFAILED、Flociは既定の設定では通って注文はCONFIRMED、FlociでIAMの評価をオンにすると拒否して注文はFAILED

AWS Emulatorは何も設定しなくても拒否したので、既定の振る舞いはOSSと逆です。OSSで権限の漏れを拾いたいなら、評価をオンにした設定を用意しておくのが良さそうです。

6-5. 起動時間とメモリ

起動の速さと、待っているあいだのメモリも測りました。起動時間は、AWS Emulatorはヘルスチェック、ほかはSQSの ListQueues が応答するまでの5回の中央値です。

起動時間はAWS Emulator 0.21秒、Floci 0.16秒、MiniStack 0.92秒、fakecloud 0.13秒、moto 0.57秒、kumo 0.11秒。待機中のメモリはAWS Emulator 173MiB、Floci 23MiB、MiniStack 40MiB、fakecloud 7MiB、moto 66MiB、kumo 9MiB。イメージの大きさはAWS Emulator 1.63GB、Floci 0.37GB、MiniStack 0.26GB、fakecloud 0.45GB、moto 0.56GB、kumo 0.07GB

起動はどれも1秒を切っていて、CIで毎回立ち上げても困らない速さです。AWS Emulatorは400以上のサービスを1つのプログラムに入れているぶん、メモリとイメージが大きめでした。ただ、Floci、fakecloud、motoはこれとはべつに、1GB前後のLambdaのイメージを取得します。関数まで動かすなら、合計の差はかなり縮まります。

6-6. motoの本来の出番はテストの中

motoにはサーバーとして動かす以外に、Pythonのテストの中でAWSを差し替える使い方があります。むしろこちらが本来の出番です。在庫の確保の関数を、モックしたDynamoDBに対して直接呼ぶテストを書きました。

import importlib, json, os, sys
import boto3, pytest
from moto import mock_aws

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "functions"))

@pytest.fixture
def table():
    # テストごとに、メモリ上の DynamoDB に空のテーブルを作る
    os.environ.update(AWS_DEFAULT_REGION="us-east-1", AWS_ACCESS_KEY_ID="test", AWS_SECRET_ACCESS_KEY="test", TABLE="orders")
    with mock_aws():
        boto3.client("dynamodb").create_table(TableName="orders", BillingMode="PAY_PER_REQUEST",
            AttributeDefinitions=[{"AttributeName": "orderId", "AttributeType": "S"}],
            KeySchema=[{"AttributeName": "orderId", "KeyType": "HASH"}])
        yield boto3.client("dynamodb")

def load(name):
    # クライアントはモジュールの import 時に作られるので、モックの中で import し直す
    return importlib.reload(importlib.import_module(name))

def test_flaky_fails_once(table):
    stock = load("reserve_stock")
    table.put_item(TableName="orders", Item={"orderId": {"S": "o-1"}, "sku": {"S": "FLAKY-1"}})
    with pytest.raises(stock.TransientError):
        stock.handler({"orderId": "o-1", "sku": "FLAKY-1", "qty": 1}, None)
    assert stock.handler({"orderId": "o-1", "sku": "FLAKY-1", "qty": 1}, None)["attempts"] == 2

motoを使ったpytestを実行し、test_post_order_is_pending、test_flaky_fails_once、test_sold_out_raisesの3件がPASSEDになり、3 passed in 0.48sと表示されている端末

Dockerを使わずに、3件のテストが0.5秒ほどで終わりました。サーバーとしてのmotoは6-1節でつまずきが多かったものの、関数1つの振る舞いを確かめるなら、いちばん手軽です。

7. 結局、どれを選ぶ?

やりたいことから逆引きできるよう、ここまでの結果を表にしました。

やりたいこと 候補 理由
サービスをいくつも経由する構成を、手元で通しで動かす Floci、MiniStack 今回の30リソースの構成が、コードを変えずに全区間で動いた
1つ1つのAPIの振る舞いを、AWSに近い形で確かめる AWS Emulator 作成直後の状態、イベントソースの権限の確認、実行ロールの評価がAWSと同じだった
Pythonの関数をユニットテストする moto(プロセスの中) Dockerなしで1秒もかからない
IAMの権限の漏れを手元で拾う AWS Emulator、またはIAMの評価をオンにしたFloci、fakecloud、MiniStack OSSは既定ではオフ

3章の注文処理に当てはめると、構成全体の通し確認はFlociかMiniStack、1つ1つの関数とAPIの呼び方はAWS Emulator、関数の細かい分岐はmoto、という組み合わせが今回の結果に合っています。AWS Emulatorは、AWSが自ら作り、AWSとの違いを issues/ と再現度のスコアで公開しながら直し続けています。2026年11月12日の時点では呼び出しの連鎖とSNSの連携で止まりましたが、この2つが直れば表は書き換わるはずです。

AWS Emulatorを試すなら、今回つまずいた点を先に知っておくと手戻りが減ります。ストリームを有効にしたテーブルやカスタムのイベントバスを使う構成は、SDKで組みます。作成直後の Pending や CREATING はウェイターで待つのが確実です。コマンドラインの --idle-timeout は、使われないまま時間がたったエミュレーターを止める設定です。AWS CLI 2.35.0以降なら、AIのコーディングエージェントに aws agent-toolkit add-skill --skill-name aws-emulator --region us-east-1 でスキルを入れ、エミュレーターの準備をエージェントに任せることもできます。

なお、今回のFlociのように全区間が動いたとしても、エミュレーターの結果は手元での確認にとどめ、本番の前には実際のAWSでも一度流します。

おわりに

ここまでお付き合いいただきありがとうございます。

AWS Emulatorは1つ1つのAPIの振る舞いが実際のAWSに近く、IAMの拒否まで返してくれた一方、サービスを長く経由する構成は連鎖3段の定数とSNSの連携で止まり、今回の注文処理を通しで動かせたのはFlociとMiniStackでした。エージェントが毎日直しているので、連鎖の定数が外れたら、同じ構成をもう一度流してみたいところです。

ではまた、お会いしましょう。

参考リンク

AWS Emulator

LocalStack

OSSのエミュレーター

AWSのドキュメント

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?