AWSは2026年11月11日にAWS Emulatorを公開しました。AWSのAPIを手元のマシンで再現するローカル版です。無料で、AWSアカウントは要らず、コードはパブリックドメイン(CC0)、対応サービスは200を超えます。LocalStackが最新イメージの利用にアカウントと認証トークンを求めるようになってから7か月後の登場だったこともあり、待ち望んでいた無料のLocalStack代替として注目している人が少なくありません。
この記事の大半はセットアップの話です。MacやWindowsマシンでのDocker、Linux用パッケージ、AWS CLIとboto3の接続先をエミュレーターにする環境変数、Docker Compose、pytest、GitHub Actionsを順に扱います。ここにあるコマンドは、すべてバージョン1.0.342で自分で実行しました。セットアップのあとは、自分のプロジェクト2つのS3テストをこのエミュレーターに向けてみて、どこが正しくてどこが間違っていたかを、LocalStack 4.12、Moto、Flociと並べて紹介します。2つのプロジェクトとは、自分のマーケットプレイスアプリのアップロード処理と、セルフホストしたCodemagic PatchサーバーがOTAバンドルを置いているバケットです。
1分でわかるAWS Emulatorの使い方#
AWSはコンテナイメージを公開していません。AWSのLinux用パッケージを入れるだけの9行のDockerfileから自分でビルドし、ポート9297で起動して、環境変数をいくつか設定します。それ以降、SDKはAWSではなくエミュレーターと通信するようになり、コードは一切変わりません。
docker build -t aws-emulator . # the Dockerfile is below
mkdir -p ~/.aws/emulator
docker run --rm -d --name aws-emulator -p 127.0.0.1:9297:9297 \
-e HOME="$HOME" -v "$HOME/.aws/emulator:$HOME/.aws/emulator" \
aws-emulator -p 9297 --telemetry off
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
aws sts get-caller-identity # "Account": "000000000000"起動は約0.5秒、アイドル時のメモリは約175 MiBです。すべてメモリ上に置かれるので、コンテナを止めると中身は消えます。
アカウントは必要?(LocalStackの有料化とAWS Emulator)#
必要ありません。AWS Emulator、Moto、Flociは、どれも何にも登録せずに動きます。AWS EmulatorにいたってはAWSの認証情報すら求めません。起動のたびに自前のアクセスキーを作り、そのキーで署名されたリクエストしか受け付けないのです。
変わったのはLocalStackのほうです。2026年3月から、最新イメージにはLOCALSTACK_AUTH_TOKENが必要になりました。トークンは無料アカウント(無料プランは趣味や非商用での利用が対象です)か、有料アカウントで手に入ります。トークンがないと、コンテナは起動時に止まります。実際に両方試してみました。latestタグのイメージ(2026.10.7、11月11日ビルド)は終了コード55で終了しますが、最後のコミュニティ版であり、LocalStack自身が固定先として勧めているバージョンでもある4.12は、今でもトークンなしで起動します。
AWS Emulatorが求めるものを1つ挙げるなら、テレメトリーです。デフォルトでオンになっています。AWSによれば、リクエストやコード、認証情報が含まれることは一切ありませんが、何も送ってほしくないなら止め方は3つあります。コマンドラインで--telemetry off、環境変数でEMULATOR_TELEMETRY=off、あるいは~/.aws/emulator/config.tomlにこう書きます。
[telemetry]
enabled = falsemacOSやWindowsでDockerを使ってAWS Emulatorをセットアップする#
MacやWindowsマシンなら、Dockerを使うことになります。空のフォルダーに、次の内容をDockerfileとして置いてください。AWS自身のもので、AWSのDockerガイドからそのまま持ってきました。
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"]Apple siliconをはじめとするARMマシンでは、x86_64.rpmをaarch64.rpmに変えてください。あとはビルドして起動します。
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ビルドには私のMacで88秒かかり、イメージは1.67 GBでした。このdocker runの行には、見た目以上に大事な部分が3つあります。
-p 9297:9297ではなく-p 127.0.0.1:9297:9297。短いほうの書き方だと、エミュレーターがネットワーク全体に公開されてしまいます。- ボリュームと
-e HOME。エミュレーターは起動時に~/.aws/emulator/9297/へ4つのファイルを書き出します。認証情報、AWSのconfigファイル、証明書(ca.pem)、そしてJavaのトラストストアです。このフォルダーをコンテナ内の同じパスにマウントすることで、ホスト側のSDKがそれらを読めるようになります。 - ポートは3か所に出てくる。変えるなら全部変えてください。エミュレーターはそれらのファイルにポート番号を書き込むので、フォルダーを共有する2つのコンテナはお互いのファイルを上書きしてしまいます。
LinuxにAWS Emulatorをインストールする#
LinuxならDockerは要りません。DebianパッケージとRPMが用意されています。
# Debian / Ubuntu (use aws-emulator_arm64.deb on ARM)
curl -fsSLo aws-emulator.deb https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator_amd64.deb
sudo apt-get install -y ./aws-emulator.deb
# Amazon Linux, Fedora, RHEL (aarch64.rpm on ARM)
sudo dnf install -y https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator-x86_64.rpm
aws-emulator --telemetry offパッケージは150 MBです。127.0.0.1:9297でフォアグラウンドで動き、同じ4つのファイルを~/.aws/emulator/9297/に書き出します。Ctrl+Cできれいに止まり、ファイルも削除されます。テストで使うなら--idle-timeout 900を付けておきましょう。15分間リクエストがなければ終了するので、止め忘れたエミュレーターが1週間居座ることもありません。
AWS CLIとboto3の接続先をエミュレーターに切り替える#
設定する変数はこれです。標準のAWS環境変数を読むSDKならどれでも拾ってくれるので、テスト対象のコードにはエンドポイントもキーも要りません。
E=~/.aws/emulator/9297
export AWS_ENDPOINT_URL=https://127.0.0.1:9297
export AWS_SHARED_CREDENTIALS_FILE=$E/emulator_credentials
export AWS_CONFIG_FILE=$E/config
export AWS_CA_BUNDLE=$E/ca.pem
export AWS_REGION=us-east-1
export AWS_EC2_METADATA_DISABLED=true
export AWS_DISABLE_HOST_PREFIX_INJECTION=true
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE
curl -s --cacert $AWS_CA_BUNDLE $AWS_ENDPOINT_URL/__admin/health # ok
aws sts get-caller-identity # 000000000000あとは普通のboto3がそのまま動きます。
import boto3
s3 = boto3.client("s3")
s3.create_bucket(Bucket="uploads")
print(s3.list_buckets()["Buckets"])unsetの行は必ず残してください。環境変数は認証情報ファイルより優先されるので、本物のAWSキーがexportされたままだと、SDKはそのキーで署名し、エミュレーターはリクエストを拒否し、あなたは原因を探して10分を無駄にします。aws sts get-caller-identityは手早い確認方法です。エミュレーターのアカウントは常に000000000000なので、自分のアカウント番号が表示されたら、話している相手はAWSです。
ほかにも、実際にはまったこと、はまりかけたことがいくつかあります。
- エンドポイントは
localhostではなく127.0.0.1のままにする。IPアドレスなら、SDKはバケット名をパスに入れます。localhostだと、JavaScript、Java、GoのSDKはバケット名をホスト名のほうに移し(uploads.localhost)、エミュレーターはそれを受け付けません。 - HTTPSのみで、証明書は起動のたびに新しくなる。boto3、AWS CLI、Goは
AWS_CA_BUNDLEを読みます。Nodeでは同じca.pemを指すNODE_EXTRA_CA_CERTSが必要で、JavaはJAVA_TOOL_OPTIONS経由でtruststore.p12を使います。AWS_CA_BUNDLEはそのプロセスが信頼する証明書を丸ごと置き換えるので、本物のHTTPS APIも呼ぶテストには、両方をまとめたバンドルが必要です。 - リセットがない。中身を空にする管理用ルートはないので、テストごとに別のバケット名を使うか、テストスイートの合間にエミュレーターを再起動してください。
- キーは起動のたびに変わる。
.envファイルにコピーせず、認証情報ファイルを指定してください。 - アプリが明示的なエンドポイントを指定して独自にクライアントを作っている場合(たとえばdjango-storagesの
AWS_S3_ENDPOINT_URL)、そちらもAWS_ENDPOINT_URLから設定してください。そうしないと、そのクライアントだけが本物のサービスと話し続けます。
Docker ComposeでAWS Emulatorを動かす#
すでにcompose.yamlがあるプロジェクトなら、次のサービスを足せば済みます。上のDockerfileからイメージをビルドし、エミュレーターのファイルをホームディレクトリではなくプロジェクトフォルダーに置きます。ヘルスチェックも付いているので、docker compose up --waitは準備ができた時点で戻ってきます。
services:
aws:
build: ./aws-emulator # the folder with the Dockerfile
command: ["-p", "9297", "--telemetry", "off"]
ports: ["127.0.0.1:9297:9297"]
environment:
HOME: ${PWD}/.aws-emulator-home
volumes:
- ./.aws-emulator-home/.aws/emulator:${PWD}/.aws-emulator-home/.aws/emulator
mem_limit: 2g
healthcheck:
test: ["CMD", "curl", "-sf", "--cacert", "${PWD}/.aws-emulator-home/.aws/emulator/9297/ca.pem",
"https://127.0.0.1:9297/__admin/health"]
interval: 1s
retries: 30mkdir -p .aws-emulator-home/.aws/emulator
docker compose up -d --wait aws # healthy in about 5 s with the image already built
export E=$PWD/.aws-emulator-home/.aws/emulator/9297 # then the variables from above.aws-emulator-home/は.gitignoreに追加してください。その回のキーが入っています。
pytestからAWS Emulatorを使う#
テストそのものに、エミュレーター固有のものは何もありません。私が足しているのはガードです。心配している失敗は、赤くなるテストではありません。本物のバケットに対して走って緑になったテストのほうです。
# conftest.py
import os
import uuid
import boto3
import pytest
@pytest.fixture(scope="session", autouse=True)
def only_against_the_emulator():
url = os.environ.get("AWS_ENDPOINT_URL", "")
if not url.startswith(("https://127.0.0.1", "http://127.0.0.1")):
pytest.exit(f"AWS_ENDPOINT_URL is {url!r}; refusing to run against real AWS")
@pytest.fixture
def s3():
return boto3.client("s3")
@pytest.fixture
def bucket(s3):
name = f"test-{uuid.uuid4().hex[:12]}" # no reset route, so never reuse a name
s3.create_bucket(Bucket=name)
return nameあとは、ほかと何も変わらないテストを書くだけです。
import os
import requests
def test_presigned_upload_rejects_the_wrong_content_type(s3, bucket):
url = s3.generate_presigned_url(
"put_object", ExpiresIn=300,
Params={"Bucket": bucket, "Key": "media/a.jpg", "ContentType": "image/jpeg"},
)
r = requests.put(url, data=b"<html>", headers={"Content-Type": "text/html"},
verify=os.environ["AWS_CA_BUNDLE"])
assert r.status_code == 403これは自分のマーケットプレイスアプリのアップロード処理から持ってきたもので、どのエミュレーターを選ぶかが大事な理由をよく表しています。私が試した5つのうち3つは、このテストに200を返すのです。詳しくは後述します。変更のたびに影響のあるテストだけを実行しているなら、pytest-testmonもこのまま使えます。
GitHub ActionsでAWS Emulatorを動かす#
GitHubのランナーでは、Dockerは使わずにDebianパッケージをインストールします。あるステップで起動したバックグラウンドプロセスは、ジョブの最後まで動き続けます。
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10
- name: Start AWS Emulator
run: |
curl -fsSLo aws-emulator.deb https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator_amd64.deb
sudo apt-get install -y ./aws-emulator.deb
aws-emulator --telemetry off --idle-timeout 900 > emulator.log 2>&1 &
E=$HOME/.aws/emulator/9297
for i in $(seq 30); do
curl -sf --cacert $E/ca.pem https://127.0.0.1:9297/__admin/health && break
sleep 1
done
{
echo "AWS_ENDPOINT_URL=https://127.0.0.1:9297"
echo "AWS_SHARED_CREDENTIALS_FILE=$E/emulator_credentials"
echo "AWS_CONFIG_FILE=$E/config"
echo "AWS_CA_BUNDLE=$E/ca.pem"
echo "AWS_REGION=us-east-1"
echo "AWS_EC2_METADATA_DISABLED=true"
echo "AWS_DISABLE_HOST_PREFIX_INJECTION=true"
} >> "$GITHUB_ENV"
- run: uv run pytest同じ手順を、まっさらなubuntu:24.04コンテナでも実行してみました(私のMacはARMなので、arm64パッケージを使っています)。インストールは29秒で、そのほとんどは150 MBのダウンロードでした。ヘルスチェックは4秒以内に応答しました。このジョブには、本物のAWS認証情報を一切追加しないでください。ジョブの中の何もAWSに届かないことを確かめる、いちばん安上がりな方法です。
LTS/latestは最後にリリースされたものを指し、後で見るとおり、それは1日に何度も変わります。CIを再現可能にしたいなら、AWSに特定のバージョンをインストールするためのページがあります。パイプラインの残りをどう組んでいるかは、今使っているGitHub Actionsのパイプラインをご覧ください。
LocalStackからの移行#
すでにテストをLocalStackに対して走らせているなら、移行の大半は設定の変更です。
| LocalStack | AWS Emulator | |
|---|---|---|
| エンドポイント | http://localhost:4566 | https://127.0.0.1:9297とAWS_CA_BUNDLE |
| 認証情報 | 何でもよい(test / test) | 起動のたびに書き出されるファイル、または--auth off |
| アカウントID | 000000000000 | 000000000000 |
| セットアップスクリプト | /etc/localstack/init/ready.dのinitフック | なし:ヘルスチェックのあと、フィクスチャでリソースを作る |
| イメージ | localstack/localstack(4.12より後はトークンが必要) | AWSのパッケージから自分でビルドする |
initフックを多用しているなら、そこは本当に手間のかかる部分です。私は使っていませんでした。テストごとに自分のバケットを作るからです。それに、再起動を越えて残るものは何もありません。エミュレーターはすべてをメモリに置いています。
S3のローカルテストでAWS Emulatorが見つけたもの、見逃したもの#
セットアップは簡単なほうです。私が知りたかったのは、自分のコードがS3の挙動に頼っているところで、このエミュレーターがS3と同じように振る舞うかどうかでした。そこでチェックを12個書き、それぞれで、AWSのドキュメントに書かれている本物のS3の挙動をアサートしました。
- 6つはマーケットプレイスアプリのアップロード処理から。アプリはブラウザに署名付きURLを渡し、ブラウザは写真をバケットに直接PUTし(なのでバケットにはCORSが必要です)、
ObjectCreatedイベントがファイルの到着をバックエンドに知らせます。オブジェクト通知についての記事で書いた流れで、Webhookの代わりにSQSを使っています。 - 6つはCodemagic Patchのバケットから。PatchはReact Nativeアプリ向けのOTAアップデートサーバーです(移行した経緯)。スマートフォンがバンドルをダウンロードできるようにバケットは公開されていますが、公開前のアップロードが待機する
_internal/プレフィックスだけは例外で、バケットポリシーでそこを非公開にしています。
これらのチェックを、AWS Emulator 1.0.342(そのままの状態と、--auth offを付けた状態の2通り)、LocalStack 4.12(そのままの状態と、S3_SKIP_SIGNATURE_VALIDATION=0を付けた状態)、サーバーモードのMoto 5.2.6、Floci 1.5.5、そしてPatchのセルフホスト用バンドルで動いているMinIOのフォーク、Siloに対して実行しました。
12個すべてに合格した構成はなく、どれも6〜8個でした。合計点より、それぞれがどのチェックを落としたかのほうが大事なので、私にとって痛かったはずのものから順に挙げます。
AWS Emulatorは権限にいちばん厳しい。私のアプリがJPEG 1枚のために署名する署名付きURLを例にします。デフォルトのLocalStack 4.12、Moto、Flociでは、その同じURLでHTMLファイルをアップロードでき、200が返ってきました。公開ファイルを配信するバケットなら、格納型XSSの火種です。AWS EmulatorはS3と同じく403で拒否し、有効期限が切れたURLも拒否しました(LocalStack 4.12はS3_SKIP_SIGNATURE_VALIDATION=0を設定すれば両方とも検出し、Flociは期限切れだけ検出して、ファイル形式のすり替えは通します)。新しいS3バケットではBlock Public Accessがオンになっていて、オフにするまで公開バケットポリシーが拒否されることを知っていたのもこれだけで、明示的なDenyを部外者だけでなくサーバー自身の署名付きリクエストにも適用したのもこれだけでした。これこそ、テストをすべて素通りして、最初の本番デプロイで失敗するたぐいのミスです。
ブラウザがやることは、まだ何もテストできない。CORSプリフライト(クロスオリジンのPUTの前にブラウザが送るOPTIONSリクエスト)には501 Not Implementedが返ります。また、デフォルトである認証オンの状態では署名のないリクエストをすべて拒否するので、ポリシーで許可していても、公開バケットからの匿名ダウンロードは403になります。どちらの穴も、すでにリポジトリに登録されています。このプロジェクトは既知の穴をissues/things-that-do-not-work/配下のファイルとして管理していて、11月13日時点でその数は6,840件、すでにthings-that-now-work/に移ったものが13,409件ありました。

