4
3

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】AWS EmulatorでCDK/Terraformのスタックはどこまで動くか【AWSEmulator】

4
Last updated at Posted at 2026-11-11

はじめに

2026年11月11日、AWSからAWS Emulatorが公式にリリースされました。AWS SDKやAWS CLIの呼び出しを、AWSアカウントなしで、ローカルやCI上のエミュレーターに向けられるツールです。公式は、実装されているAPIには有効な呼び出しにAWSと同じ結果を、無効な呼び出しには同じエラーを返し、実装されていないAPIは501で失敗させる、と説明しています。追加料金はかかりません。

初めて聞くと、次のような疑問が出ると思います。

  • 本当にAWSと同じように動くのか
  • 何が動いて、何が動かないのか
  • 自分のIaC(CDKやTerraform)は通るのか

特に3つ目は、実務で気になる点です。公式ドキュメントには、IaCツールへの対応はサービスによって異なると書かれています。

「Fidelity for data plane APIs, and support for infrastructure as code tools such as AWS CloudFormation, Terraform, and the AWS CDK, varies by service.」
出典: https://docs.aws.amazon.com/emulator/latest/userguide/fidelity.html

そこで本記事では、IaCがどこまで動くかを主軸に、Windows 11 + Docker上で実際に試した結果をまとめます。あわせて、エミュレーターを初めて知る人でも全体像がつかめるように、エミュレーターそのものの動きも確かめました。

  • CDKでリソースを段階的に追加してデプロイし、どこまで通ってどこで止まるかを表にする
  • API Gateway + Lambda + DynamoDBのAPIを作り、実際に呼び出して動くことを確かめる
  • エラー、未実装のAPI、認証、複数起動、状態の寿命といったエミュレーターの動きを確かめる
  • Terraformでも同様に試す
  • 公式が公開している忠実度のデータと既知の問題に、結果を突き合わせる

前提として、AWS SDKやAWS CLI、CDK(またはTerraform)の基本的な使い方と、Dockerの基本操作を想定しています。

AWS Emulatorとは

