<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>LocalStack · 仁才徳</title>
    <link>https://jared.lynskey.co.nz/ja/tags/localstack/</link>
    <description>ソフトウェアエンジニアリングと開発組織のリーダーシップについて。評価、採用とチームの拡大、CI/CD、DevOps モニタリング、エージェンティック AI。ソウルより。</description>
    <generator>Hugo</generator>
    <language>ja</language>
    <copyright>© 2026 仁才徳</copyright>
    <lastBuildDate>Fri, 13 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ja/tags/localstack/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>AWS Emulatorの使い方：無料のLocalStack代替をセットアップ</title>
      <link>https://jared.lynskey.co.nz/ja/posts/2026/aws-emulator-setup/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ja/posts/2026/aws-emulator-setup/</guid>
      <pubDate>Fri, 13 Nov 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>AWS Emulator</category>
      <category>LocalStack</category>
      <category>Moto</category>
      <category>Floci</category>
      <category>S3</category>
      <category>Testing</category>
      <category>Docker</category>
      <category>GitHub Actions</category>
      <category>Python</category>
      <category>Presigned URLs</category>
      <category>OTA Updates</category>
      <description>AWS純正の無料エミュレーターをDocker、Linux、GitHub Actionsでセットアップし、boto3から使うまでの手順です。S3のローカルテストでは、LocalStack 4.12が見逃したミスも捕まえてくれました。</description>
      <content:encoded><![CDATA[<p>AWSは2026年11月11日に<a href="https://github.com/aws/aws-emulator"  target="_blank" rel="noreferrer">AWS Emulator</a>を公開しました。AWSのAPIを手元のマシンで再現するローカル版です。無料で、AWSアカウントは要らず、コードはパブリックドメイン（CC0）、対応サービスは200を超えます。LocalStackが最新イメージの利用にアカウントと認証トークンを求めるようになってから7か月後の登場だったこともあり、待ち望んでいた無料のLocalStack代替として注目している人が少なくありません。</p>
<p>この記事の大半はセットアップの話です。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バンドルを置いているバケットです。</p>

<h2 class="relative group">1分でわかるAWS Emulatorの使い方
    <div id="1分でわかるaws-emulatorの使い方" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1%e5%88%86%e3%81%a7%e3%82%8f%e3%81%8b%e3%82%8baws-emulator%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>AWSはコンテナイメージを公開していません。AWSのLinux用パッケージを入れるだけの9行のDockerfileから自分でビルドし、ポート9297で起動して、環境変数をいくつか設定します。それ以降、SDKはAWSではなくエミュレーターと通信するようになり、コードは一切変わりません。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker build -t aws-emulator .           <span class="c1"># the Dockerfile is below</span>
</span></span><span class="line"><span class="cl">mkdir -p ~/.aws/emulator
</span></span><span class="line"><span class="cl">docker run --rm -d --name aws-emulator -p 127.0.0.1:9297:9297 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -e <span class="nv">HOME</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">&#34;</span> -v <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.aws/emulator:</span><span class="nv">$HOME</span><span class="s2">/.aws/emulator&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  aws-emulator -p <span class="m">9297</span> --telemetry off
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_ENDPOINT_URL</span><span class="o">=</span>https://127.0.0.1:9297
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_SHARED_CREDENTIALS_FILE</span><span class="o">=</span>~/.aws/emulator/9297/emulator_credentials
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_CONFIG_FILE</span><span class="o">=</span>~/.aws/emulator/9297/config
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_CA_BUNDLE</span><span class="o">=</span>~/.aws/emulator/9297/ca.pem
</span></span><span class="line"><span class="cl">aws sts get-caller-identity              <span class="c1"># &#34;Account&#34;: &#34;000000000000&#34;</span></span></span></code></pre></div></div>
<figure><video
    class="my-0 rounded-md"
    autoplay loop muted playsinline preload="metadata"
    poster="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-setup.jpg" style="background:url('data:image/webp;base64,UklGRloAAABXRUJQVlA4IE4AAACQBACdASogABIAPw1yskwmIyIhKAqoxiGJ6QDG9BPdK4I2hDwC1QaVpF521UAA/ui2Gzha/NBVWRJqzq2oTgehY/P2cqAeQAizoM8AAAA=') center/cover no-repeat"
    width="1280"
    height="720"
    aria-label="ターミナル：Dockerfile、AWS Emulatorコンテナの起動、okを返すヘルスチェック、アカウント000000000000を返すaws sts get-caller-identity、S3へのアップロード、HTTP 501が返ってくるCORSプリフライト">
    <source src="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-setup.mp4" type="video/mp4">
  </video><figcaption>Macで録画したセットアップの一部始終です。起動、ヘルスチェック、エミュレーターのアカウント、S3へのアップロード、そしてまだできないことが1つ（CORSプリフライトには501が返ります。詳しくは後述します）。</figcaption>
</figure>

<p>起動は約0.5秒、アイドル時のメモリは約175 MiBです。すべてメモリ上に置かれるので、コンテナを止めると中身は消えます。</p>

<h2 class="relative group">アカウントは必要？（LocalStackの有料化とAWS Emulator）
    <div id="アカウントは必要localstackの有料化とaws-emulator" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%a2%e3%82%ab%e3%82%a6%e3%83%b3%e3%83%88%e3%81%af%e5%bf%85%e8%a6%81localstack%e3%81%ae%e6%9c%89%e6%96%99%e5%8c%96%e3%81%a8aws-emulator" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>必要ありません。AWS Emulator、Moto、Flociは、どれも何にも登録せずに動きます。AWS EmulatorにいたってはAWSの認証情報すら求めません。起動のたびに自前のアクセスキーを作り、そのキーで署名されたリクエストしか受け付けないのです。</p>
<p>変わったのはLocalStackのほうです。2026年3月から、最新イメージには<code>LOCALSTACK_AUTH_TOKEN</code>が必要になりました。トークンは無料アカウント（無料プランは趣味や非商用での利用が対象です）か、有料アカウントで手に入ります。トークンがないと、コンテナは起動時に止まります。実際に両方試してみました。<code>latest</code>タグのイメージ（2026.10.7、11月11日ビルド）は終了コード55で終了しますが、最後のコミュニティ版であり、LocalStack自身が固定先として勧めているバージョンでもある4.12は、今でもトークンなしで起動します。</p>
<figure><video
    class="my-0 rounded-md"
    autoplay loop muted playsinline preload="metadata"
    poster="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/localstack-needs-account.jpg" style="background:url('data:image/webp;base64,UklGRlYAAABXRUJQVlA4IEoAAABQBACdASogABIAPw10pUymIyGhKA1QxiGJ6QDJTPgAtfKr5WoM7/AUVx4AAP7ozNYDExNyKOZLAQjfzKlQvzdAcPxaBgAGLrjAAA==') center/cover no-repeat"
    width="1280"
    height="720"
    aria-label="ターミナル：localstack/localstack:latestが&#39;License activation failed&#39;と終了コード55で終了し、続いてlocalstack/localstack:4.12が起動して、ヘルスチェックがバージョン4.12.0とS3が利用可能であることを報告する">
    <source src="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/localstack-needs-account.mp4" type="video/mp4">
  </video><figcaption>トークンなしで動かしたLocalStackの最新イメージと、バージョンを固定した4.12。今のところ固定すれば動きますが、4.12に修正が入ることはもうありません。</figcaption>
</figure>

<p>AWS Emulatorが求めるものを1つ挙げるなら、テレメトリーです。デフォルトでオンになっています。AWSによれば、リクエストやコード、認証情報が含まれることは一切ありませんが、何も送ってほしくないなら止め方は3つあります。コマンドラインで<code>--telemetry off</code>、環境変数で<code>EMULATOR_TELEMETRY=off</code>、あるいは<code>~/.aws/emulator/config.toml</code>にこう書きます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">telemetry</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">enabled</span> <span class="p">=</span> <span class="kc">false</span></span></span></code></pre></div></div>

<h2 class="relative group">macOSやWindowsでDockerを使ってAWS Emulatorをセットアップする
    <div id="macosやwindowsでdockerを使ってaws-emulatorをセットアップする" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#macos%e3%82%84windows%e3%81%a7docker%e3%82%92%e4%bd%bf%e3%81%a3%e3%81%a6aws-emulator%e3%82%92%e3%82%bb%e3%83%83%e3%83%88%e3%82%a2%e3%83%83%e3%83%97%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>MacやWindowsマシンなら、Dockerを使うことになります。空のフォルダーに、次の内容を<code>Dockerfile</code>として置いてください。AWS自身のもので、<a href="https://docs.aws.amazon.com/emulator/latest/userguide/getting-started-docker.html"  target="_blank" rel="noreferrer">AWSのDockerガイド</a>からそのまま持ってきました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">public.ecr.aws/amazonlinux/amazonlinux:2023-minimal</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ADD</span> https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator-x86_64.rpm /tmp/aws-emulator.rpm<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> rpm -i /tmp/aws-emulator.rpm <span class="o">&amp;&amp;</span> rm /tmp/aws-emulator.rpm <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="o">&amp;&amp;</span> mkdir -p /home/emulator <span class="o">&amp;&amp;</span> chown <span class="m">1000</span> /home/emulator<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">USER</span><span class="w"> </span><span class="s">1000</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">HOME</span><span class="o">=</span>/home/emulator<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">9297</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;aws-emulator&#34;</span><span class="p">,</span> <span class="s2">&#34;-b&#34;</span><span class="p">,</span> <span class="s2">&#34;0.0.0.0&#34;</span><span class="p">]</span></span></span></code></pre></div></div>
<p>Apple siliconをはじめとするARMマシンでは、<code>x86_64.rpm</code>を<code>aarch64.rpm</code>に変えてください。あとはビルドして起動します。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker build -t aws-emulator .
</span></span><span class="line"><span class="cl">mkdir -p ~/.aws/emulator
</span></span><span class="line"><span class="cl">docker run --rm -d --name aws-emulator --memory<span class="o">=</span>2g --memory-swap<span class="o">=</span>2g <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p 127.0.0.1:9297:9297 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -e <span class="nv">HOME</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">&#34;</span> -v <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.aws/emulator:</span><span class="nv">$HOME</span><span class="s2">/.aws/emulator&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  aws-emulator -p <span class="m">9297</span> --telemetry off</span></span></code></pre></div></div>
<p>ビルドには私のMacで88秒かかり、イメージは1.67 GBでした。この<code>docker run</code>の行には、見た目以上に大事な部分が3つあります。</p>
<ul>
<li><strong><code>-p 9297:9297</code>ではなく<code>-p 127.0.0.1:9297:9297</code></strong>。短いほうの書き方だと、エミュレーターがネットワーク全体に公開されてしまいます。</li>
<li><strong>ボリュームと<code>-e HOME</code></strong>。エミュレーターは起動時に<code>~/.aws/emulator/9297/</code>へ4つのファイルを書き出します。認証情報、AWSの<code>config</code>ファイル、証明書（<code>ca.pem</code>）、そしてJavaのトラストストアです。このフォルダーをコンテナ内の同じパスにマウントすることで、ホスト側のSDKがそれらを読めるようになります。</li>
<li><strong>ポートは3か所に出てくる</strong>。変えるなら全部変えてください。エミュレーターはそれらのファイルにポート番号を書き込むので、フォルダーを共有する2つのコンテナはお互いのファイルを上書きしてしまいます。</li>
</ul>

<h2 class="relative group">LinuxにAWS Emulatorをインストールする
    <div id="linuxにaws-emulatorをインストールする" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#linux%e3%81%abaws-emulator%e3%82%92%e3%82%a4%e3%83%b3%e3%82%b9%e3%83%88%e3%83%bc%e3%83%ab%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>LinuxならDockerは要りません。DebianパッケージとRPMが用意されています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Debian / Ubuntu (use aws-emulator_arm64.deb on ARM)</span>
</span></span><span class="line"><span class="cl">curl -fsSLo aws-emulator.deb https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator_amd64.deb
</span></span><span class="line"><span class="cl">sudo apt-get install -y ./aws-emulator.deb
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Amazon Linux, Fedora, RHEL (aarch64.rpm on ARM)</span>
</span></span><span class="line"><span class="cl">sudo dnf install -y https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator-x86_64.rpm
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">aws-emulator --telemetry off</span></span></code></pre></div></div>
<p>パッケージは150 MBです。<code>127.0.0.1:9297</code>でフォアグラウンドで動き、同じ4つのファイルを<code>~/.aws/emulator/9297/</code>に書き出します。Ctrl+Cできれいに止まり、ファイルも削除されます。テストで使うなら<code>--idle-timeout 900</code>を付けておきましょう。15分間リクエストがなければ終了するので、止め忘れたエミュレーターが1週間居座ることもありません。</p>

<h2 class="relative group">AWS CLIとboto3の接続先をエミュレーターに切り替える
    <div id="aws-cliとboto3の接続先をエミュレーターに切り替える" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#aws-cli%e3%81%a8boto3%e3%81%ae%e6%8e%a5%e7%b6%9a%e5%85%88%e3%82%92%e3%82%a8%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%82%bf%e3%83%bc%e3%81%ab%e5%88%87%e3%82%8a%e6%9b%bf%e3%81%88%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>設定する変数はこれです。標準のAWS環境変数を読むSDKならどれでも拾ってくれるので、テスト対象のコードにはエンドポイントもキーも要りません。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">E</span><span class="o">=</span>~/.aws/emulator/9297
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_ENDPOINT_URL</span><span class="o">=</span>https://127.0.0.1:9297
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_SHARED_CREDENTIALS_FILE</span><span class="o">=</span><span class="nv">$E</span>/emulator_credentials
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_CONFIG_FILE</span><span class="o">=</span><span class="nv">$E</span>/config
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_CA_BUNDLE</span><span class="o">=</span><span class="nv">$E</span>/ca.pem
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_REGION</span><span class="o">=</span>us-east-1
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_EC2_METADATA_DISABLED</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_DISABLE_HOST_PREFIX_INJECTION</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="nb">unset</span> AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">curl -s --cacert <span class="nv">$AWS_CA_BUNDLE</span> <span class="nv">$AWS_ENDPOINT_URL</span>/__admin/health   <span class="c1"># ok</span>
</span></span><span class="line"><span class="cl">aws sts get-caller-identity                                          <span class="c1"># 000000000000</span></span></span></code></pre></div></div>
<p>あとは普通のboto3がそのまま動きます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">boto3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">s3</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s2">&#34;s3&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">s3</span><span class="o">.</span><span class="n">create_bucket</span><span class="p">(</span><span class="n">Bucket</span><span class="o">=</span><span class="s2">&#34;uploads&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">s3</span><span class="o">.</span><span class="n">list_buckets</span><span class="p">()[</span><span class="s2">&#34;Buckets&#34;</span><span class="p">])</span></span></span></code></pre></div></div>
<p><code>unset</code>の行は必ず残してください。環境変数は認証情報ファイルより優先されるので、本物のAWSキーがexportされたままだと、SDKはそのキーで署名し、エミュレーターはリクエストを拒否し、あなたは原因を探して10分を無駄にします。<code>aws sts get-caller-identity</code>は手早い確認方法です。エミュレーターのアカウントは常に<code>000000000000</code>なので、自分のアカウント番号が表示されたら、話している相手はAWSです。</p>
<p>ほかにも、実際にはまったこと、はまりかけたことがいくつかあります。</p>
<ul>
<li><strong>エンドポイントは<code>localhost</code>ではなく<code>127.0.0.1</code>のままにする</strong>。IPアドレスなら、SDKはバケット名をパスに入れます。<code>localhost</code>だと、JavaScript、Java、GoのSDKはバケット名をホスト名のほうに移し（<code>uploads.localhost</code>）、エミュレーターはそれを受け付けません。</li>
<li><strong>HTTPSのみで、証明書は起動のたびに新しくなる</strong>。boto3、AWS CLI、Goは<code>AWS_CA_BUNDLE</code>を読みます。Nodeでは同じ<code>ca.pem</code>を指す<code>NODE_EXTRA_CA_CERTS</code>が必要で、Javaは<code>JAVA_TOOL_OPTIONS</code>経由で<code>truststore.p12</code>を使います。<code>AWS_CA_BUNDLE</code>はそのプロセスが信頼する証明書を丸ごと置き換えるので、本物のHTTPS APIも呼ぶテストには、両方をまとめたバンドルが必要です。</li>
<li><strong>リセットがない</strong>。中身を空にする管理用ルートはないので、テストごとに別のバケット名を使うか、テストスイートの合間にエミュレーターを再起動してください。</li>
<li><strong>キーは起動のたびに変わる</strong>。<code>.env</code>ファイルにコピーせず、認証情報ファイルを指定してください。</li>
<li><strong>アプリが明示的なエンドポイントを指定して独自にクライアントを作っている場合</strong>（たとえばdjango-storagesの<code>AWS_S3_ENDPOINT_URL</code>）、そちらも<code>AWS_ENDPOINT_URL</code>から設定してください。そうしないと、そのクライアントだけが本物のサービスと話し続けます。</li>
</ul>

<h2 class="relative group">Docker ComposeでAWS Emulatorを動かす
    <div id="docker-composeでaws-emulatorを動かす" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#docker-compose%e3%81%a7aws-emulator%e3%82%92%e5%8b%95%e3%81%8b%e3%81%99" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>すでに<code>compose.yaml</code>があるプロジェクトなら、次のサービスを足せば済みます。上のDockerfileからイメージをビルドし、エミュレーターのファイルをホームディレクトリではなくプロジェクトフォルダーに置きます。ヘルスチェックも付いているので、<code>docker compose up --wait</code>は準備ができた時点で戻ってきます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">aws</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">./aws-emulator           </span><span class="w"> </span><span class="c"># the folder with the Dockerfile</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;-p&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;9297&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--telemetry&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;off&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;127.0.0.1:9297:9297&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HOME</span><span class="p">:</span><span class="w"> </span><span class="l">${PWD}/.aws-emulator-home</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./.aws-emulator-home/.aws/emulator:${PWD}/.aws-emulator-home/.aws/emulator</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mem_limit</span><span class="p">:</span><span class="w"> </span><span class="l">2g</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;curl&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-sf&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--cacert&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;${PWD}/.aws-emulator-home/.aws/emulator/9297/ca.pem&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">             </span><span class="s2">&#34;https://127.0.0.1:9297/__admin/health&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">1s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">30</span></span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p .aws-emulator-home/.aws/emulator
</span></span><span class="line"><span class="cl">docker compose up -d --wait aws       <span class="c1"># healthy in about 5 s with the image already built</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">E</span><span class="o">=</span><span class="nv">$PWD</span>/.aws-emulator-home/.aws/emulator/9297   <span class="c1"># then the variables from above</span></span></span></code></pre></div></div>
<p><code>.aws-emulator-home/</code>は<code>.gitignore</code>に追加してください。その回のキーが入っています。</p>

<h2 class="relative group">pytestからAWS Emulatorを使う
    <div id="pytestからaws-emulatorを使う" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#pytest%e3%81%8b%e3%82%89aws-emulator%e3%82%92%e4%bd%bf%e3%81%86" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>テストそのものに、エミュレーター固有のものは何もありません。私が足しているのはガードです。心配している失敗は、赤くなるテストではありません。本物のバケットに対して走って緑になったテストのほうです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># conftest.py</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">uuid</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">boto3</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="s2">&#34;session&#34;</span><span class="p">,</span> <span class="n">autouse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">only_against_the_emulator</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;AWS_ENDPOINT_URL&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">url</span><span class="o">.</span><span class="n">startswith</span><span class="p">((</span><span class="s2">&#34;https://127.0.0.1&#34;</span><span class="p">,</span> <span class="s2">&#34;http://127.0.0.1&#34;</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">        <span class="n">pytest</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;AWS_ENDPOINT_URL is </span><span class="si">{</span><span class="n">url</span><span class="si">!r}</span><span class="s2">; refusing to run against real AWS&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">s3</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">boto3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s2">&#34;s3&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">bucket</span><span class="p">(</span><span class="n">s3</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;test-</span><span class="si">{</span><span class="n">uuid</span><span class="o">.</span><span class="n">uuid4</span><span class="p">()</span><span class="o">.</span><span class="n">hex</span><span class="p">[:</span><span class="mi">12</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span>   <span class="c1"># no reset route, so never reuse a name</span>
</span></span><span class="line"><span class="cl">    <span class="n">s3</span><span class="o">.</span><span class="n">create_bucket</span><span class="p">(</span><span class="n">Bucket</span><span class="o">=</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">name</span></span></span></code></pre></div></div>
<p>あとは、ほかと何も変わらないテストを書くだけです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_presigned_upload_rejects_the_wrong_content_type</span><span class="p">(</span><span class="n">s3</span><span class="p">,</span> <span class="n">bucket</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="n">s3</span><span class="o">.</span><span class="n">generate_presigned_url</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;put_object&#34;</span><span class="p">,</span> <span class="n">ExpiresIn</span><span class="o">=</span><span class="mi">300</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">Params</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Bucket&#34;</span><span class="p">:</span> <span class="n">bucket</span><span class="p">,</span> <span class="s2">&#34;Key&#34;</span><span class="p">:</span> <span class="s2">&#34;media/a.jpg&#34;</span><span class="p">,</span> <span class="s2">&#34;ContentType&#34;</span><span class="p">:</span> <span class="s2">&#34;image/jpeg&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="sa">b</span><span class="s2">&#34;&lt;html&gt;&#34;</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Content-Type&#34;</span><span class="p">:</span> <span class="s2">&#34;text/html&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">                     <span class="n">verify</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;AWS_CA_BUNDLE&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">r</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">403</span></span></span></code></pre></div></div>
<p>これは自分のマーケットプレイスアプリのアップロード処理から持ってきたもので、どのエミュレーターを選ぶかが大事な理由をよく表しています。私が試した5つのうち3つは、このテストに200を返すのです。詳しくは後述します。変更のたびに影響のあるテストだけを実行しているなら、<a href="https://jared.lynskey.co.nz/ja/posts/2026/2026-10-26-pytest-testmon/" >pytest-testmon</a>もこのまま使えます。</p>

<h2 class="relative group">GitHub ActionsでAWS Emulatorを動かす
    <div id="github-actionsでaws-emulatorを動かす" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#github-actions%e3%81%a7aws-emulator%e3%82%92%e5%8b%95%e3%81%8b%e3%81%99" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>GitHubのランナーでは、Dockerは使わずにDebianパッケージをインストールします。あるステップで起動したバックグラウンドプロセスは、ジョブの最後まで動き続けます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">astral-sh/setup-uv@v10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Start AWS Emulator</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          curl -fsSLo aws-emulator.deb https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator_amd64.deb
</span></span></span><span class="line"><span class="cl"><span class="sd">          sudo apt-get install -y ./aws-emulator.deb
</span></span></span><span class="line"><span class="cl"><span class="sd">          aws-emulator --telemetry off --idle-timeout 900 &gt; emulator.log 2&gt;&amp;1 &amp;
</span></span></span><span class="line"><span class="cl"><span class="sd">          E=$HOME/.aws/emulator/9297
</span></span></span><span class="line"><span class="cl"><span class="sd">          for i in $(seq 30); do
</span></span></span><span class="line"><span class="cl"><span class="sd">            curl -sf --cacert $E/ca.pem https://127.0.0.1:9297/__admin/health &amp;&amp; break
</span></span></span><span class="line"><span class="cl"><span class="sd">            sleep 1
</span></span></span><span class="line"><span class="cl"><span class="sd">          done
</span></span></span><span class="line"><span class="cl"><span class="sd">          {
</span></span></span><span class="line"><span class="cl"><span class="sd">            echo &#34;AWS_ENDPOINT_URL=https://127.0.0.1:9297&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">            echo &#34;AWS_SHARED_CREDENTIALS_FILE=$E/emulator_credentials&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">            echo &#34;AWS_CONFIG_FILE=$E/config&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">            echo &#34;AWS_CA_BUNDLE=$E/ca.pem&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">            echo &#34;AWS_REGION=us-east-1&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">            echo &#34;AWS_EC2_METADATA_DISABLED=true&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">            echo &#34;AWS_DISABLE_HOST_PREFIX_INJECTION=true&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">          } &gt;&gt; &#34;$GITHUB_ENV&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">uv run pytest</span></span></span></code></pre></div></div>
<p>同じ手順を、まっさらな<code>ubuntu:24.04</code>コンテナでも実行してみました（私のMacはARMなので、arm64パッケージを使っています）。インストールは29秒で、そのほとんどは150 MBのダウンロードでした。ヘルスチェックは4秒以内に応答しました。このジョブには、本物のAWS認証情報を一切追加しないでください。ジョブの中の何もAWSに届かないことを確かめる、いちばん安上がりな方法です。</p>
<p><code>LTS/latest</code>は最後にリリースされたものを指し、後で見るとおり、それは1日に何度も変わります。CIを再現可能にしたいなら、AWSに<a href="https://docs.aws.amazon.com/emulator/latest/userguide/versioning.html"  target="_blank" rel="noreferrer">特定のバージョンをインストールする</a>ためのページがあります。パイプラインの残りをどう組んでいるかは、<a href="https://jared.lynskey.co.nz/ja/posts/2026/2026-10-22-docker-compose-ci-github-actions/" >今使っているGitHub Actionsのパイプライン</a>をご覧ください。</p>

<h2 class="relative group">LocalStackからの移行
    <div id="localstackからの移行" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#localstack%e3%81%8b%e3%82%89%e3%81%ae%e7%a7%bb%e8%a1%8c" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>すでにテストをLocalStackに対して走らせているなら、移行の大半は設定の変更です。</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>LocalStack</th>
					<th>AWS Emulator</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>エンドポイント</td>
					<td><code>http://localhost:4566</code></td>
					<td><code>https://127.0.0.1:9297</code>と<code>AWS_CA_BUNDLE</code></td>
			</tr>
			<tr>
					<td>認証情報</td>
					<td>何でもよい（<code>test</code> / <code>test</code>）</td>
					<td>起動のたびに書き出されるファイル、または<code>--auth off</code></td>
			</tr>
			<tr>
					<td>アカウントID</td>
					<td><code>000000000000</code></td>
					<td><code>000000000000</code></td>
			</tr>
			<tr>
					<td>セットアップスクリプト</td>
					<td><code>/etc/localstack/init/ready.d</code>のinitフック</td>
					<td>なし：ヘルスチェックのあと、フィクスチャでリソースを作る</td>
			</tr>
			<tr>
					<td>イメージ</td>
					<td><code>localstack/localstack</code>（4.12より後はトークンが必要）</td>
					<td>AWSのパッケージから自分でビルドする</td>
			</tr>
	</tbody>
</table>
<p>initフックを多用しているなら、そこは本当に手間のかかる部分です。私は使っていませんでした。テストごとに自分のバケットを作るからです。それに、再起動を越えて残るものは何もありません。エミュレーターはすべてをメモリに置いています。</p>

<h2 class="relative group">S3のローカルテストでAWS Emulatorが見つけたもの、見逃したもの
    <div id="s3のローカルテストでaws-emulatorが見つけたもの見逃したもの" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#s3%e3%81%ae%e3%83%ad%e3%83%bc%e3%82%ab%e3%83%ab%e3%83%86%e3%82%b9%e3%83%88%e3%81%a7aws-emulator%e3%81%8c%e8%a6%8b%e3%81%a4%e3%81%91%e3%81%9f%e3%82%82%e3%81%ae%e8%a6%8b%e9%80%83%e3%81%97%e3%81%9f%e3%82%82%e3%81%ae" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>セットアップは簡単なほうです。私が知りたかったのは、自分のコードがS3の挙動に頼っているところで、このエミュレーターがS3と同じように振る舞うかどうかでした。そこでチェックを12個書き、それぞれで、AWSのドキュメントに書かれている本物のS3の挙動をアサートしました。</p>
<ul>
<li><strong>6つはマーケットプレイスアプリのアップロード処理から</strong>。アプリはブラウザに署名付きURLを渡し、ブラウザは写真をバケットに直接PUTし（なのでバケットにはCORSが必要です）、<code>ObjectCreated</code>イベントがファイルの到着をバックエンドに知らせます。<a href="https://jared.lynskey.co.nz/ja/posts/2026/tigris-object-notifications/" >オブジェクト通知についての記事</a>で書いた流れで、Webhookの代わりにSQSを使っています。</li>
<li><strong>6つはCodemagic Patchのバケットから</strong>。PatchはReact Nativeアプリ向けのOTAアップデートサーバーです（<a href="https://jared.lynskey.co.nz/ja/posts/2026/2026-09-13-moving-to-codemagic-patch/" >移行した経緯</a>）。スマートフォンがバンドルをダウンロードできるようにバケットは公開されていますが、公開前のアップロードが待機する<code>_internal/</code>プレフィックスだけは例外で、バケットポリシーでそこを非公開にしています。</li>
</ul>
<p>これらのチェックを、AWS Emulator 1.0.342（そのままの状態と、<code>--auth off</code>を付けた状態の2通り）、LocalStack 4.12（そのままの状態と、<code>S3_SKIP_SIGNATURE_VALIDATION=0</code>を付けた状態）、サーバーモードのMoto 5.2.6、Floci 1.5.5、そしてPatchのセルフホスト用バンドルで動いているMinIOのフォーク、Siloに対して実行しました。</p>
<figure><video
    class="my-0 rounded-md"
    autoplay loop muted playsinline preload="metadata"
    poster="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/s3-emulator-test-run.jpg" style="background:url('data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAAAQBACdASogABIAPw10rkymIyIhKAqoxiGJ6QDM0PgAtf/5Tl8J29EIQAD+6MzcUMCFo8DULSYQXf8FTN9rmZMAAAA=') center/cover no-repeat"
    width="1280"
    height="720"
    aria-label="ターミナル：スクリプトが同じ12個のS3チェックを7つのエミュレーター構成に対して実行し、合否の表を出力する。どの構成も12個中6〜8個で終わる">
    <source src="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/s3-emulator-test-run.mp4" type="video/mp4">
  </video><figcaption>実際の実行結果です。12個すべてに通ったものはありませんでした。</figcaption>
</figure>

<p>12個すべてに合格した構成はなく、どれも6〜8個でした。合計点より、それぞれがどのチェックを落としたかのほうが大事なので、私にとって痛かったはずのものから順に挙げます。</p>
<p><strong>AWS Emulatorは権限にいちばん厳しい</strong>。私のアプリがJPEG 1枚のために署名する署名付きURLを例にします。デフォルトのLocalStack 4.12、Moto、Flociでは、その同じURLでHTMLファイルをアップロードでき、200が返ってきました。公開ファイルを配信するバケットなら、格納型XSSの火種です。AWS EmulatorはS3と同じく403で拒否し、有効期限が切れたURLも拒否しました（LocalStack 4.12は<code>S3_SKIP_SIGNATURE_VALIDATION=0</code>を設定すれば両方とも検出し、Flociは期限切れだけ検出して、ファイル形式のすり替えは通します）。新しいS3バケットではBlock Public Accessがオンになっていて、オフにするまで公開バケットポリシーが拒否されることを知っていたのもこれだけで、明示的な<code>Deny</code>を部外者だけでなくサーバー自身の署名付きリクエストにも適用したのもこれだけでした。これこそ、テストをすべて素通りして、最初の本番デプロイで失敗するたぐいのミスです。</p>
<p><strong>ブラウザがやることは、まだ何もテストできない</strong>。CORSプリフライト（クロスオリジンのPUTの前にブラウザが送る<code>OPTIONS</code>リクエスト）には<code>501 Not Implemented</code>が返ります。また、デフォルトである認証オンの状態では署名のないリクエストをすべて拒否するので、ポリシーで許可していても、公開バケットからの匿名ダウンロードは403になります。どちらの穴も、すでにリポジトリに登録されています。このプロジェクトは既知の穴を<code>issues/things-that-do-not-work/</code>配下のファイルとして管理していて、11月13日時点でその数は6,840件、すでに<code>things-that-now-work/</code>に移ったものが13,409件ありました。</p>
<p><figure><span class="mc-img"><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
    alt="GitHubのthings-that-do-not-workフォルダーにある、S3についてのAWS Emulatorのissueファイル。タイトルは「7 net-new causes — CORS preflight answers 501…」"
    width="1400"
    height="804"
    src="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-cors-preflight-issue_hu_33033b091b284c0d.webp"
    srcset="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-cors-preflight-issue_hu_33033b091b284c0d.webp 800w, https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-cors-preflight-issue_hu_f0294a8a0a022038.webp 1280w"
    sizes="(min-width: 768px) 50vw, 65vw"
    data-zoom-src="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-cors-preflight-issue_hu_925176ba1af6cb05.webp"></span><figcaption>CORSの穴。11月9日に、プロジェクト自身のissue探しエージェントが登録したものです。</figcaption></figure>
</p>
<p><strong><code>--auth off</code>にすると、得意と不得意が入れ替わる</strong>。認証をオフにすると匿名読み取りが通り、バケットポリシーも正しく評価されるので、Patchの6つのチェックはすべて通ります。それができたのはこの構成だけでした。ただし署名付きURLも検証されなくなるので、アップロード側のチェックは2つまで落ちます。認証ありとなしの2つのエミュレーターを別々のポートで動かせば両方をカバーできますが、次にCIファイルを読む人に説明することが1つ増えます。</p>
<p><strong>動きが速い</strong>。v1.0.290からv1.0.342までのリリースが3日で出ていて、どれにも長い修正リストが付いています（AWSによればコーディングエージェントが作ったとのことで、このペースにもそれが表れています）。11月11日の修正の1つは、S3の「presigned GET refused with 403」でした。私の表のどこが来週には変わっていてもおかしくありませんし、良くなる方向にも悪くなる方向にもありえます。CIではバージョンを固定し、上げるときは意図して上げてください。</p>
<p><figure><span class="mc-img"><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
    alt="GitHubのaws/aws-emulatorのリリースページ：3時間前にリリースされたv1.0.342にLatestの印が付き、サイドバーにはさらに9つのリリースが並んでいる"
    width="1600"
    height="1125"
    src="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-releases_hu_e39a0eec6b783a7a.webp"
    srcset="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-releases_hu_e39a0eec6b783a7a.webp 800w, https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-releases_hu_df197ee02b7c435c.webp 1280w"
    sizes="(min-width: 768px) 50vw, 65vw"
    data-zoom-src="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/aws-emulator-releases_hu_59bd3abd9b9aaf92.webp"></span><figcaption>3日で10リリース。バージョンは固定しましょう。</figcaption></figure>
</p>

<h2 class="relative group">Patchのバケット：本物のS3でだけ効いてくるポリシーの条件
    <div id="patchのバケット本物のs3でだけ効いてくるポリシーの条件" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#patch%e3%81%ae%e3%83%90%e3%82%b1%e3%83%83%e3%83%88%e6%9c%ac%e7%89%a9%e3%81%aes3%e3%81%a7%e3%81%a0%e3%81%91%e5%8a%b9%e3%81%84%e3%81%a6%e3%81%8f%e3%82%8b%e3%83%9d%e3%83%aa%e3%82%b7%e3%83%bc%e3%81%ae%e6%9d%a1%e4%bb%b6" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ここは予想していなかった部分です。Patchのセルフホスト用バンドルはすべてを1つのバケットに置いていて、同梱のストレージに適用しているポリシーはごく素直なものです。誰でも読めるが、<code>_internal/</code>配下だけは除く、というものです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;Version&#34;</span><span class="p">:</span> <span class="s2">&#34;2012-10-17&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;Statement&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nt">&#34;Effect&#34;</span><span class="p">:</span> <span class="s2">&#34;Allow&#34;</span><span class="p">,</span> <span class="nt">&#34;Principal&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span> <span class="nt">&#34;Action&#34;</span><span class="p">:</span> <span class="s2">&#34;s3:GetObject&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Resource&#34;</span><span class="p">:</span> <span class="s2">&#34;arn:aws:s3:::example-ota/*&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="nt">&#34;Effect&#34;</span><span class="p">:</span> <span class="s2">&#34;Deny&#34;</span><span class="p">,</span> <span class="nt">&#34;Principal&#34;</span><span class="p">:</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span> <span class="nt">&#34;Action&#34;</span><span class="p">:</span> <span class="s2">&#34;s3:GetObject&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Resource&#34;</span><span class="p">:</span> <span class="s2">&#34;arn:aws:s3:::example-ota/_internal/*&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Siloで管理者の認証情報を使って署名すれば、このポリシーは書いてあるとおりに動きます。部外者は<code>_internal/</code>を読めず、サーバー自身による<code>_internal/</code>から公開プレフィックスへのコピー（「公開」のステップ）は通ります。ところが本物のS3では、<code>&quot;Principal&quot;: &quot;*&quot;</code>を指定した明示的な<code>Deny</code>は自分の認証情報も含めて全員に適用されるので、同じポリシーだとサーバー自身の公開ステップがブロックされてしまいます。AWS Emulatorはそのコピーを403で拒否しました。AWSのポリシー評価ルールに書かれているS3の挙動そのものです。</p>
<p>Patchのメンテナーはこのことをよくわかっているようで、<a href="https://github.com/codemagic-ci-cd/codemagic-patch/blob/main/deploy/selfhost/aws-s3-bucket-policy.example.json"  target="_blank" rel="noreferrer">AWS S3向けのポリシー例</a>では<code>Deny</code>に条件を1つ足しています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;Condition&#34;</span><span class="err">:</span> <span class="p">{</span> <span class="nt">&#34;StringNotEquals&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;aws:PrincipalAccount&#34;</span><span class="p">:</span> <span class="s2">&#34;YOUR_AWS_ACCOUNT_ID&#34;</span> <span class="p">}</span> <span class="p">}</span></span></span></code></pre></div></div>
<p>この1行があれば、AWS Emulator（匿名側もテストできるよう<code>--auth off</code>で動かしたもの）とMotoはどちらも、<code>_internal/</code>の匿名読み取りは拒否したまま、ワーカーによる公開を通します。ところが、この1行がないバージョンを見抜いたのはAWS Emulatorだけでした。Moto、LocalStack 4.12、Flociはどちらのバージョンでもコピーを通したので、どれを使っていても、壊れたポリシーをそのまま出荷していたはずです。手に入るなかでいちばん厳しいエミュレーターでポリシーをテストすべきだという、今回見つけたいちばんはっきりした根拠です。MinIOの構成からS3へ、この条件なしでポリシーをコピーすると、最初の本番の公開まで何も文句を言われません。</p>
<p>（私自身のPatchサーバーはバンドルをTigrisに置いていて、Tigrisにはそもそもバケットポリシーがないので、<code>_internal/</code>は別の非公開バケットに分けています。Patchの設定にある<code>S3_INTERNAL_BUCKET</code>はそのためのものです。また、私はCodemagicのためにPatchについての有料のゲスト記事も書いています。この記事はそのひとつではなく、誰からもお金はもらっていません。）</p>

<h2 class="relative group">今ならどれを使うか
    <div id="今ならどれを使うか" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bb%8a%e3%81%aa%e3%82%89%e3%81%a9%e3%82%8c%e3%82%92%e4%bd%bf%e3%81%86%e3%81%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>起動時間</th>
					<th>アイドル時のメモリ</th>
					<th>イメージ</th>
					<th>アカウント</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>AWS Emulator 1.0.342</td>
					<td>0.4–0.5秒</td>
					<td>176 MiB</td>
					<td>1.67 GB（自分でビルド）</td>
					<td>不要</td>
			</tr>
			<tr>
					<td>LocalStack 4.12</td>
					<td>0.9–2.2秒</td>
					<td>90 MiB</td>
					<td>1.89 GB</td>
					<td>4.12は不要、それ以降はトークンが必要</td>
			</tr>
			<tr>
					<td>Moto 5.2.6（サーバー）</td>
					<td>0.7–0.8秒</td>
					<td>45 MiB</td>
					<td>560 MB</td>
					<td>不要</td>
			</tr>
			<tr>
					<td>Floci 1.5.5</td>
					<td>0.3–0.4秒</td>
					<td>11 MiB</td>
					<td>292 MB</td>
					<td>不要</td>
			</tr>
	</tbody>
</table>
<p>私のMacのDocker上で、それぞれ3回コールドスタートして測りました。どれも、気になるほど遅くはありません。</p>
<p>バックエンドのテストなら、私はAWS Emulatorから始めます。これが捕まえるミス（権限、署名、Block Public Access）は、ほかの方法では本番で初めて表に出るものですし、この中で無料で、最新で、しかもS3を作っている人たち自身が作っているのはこれだけだからです。CORSプリフライトが実装されるまでは、ブラウザ向けのチェック（プリフライトと、公開プレフィックスからの匿名ダウンロード）用に、Flociで動かす小さなジョブを1つ残しておくつもりです。署名を厳格にしたLocalStack 4.12もこれらには通りますが、更新が止まっていて、修正が入ることはありません。テストがPythonで、コンテナを動かしたくないなら、Motoのインプロセスのモックは今でもいちばん軽い選択肢です。ただし、署名付きURLのチェックには頼らないでください。</p>
<p>12個のチェックは、自分のバケット向けに午後いっぱいで書ける程度の短いものです。どんなエミュレーターでも、信頼する前にこれをやっておくことをおすすめします。このエミュレーターも例外ではありません。表のツールはそれぞれ違うチェックで落ちていて、そこがまさに、テストが黙って素通りしてしまう穴なのです。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/aws-emulator-setup/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