--auth offにすると、得意と不得意が入れ替わる。認証をオフにすると匿名読み取りが通り、バケットポリシーも正しく評価されるので、Patchの6つのチェックはすべて通ります。それができたのはこの構成だけでした。ただし署名付きURLも検証されなくなるので、アップロード側のチェックは2つまで落ちます。認証ありとなしの2つのエミュレーターを別々のポートで動かせば両方をカバーできますが、次にCIファイルを読む人に説明することが1つ増えます。
動きが速い。v1.0.290からv1.0.342までのリリースが3日で出ていて、どれにも長い修正リストが付いています(AWSによればコーディングエージェントが作ったとのことで、このペースにもそれが表れています)。11月11日の修正の1つは、S3の「presigned GET refused with 403」でした。私の表のどこが来週には変わっていてもおかしくありませんし、良くなる方向にも悪くなる方向にもありえます。CIではバージョンを固定し、上げるときは意図して上げてください。

Patchのバケット:本物のS3でだけ効いてくるポリシーの条件#
ここは予想していなかった部分です。Patchのセルフホスト用バンドルはすべてを1つのバケットに置いていて、同梱のストレージに適用しているポリシーはごく素直なものです。誰でも読めるが、_internal/配下だけは除く、というものです。
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-ota/*" },
{ "Effect": "Deny", "Principal": "*", "Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-ota/_internal/*" }
]
}Siloで管理者の認証情報を使って署名すれば、このポリシーは書いてあるとおりに動きます。部外者は_internal/を読めず、サーバー自身による_internal/から公開プレフィックスへのコピー(「公開」のステップ)は通ります。ところが本物のS3では、"Principal": "*"を指定した明示的なDenyは自分の認証情報も含めて全員に適用されるので、同じポリシーだとサーバー自身の公開ステップがブロックされてしまいます。AWS Emulatorはそのコピーを403で拒否しました。AWSのポリシー評価ルールに書かれているS3の挙動そのものです。
Patchのメンテナーはこのことをよくわかっているようで、AWS S3向けのポリシー例ではDenyに条件を1つ足しています。
"Condition": { "StringNotEquals": { "aws:PrincipalAccount": "YOUR_AWS_ACCOUNT_ID" } }この1行があれば、AWS Emulator(匿名側もテストできるよう--auth offで動かしたもの)とMotoはどちらも、_internal/の匿名読み取りは拒否したまま、ワーカーによる公開を通します。ところが、この1行がないバージョンを見抜いたのはAWS Emulatorだけでした。Moto、LocalStack 4.12、Flociはどちらのバージョンでもコピーを通したので、どれを使っていても、壊れたポリシーをそのまま出荷していたはずです。手に入るなかでいちばん厳しいエミュレーターでポリシーをテストすべきだという、今回見つけたいちばんはっきりした根拠です。MinIOの構成からS3へ、この条件なしでポリシーをコピーすると、最初の本番の公開まで何も文句を言われません。
(私自身のPatchサーバーはバンドルをTigrisに置いていて、Tigrisにはそもそもバケットポリシーがないので、_internal/は別の非公開バケットに分けています。Patchの設定にあるS3_INTERNAL_BUCKETはそのためのものです。また、私はCodemagicのためにPatchについての有料のゲスト記事も書いています。この記事はそのひとつではなく、誰からもお金はもらっていません。)
今ならどれを使うか#
| 起動時間 | アイドル時のメモリ | イメージ | アカウント | |
|---|---|---|---|---|
| AWS Emulator 1.0.342 | 0.4–0.5秒 | 176 MiB | 1.67 GB(自分でビルド) | 不要 |
| LocalStack 4.12 | 0.9–2.2秒 | 90 MiB | 1.89 GB | 4.12は不要、それ以降はトークンが必要 |
| Moto 5.2.6(サーバー) | 0.7–0.8秒 | 45 MiB | 560 MB | 不要 |
| Floci 1.5.5 | 0.3–0.4秒 | 11 MiB | 292 MB | 不要 |
私のMacのDocker上で、それぞれ3回コールドスタートして測りました。どれも、気になるほど遅くはありません。
バックエンドのテストなら、私はAWS Emulatorから始めます。これが捕まえるミス(権限、署名、Block Public Access)は、ほかの方法では本番で初めて表に出るものですし、この中で無料で、最新で、しかもS3を作っている人たち自身が作っているのはこれだけだからです。CORSプリフライトが実装されるまでは、ブラウザ向けのチェック(プリフライトと、公開プレフィックスからの匿名ダウンロード)用に、Flociで動かす小さなジョブを1つ残しておくつもりです。署名を厳格にしたLocalStack 4.12もこれらには通りますが、更新が止まっていて、修正が入ることはありません。テストがPythonで、コンテナを動かしたくないなら、Motoのインプロセスのモックは今でもいちばん軽い選択肢です。ただし、署名付きURLのチェックには頼らないでください。
12個のチェックは、自分のバケット向けに午後いっぱいで書ける程度の短いものです。どんなエミュレーターでも、信頼する前にこれをやっておくことをおすすめします。このエミュレーターも例外ではありません。表のツールはそれぞれ違うチェックで落ちていて、そこがまさに、テストが黙って素通りしてしまう穴なのです。