項目 内容 出典
位置づけ ローカル/CIでAWS APIを試すための公式エミュレーター 公式(What's New、2026-11-11)
状態 メモリ上のみ。停止すると消える 公式
未実装のAPI HTTP 501 NotImplemented を返す 公式
料金 追加料金なし 公式
本番利用 不可 公式
セキュリティテスト 不可(IAMポリシー、リソースポリシー、KMSキーポリシー、クロスアカウントは対象外) 公式
Windows/macOS Dockerで実行。既製イメージはなく、Dockerfileから自前でビルドする 公式
リージョン/アカウント 1つのリージョン・1つのアカウントで使う。複数リージョンを試すならエミュレーターを分ける 公式

aws-emulator-iac-compat-overview.png

図の見方: 自分のコードやツールは、環境変数 AWS_ENDPOINT_URL の有無で行き先が変わります。未設定なら実AWSに、https://127.0.0.1:9297 を設定するとローカルのエミュレーターに向かいます。

AWS Emulatorを使うとどのような良い点があるか

検証に入る前に、AWS Emulatorを使うと何が良くなるのかを整理します。AWS所属者の発信(AWS News Blog、AWS幹部のSwami Sivasubramanian氏がWerner Vogels氏のブログに寄せたゲスト投稿、AWS organizationのDEV.to記事)に書かれている使いどころを、本記事の検証で確かめられたものと対応させました。表の出典名と、参照したURLは次のとおりです。いずれも2026-11-11の公開です。

表の出典名 内容 URL
AWS News Blog Test your applications against AWS locally with AWS Emulator(著者はAWSのsenior specialist solutions architect) https://aws.amazon.com/blogs/aws/test-your-applications-against-aws-locally-with-aws-emulator/
Swami氏の投稿 AWS Emulator and the engineer's job when agents write the code(Werner Vogels氏のブログ All Things Distributed への、Swami Sivasubramanian氏のゲスト投稿) https://www.allthingsdistributed.com/2026/11/aws-emulator-and-the-engineers-job-when-agents-write-the-code.html
DEV.to How emulators might catch what you might miss(AWS organization配下の記事) https://dev.to/aws/how-emulators-might-catch-what-you-might-miss-34hd
良い点 AWS側の発信 本記事で確認できたこと
デプロイ待ちなしで試せる 単体テストを、デプロイなしでローカルで実行できる(AWS News Blog)。デプロイもAWSアカウントも不要なので、変更ごとにテストを回せる(DEV.to) CDKのデプロイは12〜18秒で完了した。API呼び出しは1回あたり数ミリ秒〜20ミリ秒台だった(1回の計測、実AWSとの比較なし)
コードやIaCを変えずに向け先だけ変えられる 環境変数でエンドポイントを向けるだけで、アプリケーションのコードは変更不要(AWS News Blog) boto3のコードは環境変数を外せば実AWSでも動く形のまま動いた。CDKとTerraformも、AWS_ENDPOINT_URL の設定だけでデプロイできた
モックが見逃す不具合を見つけられる SQSのメッセージを削除せずに課金する不具合は、モックでは通るが、エミュレーターでは再配信されてテストが失敗する(DEV.to) AWS形式のエラー(NoSuchKey、ConditionalCheckFailedException など)が返った。Lambdaのロールの権限不足も AccessDeniedException として検出された。SQSの再配信そのものは未検証
未実装のAPIがすぐ分かる 未実装のAPIはHTTP 501を返し、テストはすぐ失敗する(AWS News Blog) 27個のAPIを試して4つが501になった。黙って成功にならないので、気づける
複数のテストやエージェントを並列に動かせる エージェントごとに独自のコピー(ポート、キー、状態が別)を持ち、共有のテストアカウントが不要(DEV.to) 2つ目のエミュレーター(9298)を起動し、リソースも認証情報も1つ目から見えないことを確認した
コーディングエージェントが検証を繰り返せる リソースを作り、結果を確認し、再度試すことを、リージョンでのデプロイ待ちなしで行える。デモはKiroでDynamoDBを操作(AWS News Blog、Swami氏の投稿) 本記事の検証は、エージェントではなく私がCLIで操作した。エージェント経由での動作は未検証
クラウドの費用や共有リソースの競合を避けられる 実クラウドでのテストは費用がかかり、並行して動くテストが共有リソースを書き換えて競合する(DEV.to) 追加料金なし(公式の記載)。本記事の検証はすべてエミュレーター上で行い、実AWSにはリソースを作っていない
毎回まっさらな状態から始められる (今回読んだ発信では言及なし) 再起動は2秒で、スタックもLambdaも全て消えた。テストの前提を揃えやすい反面、途中で再起動すると消える
使う前に、どこまで一致するかを確認できる 忠実度のスコアをサービスごとに「合格数/総数」で公開し、テストもGitHubで公開している(AWS News Blog)。AWSと一致しない既知の挙動もリポジトリに公開している(Swami氏の投稿) 手順5と手順6で、スコアと既知の問題を自分の検証結果と突き合わせた

AWS側の発信は、いずれも提供元の説明です。本記事の右列は、Windows 11 + Dockerで1回ずつ試した結果であり、実AWSとの比較は行っていません。

一方で、AWS側の発信は、エミュレーターで何でも済むとは言っていません。

  • モックは「自分が制御するロジック」に残し、AWSサービスの挙動に依存する部分にエミュレーターを使う、という使い分けが示されています(DEV.to)
  • IAMポリシーやリソースポリシーをAWSと同じように評価しないため、セキュリティのテストには使わない(AWS News Blog)
  • 1つのエミュレーターは1アカウントとして動き、クロスアカウントやクロスリージョンの挙動はない(AWS News Blog)

なお、これらの良い点は、使うサービスの忠実度に左右されます。「良い点」が自分の構成でも成り立つかは、次章以降の手順で確かめます。

忠実度(fidelity)の読み方

公式は、エミュレーターとAWSの一致度を「忠実度テスト」で測り、サービスごとの合格数/総数を各リリースで公開しています。

用語 意味
忠実度テスト SDKでワークフローを実行し、結果・状態・エラーをAWSと比較する短いプログラム
忠実度スコア サービスごとの「合格数 / 総数」(例: 40 / 50)
コントロールプレーン 作成・参照・更新・削除・一覧のAPI
データプレーン サービスの主機能(S3のオブジェクトの取得・保存など)

スコアは同じテストバージョン同士でのみ比較できます。

検証環境

項目 内容
OS Windows 11
実行方法 Docker 28.0.4(公式のDockerfile)
エミュレーター 1.0.322(Release date 2026.11.10、commit 7753983)。GitHubの最新リリース v1.0.322 と一致
検証日 2026-11-12
IaC AWS CDK(CLI 2.1148.1、aws-cdk-lib 2.275.0)、Terraform 1.16.2(AWSプロバイダー 6.73.0)
その他 Python 3.12.7、boto3 1.43.91、AWS CLI 2.39.3、Node.js v22.13.1
起動オプション -p 9297 --telemetry off。ポートは 127.0.0.1 に限定して公開、メモリ上限2GiB

エミュレーターは1日に複数回リリースされているため、結果は検証日とリリースに依存します。

手順1: エミュレーターを起動する

公式のDockerfileでビルドし、docker run で起動します。Dockerfileは公式の内容のまま使いました(x86_64の場合。Armの場合は、rpmの名前を aarch64 に変えます)。ビルド中にrpm署名の NOKEY 警告が出ましたが、そのまま完了しました。

FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal
ADD https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator-x86_64.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"]
docker build -t aws-emulator .
docker run -d --name emu --memory=2g --memory-swap=2g -p 127.0.0.1:9297:9297 `
  -v "$env:USERPROFILE\.aws\emulator:/home/emulator/.aws/emulator" `
  aws-emulator -p 9297 --telemetry off

起動ログ(docker logs emu)です。

{"timestamp":"2026-11-11T22:33:58.891517Z","level":"INFO","fields":{"message":"aws-emulator emulator starting"},"target":"aws_emulator"}
This emulator collects usage and error telemetry: disabled (via the embedding configuration) — to turn it back on, remove every opt-out setting and restart
{"timestamp":"2026-11-11T22:33:58.944196Z","level":"INFO","fields":{"message":"request authentication enabled","credentials_file":"/home/emulator/.aws/emulator/9297/emulator_credentials"},"target":"aws_emulator_server"}
{"timestamp":"2026-11-11T22:33:58.963607Z","level":"INFO","fields":{"message":"emulator server information","endpoint_url":"https://127.0.0.1:9297","health_check_url":"https://127.0.0.1:9297/__admin/health","config_file":"/home/emulator/.aws/emulator/9297/config","ca_file":"/home/emulator/.aws/emulator/9297/ca.pem","java_truststore_file":"/home/emulator/.aws/emulator/9297/truststore.p12"},"target":"aws_emulator_server"}
{"timestamp":"2026-11-11T22:33:58.963668Z","level":"WARN","fields":{"message":"the emulator listens on 0.0.0.0, not loopback: other machines that can reach port 9297 can call it; bind 127.0.0.1 (-b 127.0.0.1) unless that is intended","bind_address":"0.0.0.0"},"target":"aws_emulator_server"}

ログから、テレメトリが無効になっていること、リクエストの認証が有効なこと、エンドポイントが https://127.0.0.1:9297 であることが分かります。最後の 0.0.0.0 の警告は、Dockerfileの -b 0.0.0.0 で、コンテナの中では全てのアドレスで待ち受けているために出ます。ホスト側は -p 127.0.0.1:9297:9297 で自分のPCにだけ公開しているので、外部から届く状態ではありません。

続いて、公式の手順どおり環境変数でSDK/CLIを向けます。実認証情報の変数は外しておきます。

$env:AWS_ENDPOINT_URL = "https://127.0.0.1:9297"
$env:AWS_SHARED_CREDENTIALS_FILE = "$env:USERPROFILE\.aws\emulator\9297\emulator_credentials"
$env:AWS_CONFIG_FILE = "$env:USERPROFILE\.aws\emulator\9297\config"
$env:AWS_CA_BUNDLE = "$env:USERPROFILE\.aws\emulator\9297\ca.pem"
$env:AWS_EC2_METADATA_DISABLED = "true"
$env:AWS_DISABLE_HOST_PREFIX_INJECTION = "true"
$env:AWS_REGION = "us-east-1"

疎通確認の結果です。

確認 結果
/__admin/health ok
aws sts get-caller-identity Account 000000000000、user/emulator
S3の通常名のバケット作成・put/get・削除 成功
DynamoDB 表名 ab AWSと同じ形式の ValidationException
S3 amzn-s3-demo-bucket(公式ドキュメントの例の名前) InvalidBucketName で失敗

最後の行は意外でした。公式ドキュメントの例に使われている名前では、バケットを作成できません。これはエミュレーターの不具合ではなく、S3のバケット名の規則で amzn-s3-demo- で始まる名前が予約されているためです(出典: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html )。エミュレーターが、この規則までAWSと同じように検証している、ということでもあります。

つまずきやすい点として、公式に次の注意があります。

  • AWS_ENDPOINT_URL が未設定のシェルでテストを実行すると、見つかった認証情報で本物のAWSを呼ぶ
  • 環境変数の AWS_ACCESS_KEY_ID などが残っているとエミュレーターに拒否される
  • ポートは -p 127.0.0.1:9297:9297 のように限定する(短縮形はネットワーク全体に公開される)

手順2: CDKを段階的にデプロイする

1つのスタックに、-c stage=N でリソースを段階的に追加していきます。

cdk bootstrap

cdk bootstrap aws://000000000000/us-east-1 は17秒で成功しました。作られたCDKToolkitスタックには、S3バケット、ECRリポジトリ、IAMロール(5つ)、SSMパラメータが含まれ、SSMの /cdk-bootstrap/hnb659fds/version も取得できました。

メモ: CDK CLI 2.1148.1とaws-cdk-lib 2.276.0の組み合わせは、ライブラリが未公開のCLI 2.1148.2を要求して失敗しました。これはCDK側の問題で、エミュレーターとは関係ありません。本記事ではlibを2.275.0に固定しています。

段階ごとの結果

段階 リソース cdk deploy 実際に動くか
1 S3バケット 成功(17秒) head-bucket 成功
2 DynamoDB(PAY_PER_REQUEST) 成功(17秒) ACTIVE、put-item/get-item 成功
3 SQS + SNS + サブスクリプション 成功(17秒) SNS publish がSQSに届いた
4 IAMロール + Lambda(inline、Node 22) 成功(12秒) State=Active だが、invoke は exports is not defined で失敗
5 EventBridge(rate 5分)→ SQS 成功(17秒) 定期実行が発火し、SQSにイベントが届いた
6 Step Functions(Pass) 成功(17秒) start-execution → SUCCEEDED
7 CloudWatch Logs + Alarm 成功(17秒) ロググループは作成。Alarmは describe-alarms に出ない
8 Lambda(Code.fromAsset) 成功(13秒) invoke 成功。アセットはステージングバケットに公開された
9 Lambda + API Gateway(REST、GET) 3回失敗し、4回目で成功(18秒) HelloEmulator が返る(下の「動作確認」)
10 API Gateway → Lambda → DynamoDB(アイテム登録API) 2回失敗し、3回目で成功(権限を足して再デプロイ) POST で保存、GET で取得できた(下の「エミュレーターの動きを確かめる」)
destroy スタック削除 成功(12秒) 残存は Lambda 用のロググループのみ(CDKが DeletionPolicy: Retain で作るため。エミュレーターの問題ではない)

段階8のスタックを、変更なしで再デプロイしたときの出力です。

CdkSampleStack: deploying... [1/1]

✅  CdkSampleStack (no changes)

✨  Deployment time: 0s

Outputs:
CdkSampleStack.BucketName = cdksamplestack-bucket83908e77-db9d1636caba
CdkSampleStack.FnAssetName = CdkSampleStack-FnAssetDC7A43AF-BEA29F2C2421
CdkSampleStack.FnName = CdkSampleStack-Fn9270CBC0-3ADA671FD9E8
CdkSampleStack.LogGroupName = CdkSampleStack-Lg419948D3-9E819B94EA09
CdkSampleStack.QueueUrl = https://sqs.us-east-1.amazonaws.com/000000000000/CdkSampleStack-Queue4A7E3555-148DCCAAE08B
CdkSampleStack.SmArn = arn:aws:states:us-east-1:000000000000:stateMachine:CdkSampleStack-SM934E715A-EFE2C378FC2A
CdkSampleStack.TableName = CdkSampleStack-TableCD117FA1-A02DF716D6D7
CdkSampleStack.TopicArn = arn:aws:sns:us-east-1:000000000000:CdkSampleStack-TopicBFC7AF6E-7C0061677BC0

デプロイしたスタックの内容は、list-stack-resources でも確認できます。段階8の時点で、テンプレートの18個のリソースがすべて CREATE_COMPLETE でした。ただし、前述のとおり、Alarm は CREATE_COMPLETE でも describe-alarms に出ません。

$ aws cloudformation list-stack-resources --stack-name CdkSampleStack --query "StackResourceSummaries[].[ResourceType,ResourceStatus]" --output table
---------------------------------------------------------
|                  ListStackResources                   |
+-----------------------------------+-------------------+
|  AWS::CloudWatch::Alarm           |  CREATE_COMPLETE  |
|  AWS::S3::Bucket                  |  CREATE_COMPLETE  |
|  AWS::CDK::Metadata               |  CREATE_COMPLETE  |
|  AWS::Lambda::Function            |  CREATE_COMPLETE  |
|  AWS::Lambda::Function            |  CREATE_COMPLETE  |
|  AWS::Logs::LogGroup              |  CREATE_COMPLETE  |
|  AWS::IAM::Role                   |  CREATE_COMPLETE  |
|  AWS::Logs::LogGroup              |  CREATE_COMPLETE  |
|  AWS::IAM::Role                   |  CREATE_COMPLETE  |
|  AWS::Logs::LogGroup              |  CREATE_COMPLETE  |
|  AWS::SQS::Queue                  |  CREATE_COMPLETE  |
|  AWS::SNS::Subscription           |  CREATE_COMPLETE  |
|  AWS::SQS::QueuePolicy            |  CREATE_COMPLETE  |
|  AWS::Events::Rule                |  CREATE_COMPLETE  |
|  AWS::StepFunctions::StateMachine |  CREATE_COMPLETE  |
|  AWS::IAM::Role                   |  CREATE_COMPLETE  |
|  AWS::DynamoDB::Table             |  CREATE_COMPLETE  |
|  AWS::SNS::Topic                  |  CREATE_COMPLETE  |
+-----------------------------------+-------------------+

$ aws cloudwatch describe-alarms
{
    "MetricAlarms": [],
    "CompositeAlarms": [],
    "LogAlarms": []
}

止まった、または気になった点

分類 内容 該当
未実装(501) 今回のIaCでは遭遇しなかった。別に27個のAPIを試すと4つが501だった(後述) なし
状態遷移 DynamoDB は ACTIVE になった なし
CloudFormationが成功なのに使えない Alarm がCREATE_COMPLETEなのに describe-alarms に出ない。直接 put-metric-alarm したアラームは一覧に出る 段階7
Lambda inline コード inline の exports.handler が index.mjs として扱われ、exports is not defined。Code.fromAsset の index.js では動く 段階4
出力値の形式 SQSのQueueUrlやAPI GatewayのURLが実AWSのホスト名(sqs.us-east-1.amazonaws.com、*.execute-api.us-east-1.amazonaws.com)の形式。エンドポイントが上書きされるのでCLI/SDKの操作は通るが、このURLをcurlやブラウザで開くと実AWSに向かう 段階3、段階9
Lambda Function URL addFunctionUrl で作る権限リソースが、「InvokedViaFunctionUrl プロパティを適用しない」というメッセージで CREATE_FAILED になり、ロールバックした 段階9(最初の試行)
ステートメントIDが長すぎる LambdaRestApi の既定(proxy+ のプロキシ)が作る AWS::Lambda::Permission が、「statementId は100文字以下」の検証エラーで失敗した。ステートメントIDにスタック名と長い論理IDが入るため。GETを1つだけ定義する構成や、短いIDに変えて回避した。公式リポジトリの既知issueに同じ問題が記録されている(手順6) 段階9、段階10
ステージ名の重複 同じスタックに2つ目のREST APIを prod ステージで作ると、prod already exists in stack で CREATE_FAILED。ステージ名を変えて回避した。AWSではステージはAPIごとの名前だが、エミュレーターではスタック内で重複を許さない。実AWSでの挙動は未確認 段階10
ロールバック後の再deploy CDKが作るロググループは Retain のため、失敗のロールバックでも残り、再deployで「ロググループが既に存在する」(AlreadyExists) になった。手で削除して再deployした。CDKとCloudFormationの仕様で、エミュレーター固有ではない 段階9、段階10
Lambdaの権限不足 段階10で grantReadWriteData を書き忘れ、LambdaがDynamoDBに AccessDeniedException で拒否された。エミュレーターがLambdaのロールの権限不足を検出した。コードの誤りを見つけるのに役立つ 段階10

Lambda inline コードは、実AWSで同じコードがどう動くかを確認していないため、エミュレーターの差かどうかは断定できません。

動作確認: HelloEmulator が返るか

デプロイが成功しても、動くとは限りません。そこで、Lambdaが HelloEmulator を返し、API Gatewayから呼べることを確認しました。

Lambdaのコード(lambda/hello.js)です。

exports.handler = async () => ({
  statusCode: 200,
  headers: { 'Content-Type': 'text/plain' },
  body: 'HelloEmulator',
});

CDKでは、このLambdaを LambdaRestApi に接続し、ルート(/)に GET を1つだけ定義しています。

呼び出し方 結果
aws lambda invoke StatusCode 200、レスポンスのbodyが HelloEmulator
aws apigateway test-invoke-method status 200、body HelloEmulator
curl(Host ヘッダーにAPIのホスト名を指定、接続先はエミュレーター) HTTP/1.1 200 OK、content-type: text/plain、本文 HelloEmulator
curl(/restapis/<id>/prod/_user_request_/ のパス形式) 404 UnknownService(このパスでは到達できない)

curlの実行例です。API Gatewayの呼び出しは、エミュレーターの1つのエンドポイントに対し、Host ヘッダーで対象のAPIを指定します。

curl.exe -i --cacert $env:AWS_CA_BUNDLE `
  -H "Host: <restApiId>.execute-api.us-east-1.amazonaws.com" `
  https://127.0.0.1:9297/prod/
HTTP/1.1 200 OK
content-type: text/plain
content-length: 13
x-amzn-requestid: 863869a5-ede1-4e56-bf04-151a017052da
date: Wed, 11 Nov 2026 22:58:41 GMT

HelloEmulator

CDKが出力するURL(https://<id>.execute-api.us-east-1.amazonaws.com/prod/)をそのまま開くと実AWSに向かいます。エミュレーターに送るには、上のように接続先を127.0.0.1にして Host ヘッダーでAPIを指定します。

手順3: エミュレーターの動きを確かめる

ここまでは「IaCがデプロイできるか」でした。AWS Emulatorを初めて知る人のために、エミュレーターそのものの動きも、いくつかの観点で確かめました。以降の例は、すべて手順1の環境変数を設定したシェルで実行しています。

1. コードを変えずにエミュレーターへ向く

アプリのコードにエンドポイントも認証情報も書かず、環境変数だけで向け先が決まります。次のPython(boto3)は、環境変数を外せばそのまま本物のAWSでも動くコードです。

import boto3, uuid

s3 = boto3.client("s3"); ddb = boto3.client("dynamodb"); sqs = boto3.client("sqs")
sfx = uuid.uuid4().hex[:8]
s3.create_bucket(Bucket=f"demo-{sfx}")
s3.put_object(Bucket=f"demo-{sfx}", Key="a.txt", Body=b"hello")
print(s3.get_object(Bucket=f"demo-{sfx}", Key="a.txt")["Body"].read())
ddb.create_table(TableName=f"demo-{sfx}", BillingMode="PAY_PER_REQUEST",
    AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
    KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}])
ddb.put_item(TableName=f"demo-{sfx}", Item={"id": {"S": "1"}})

実行結果(1回の計測、ローカルのDocker上、単位はミリ秒)です。

操作 時間
S3 create_bucket 18.5
S3 put_object 3.9
S3 get_object 2.9
DynamoDB create_table(直後に ACTIVE) 4.5
DynamoDB put_item 2.6
SQS create_queue 22.0
SQS send_message 1.9
SQS receive_message 1.8

公式は「ミリ秒で返る」としています。今回の計測でも、1回の呼び出しは数ミリ秒から20ミリ秒台でした。実AWSとの比較はしていません。

2. 無効な呼び出しには、AWS形式のエラーを返す

呼び出し 返ったエラー
S3 存在しないキーの取得 NoSuchKey: The specified key does not exist.
S3 存在しないバケットの head-bucket 404 Not Found
SQS 存在しないキューの URL 取得 AWS.SimpleQueueService.NonExistentQueue
DynamoDB 存在しないテーブルの describe ResourceNotFoundException: Requested resource not found: Table: nope not found
DynamoDB 条件付き書き込みが条件に合わない ConditionalCheckFailedException
Lambda 存在しない関数の取得 ResourceNotFoundException: Function not found: arn:aws:lambda:...:function:nope

モックのように「呼べば成功する」のではなく、実装されているAPIは検証とエラーまで実行します。実AWSとの一致までは確認していませんが、エラーコードはAWSのものと同じ形式です。

3. 実装されていないAPIは、はっきり失敗する

公式は、未実装のAPIは HTTP 501 を返すと書いています。27個のAPIを試したところ、4つが501でした。

API 結果
athena list-work-groups 501 NotImplemented(operation ListWorkGroups is scaffolded but not implemented in this emulator)
translate list-text-translation-jobs 501
comprehend list-entity-recognizers 501
quicksight list-dashboards 501
残り23個 エラーなしで応答(挙動がAWSと一致するかは未確認)

「エラーなしで返った」ことは、挙動が正しいことを意味しません。ただ、未実装のAPIが黙って成功にならず、テストがすぐ失敗として気づける点は、501の利点です。

4. 認証が必要で、起動ごとに変わる

呼び出し 結果
署名なしのリクエスト(curl) 403 UnrecognizedClientException: The security token included in the request is invalid.
適当なキーで署名したリクエスト AccessDenied: Sign the request with the emulator credentials
エミュレーター再起動のあと 認証情報のファイルが書き換わる(起動のたびに新しいキーを作る)

これは、他のWebページやローカルのプログラムが、知らないうちにエミュレーターを呼ぶことを防ぐための設計だと公式に書かれています。

5. 複数のエミュレーターは独立している

ポートを分けて2つ目のエミュレーター(9298)を起動し、確認しました。

操作 結果
1つ目(9297)にバケットを作る 9297 にだけ存在する
2つ目(9298)のバケット一覧 空(9297 のリソースは見えない)
2つ目にバケットを作る 9298 にだけ存在し、9297 には見えない
1つ目の認証情報で2つ目を呼ぶ AccessDenied(キーが別)

エージェントごと、CIジョブごと、ブランチごとにエミュレーターを起動すれば、互いのリソースは見えません。

6. 状態はメモリ上だけで、再起動すると消える

再起動前 再起動後
スタック2つ(CDKToolkit、CdkSampleStack)、バケット3つ、テーブル3つ すべて0(スタックもLambdaも消えた)

再起動は2秒で完了しました。毎回まっさらな状態から始められる反面、テストの途中で再起動すると全て消えます。永続化はありません(公式の記載どおり)。

7. サービスをまたぐアプリが動く(API Gateway → Lambda → DynamoDB)

アイテムを登録・取得する小さなAPIを作りました。POST で受け取った値をLambdaがDynamoDBに保存し、GET で取り出します。

const { DynamoDBClient, PutItemCommand, GetItemCommand } = require('@aws-sdk/client-dynamodb');
const ddb = new DynamoDBClient({});
exports.handler = async (event) => {
  if (event.httpMethod === 'POST') {
    const { id, text } = JSON.parse(event.body || '{}');
    await ddb.send(new PutItemCommand({ TableName: process.env.TABLE_NAME, Item: { id: { S: id }, text: { S: text } } }));
    return { statusCode: 201, body: JSON.stringify({ saved: id }) };
  }
  const id = event.pathParameters.k;
  const r = await ddb.send(new GetItemCommand({ TableName: process.env.TABLE_NAME, Key: { id: { S: id } } }));
  return r.Item ? { statusCode: 200, body: JSON.stringify({ id, text: r.Item.text.S }) } : { statusCode: 404, body: '{"message":"not found"}' };
};

Lambdaの中からエミュレーターのDynamoDBを呼ぶには、環境変数を明示する必要がありました。エミュレーターは、Lambdaの実行環境にエンドポイントも認証情報も自動では渡しません。確認した値は次のとおりです。

項目 内容
Lambdaの実行環境 Node.js(process.version は v22.0.0)。DynamoDBのクライアント(@aws-sdk/client-dynamodb)を読み込めた
既定の環境変数 AWS_ENDPOINT_URL なし、認証情報なし
追加した環境変数 AWS_ENDPOINT_URL=https://127.0.0.1:9297、AWS_SHARED_CREDENTIALS_FILE、AWS_CONFIG_FILE、NODE_EXTRA_CA_CERTS(いずれもコンテナ内のパス)

最初のデプロイでは、DynamoDBへの権限付与(grantReadWriteData)を書き忘れました。すると、エミュレーターは、Lambdaのロールの権限不足を検出して、500を返しました。

AccessDeniedException: User: arn:aws:sts::000000000000:assumed-role/CdkSampleStack-AppServiceRole.../CdkSampleStack-App... is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:us-east-1:000000000000:table/... because no identity-based policy allows the dynamodb:PutItem action

権限付与を追加して再デプロイしたあとの結果です。

リクエスト 結果
POST /v1/i({"id":"item-1","text":"HelloEmulator from DynamoDB"}) 201 Created、{"saved":"item-1"}
GET /v1/i/item-1 200 OK、{"id":"item-1","text":"HelloEmulator from DynamoDB"}
GET /v1/i/nope 404 Not Found、{"message":"not found"}
aws dynamodb scan(CLIから直接) 1件、item-1 が保存されている

これで、リクエスト → API Gateway → Lambda → DynamoDB → 取得、までの一連が動くことを確認できました。流れを図にまとめます。

aws-emulator-iac-compat-flow.png

ただし、公式は、IAMポリシーなどのセキュリティのテストにエミュレーターを使わないよう求めています。今回の権限エラーはエミュレーターが検出してくれましたが、AWSと同じ判定とは限りません(公式は、評価の仕方がAWSとは異なると書いています)。

手順4: Terraformで試す

S3バケットとDynamoDBテーブルの2つを、AWS_ENDPOINT_URL の環境変数だけでエミュレーターに向けました。プロバイダー側には skip_credentials_validation、skip_requesting_account_id、skip_metadata_api_check、s3_use_path_style を設定しています。

操作 結果
init 成功
apply 成功(13秒)。DynamoDBの作成に6秒
destroy 成功(6秒)
apply 直後の plan DynamoDB で差分が出る(on_demand_throughput の max_read_request_units/max_write_request_units が -1 -> null)。1 to change
再度 apply UpdateTable が ValidationException で失敗

この差分は、公式リポジトリの既知issueに記録されています(手順6)。原因の切り分けとして、describe-table を直接叩くと、OnDemandThroughput に MaxReadRequestUnits: -1、MaxWriteRequestUnits: -1 が返りました。プロバイダーは、設定していない項目がこの値で返ってくることを差分として扱い、更新を試みますが、更新内容が空のため UpdateTable がエラーになったと考えられます。実AWSでは未設定のテーブルでこの項目がどう返るか、確認していません。

terraform apply の直後に terraform plan を実行したときの出力です。

$ terraform plan
aws_dynamodb_table.t: Refreshing state... [id=tf-emu-sample-table]
aws_s3_bucket.b: Refreshing state... [id=tf-emu-sample-bucket-20261112]

Terraform will perform the following actions:

  # aws_dynamodb_table.t will be updated in-place
  ~ resource "aws_dynamodb_table" "t" {
        id                          = "tf-emu-sample-table"
        name                        = "tf-emu-sample-table"
        tags                        = {}
        # (13 unchanged attributes hidden)

      - on_demand_throughput {
          - max_read_request_units  = -1 -> null
          - max_write_request_units = -1 -> null
        }

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

この状態で、もう一度 terraform apply を実行すると、次のエラーで失敗します。

Error: updating AWS DynamoDB Table (tf-emu-sample-table): operation error DynamoDB: UpdateTable, https response error StatusCode: 400, RequestID: (省略), api error ValidationException: At least one of ProvisionedThroughput, BillingMode, UpdateStreamEnabled, GlobalSecondaryIndexUpdates, SSESpecification, ReplicaUpdates, MultiAccountReplicaReady, ReplicaTransitRoleArn, MultiRegionConsistency, DeletionProtectionEnabled, OnDemandThroughput, WarmThroughput or TableClass is required

手順5: 忠実度スコアと突き合わせる

リリース v1.0.322 には、スコアのデータ scores.json.gz(commit 7753983、集計日時 2026-11-11 00:09 UTC)が添付されています。中身は忠実度テスト1件ごとの合否(pass/fail/skipped)とサービスの一覧です。公式がまとめた表ではないため、本記事ではサービスごとに合否を数え直しました。

  • 全体: 42,857件(skipped を除く)のうち 24,243件が pass(56.6%)
  • 集計方法: 対象サービスが1つだけのテストについて、サービスごとに pass/総数を数えた(複数サービスをまたぐテストは含めていない)
  • 注意: 同じテストバージョン同士でのみ比較できる(公式の記載)。私の集計は公式のスコア表ではない
サービス pass / 総数 割合 今回のCDK/Terraformでの結果
SQS 299 / 333 90% SNS経由の配信まで動作
ECR 233 / 287 81% bootstrap で作成された
S3 965 / 1,220 79% 作成・put/get・アセット公開が動作
DynamoDB 807 / 1,044 77% CDKは動作。Terraformは再applyが失敗
SNS 210 / 284 74% 動作
Step Functions 432 / 589 73% 実行が成功
CloudWatch 621 / 848 73% Alarm が CFN 経由で見えない
CloudWatch Logs 466 / 642 73% ロググループ作成は動作
IAM 488 / 691 71% ロール作成は動作
STS 94 / 169 56% get-caller-identity は動作
Lambda 365 / 787 46% asset は動作、inline は失敗
SSM 92 / 452 20% bootstrap のパラメータ取得は動作
CloudFormation 414 / 2,612 16% 今回のスタックは全て動作

スコアが低くても、今回の単純なスタックは動きました。CloudFormationの16%は、テストの多くが複雑な構成(サービスをまたぐもの、ロールバックや更新を伴うもの)を対象にしているためだと考えられますが、内訳までは確認していません。「割合が低い=自分の構成が動かない」とは限らないので、使うAPIの範囲で個別に試すのが確実です。

手順6: 公式リポジトリの既知の問題と突き合わせる

aws/aws-emulator の issues/ には、既知の差異が1件ずつファイルになっています。things-that-do-not-work/(未解決)と things-that-now-work/(解決済み)の2つのフォルダに分かれています。今回の検証で見つけた差異が、すでに記録されているかを調べました。

調べ方は次のとおりです。

  • リポジトリの issues/ だけをスパースチェックアウトした(HEADは、今回使ったリリース v1.0.322 と同じコミット 7753983)
  • 件数は、未解決が6,544件、解決済みが12,708件
  • 全ファイルを、今回の差異に関するキーワードで全文検索し、該当ファイルを読んだ

結果です。

今回の観察 既知のissue 判定
DynamoDB の on_demand_throughput が -1 で返り、Terraform で差分が出続ける 20261108T011128Z-dynamodb-2-net-new-causes-describetable-invents-an.md(未解決)。「DescribeTable が、最大値を要求していないテーブルにも OnDemandThroughput の -1/-1 を返す。CloudFormation、Terraform などのドリフト検出が、そのようなテーブルごとに差分を報告する」と記載 一致
AWS::Lambda::Permission の statementId が100文字を超える 20261105T132157Z-cloudformation-8-net-new-root-causes-in-the-684719.md(未解決、原因1)と 20261109T152222Z-apigateway-cloudformation-lambda-cfn-generated-lambda-permission-statementid.md(未解決)。「CloudFormation が生成するステートメントIDを100文字以内に切り詰めない。CDKの RestApi/LambdaRestApi のスタックがすべて失敗する」と記載 一致
CloudFormation 経由の CloudWatch Alarm が describe-alarms に出ない 20261023T113357Z-cloudformation-reports-create-complete-for-logs-cloudwatch-ecs.md(未解決)。トレースに、CFNでデプロイした Lambda + Alarm の describe_alarms が空になる確認が含まれる 一致
Lambda のインラインコード(Code.fromInline)が exports is not defined で失敗 該当するissueなし。関連として、Node.js ランタイムの実行の忠実度を扱うissueが複数ある。index.handler が CommonJS(exports.handler)でも動くべきという記載(human-lambda-invoke-must-run-nodejs20-x-and-nodejs22-x-handlers.md)はあるが、インライン配置のファイル名についての記載は見つからなかった なし(未報告の可能性)
Lambda Function URL(addFunctionUrl)の権限が InvokedViaFunctionUrl で失敗 該当するissueなし。Function URL に関する別のissue(例: 20261029T233239Z-lambda-function-url-configs-return-a-default-invokemode.md)はあるが、InvokedViaFunctionUrl を扱うものはない なし(未報告の可能性)
同じスタックの2つ目のREST APIが prod ステージで失敗(prod already exists in stack) 該当するissueなし。already exists in stack は、別の用途(物理名の衝突時のメッセージ文言)で複数のissueに登場するが、ステージ名の重複を扱うものはない なし(未報告の可能性)

読み取れること:

  • 見つけた差異のうち3件は、すでに公式リポジトリに記録されていました。うち2件(on_demand_throughput、statementId)には、AWS側の挙動(「CloudFormationはIDを100文字以内にする」「OnDemandThroughput は省略される」)が書かれています。これは私が実AWSで確かめたことではなく、issueの記載です。
  • 残りの3件(インライン、Function URL、ステージ名の重複)は、全文検索では該当が見つかりませんでした。未報告の可能性があります。公式は、差異の報告をGitHubのIssuesで受け付けています。

注意点:

  • issueファイルは、テストの実行結果から起こされた差異の記録で、一定時点のコミットに対して書かれたものです(ファイル内にそのコミットを示す注記があります)。今のリリースで同じ問題が起きるとは限りません。
  • 実際に、古さが分かる例がありました。未解決フォルダの 20261106T052537Z-apigatewayv2-lambda-logs-the-nodejs22-x-lambda-runtime.md は「nodejs22.x のランタイムにAWS SDK v3が同梱されない」としていますが、今回のリリースでは、Lambdaから @aws-sdk/client-dynamodb を読み込めて動きました。未解決フォルダにあっても、すでに直っていることがあります。
  • 全文検索は、キーワードに依存します。検索語に合わない表現で書かれたissueは、見落としている可能性があります。

実務での使い方

観点 内容
向いている IaCの構文エラー、リソース間の参照、基本的な作成・削除、CIでのスモークテスト、SDKを使うアプリの結合テスト(API → Lambda → DynamoDBまで確認できた)
向かない IAMやリソースポリシーの検証(公式が対象外としている。今回Lambdaの権限不足は検出できたが、AWSと同じ判定とは限らない)
注意: ステータス 「スタックが成功」だけで判断せず、作成されたリソースを別途describeや実呼び出しで確かめる(Alarmの例)
注意: 接続先 AWS_ENDPOINT_URL が空のシェルで実行すると実AWSに向かう。CDKが出力するURLは実AWSのホスト名なので、そのまま開かない
注意: Lambda Lambdaの中から他のサービスを呼ぶなら、エンドポイントと認証情報の環境変数を明示する。インラインコードは動かなかったので、アセットで配る
注意: 状態 メモリ上のみ。再起動で全て消える。テストごとに作り直す前提で使う
運用 エミュレーターのリリース、検証日、使うサービスの忠実度スコアを記録しておく。1日に複数回リリースされるため、結果は変わりうる

さいごに

AWS Emulatorは、リリース直後の時点でも、CDKのデプロイから、API Gateway、Lambda、DynamoDBをまたぐ処理まで、想定していたより広い範囲を動かせました。一方で、CloudFormationのステータスが成功でも、Alarmのように実際には使えないリソースがありました。デプロイの成否だけで判断せず、作成されたリソースの動作まで確認することが、運用の前提になります。用途としては、CIでの結合テストや、AIエージェントに安全に試行させる環境が向いており、IAMなどの権限の検証は、引き続き実AWSで行う必要があります。エミュレーターは1日に複数回更新されているため、本記事で確認した差異も、今後変わる可能性があります。ご自身の構成で試し、公式の記録にない差異を見つけた場合は、GitHubのIssuesに報告することが、改善につながります。なお、検証はすべてエミュレーター上で行っており、実AWSとの比較は行っていません。

参考文献

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?