ローカル開発におけるAWSエミュレーション手法の比較
約半年前、私たちのチームはマイクロサービスシステムのリファクタリングプロジェクトを担当しました。そのシステムでは、ユーザー画像の保存にS3、バックグラウンドワーカーのキューにSQS、カートセッションの管理にDynamoDBを使用していました。しかし、各開発者に独立したテスト環境が必要になったことで問題が発生しました。テスト用に作成したリソースの削除忘れが重なり、その月のAWS利用料金が400ドル以上も跳ね上がってしまったのです。
そこでチームで議論を重ね、以下の3つのアプローチを検討しました。
- アプローチ1:クラウド上に開発専用のAWSアカウント(サンドボックス環境)を用意する: 開発者ごとに個別のアカウントやIAMユーザーを割り当てます。機能や動作は本番環境と100%同じです。
- アプローチ2:コード内でモックライブラリ(PythonのMotoやNode.jsのaws-sdk-mockなど)を使用する: アプリケーション層でSDKの呼び出し関数をモック化して単体テストを作成します。
- アプローチ3:Docker ComposeでLocalStackを実行する: ローカル環境にLocalStackコンテナを立ち上げ、AWS SDKと互換性のあるエンドポイントを提供します。
各アプローチのメリット・デメリット分析
3つの手法を実際の開発で2スプリント試用した結果、以下のような特徴が見えてきました。
1. クラウド上のAWS開発用アカウント
- メリット: 本番環境との完全な一致性。IAM権限の仕組み、ネットワークレイテンシ、最新機能の挙動がすべて本番環境と完全に一致します。
- デメリット: コストが高い点。安定したインターネット接続が必須であり、テスト終了後の不要リソースのクリーンアップに手間がかかります。また、ポリシーの設定ミスやLambdaとSQS間の無限ループなどが発生した場合、一晩で利用料金が跳ね上がるリスクもあります。
2. ソースコード内でのモックライブラリ(Moto、aws-sdk-mock)
- メリット: 実行速度が非常に高速。追加のランタイムや外部コンテナをインストールする必要がありません。
- デメリット: 単体テスト(Unit Test)に用途が限定される点。サービス間の結合テスト(Integration Test)—例えばService AがSQSにイベントを送信し、Service Bがそれを処理してS3にファイルを保存するようなケース—では、コードのモック化が複雑化し、実際のネットワーク通信フローを検証できません。
3. Docker ComposeによるLocalStack
- メリット: ローカル環境でAWSのHTTPエンドポイントを完全にエミュレート可能。バックエンドコードのロジックはそのままに、
endpoint_urlをlocalhostに向けるだけで動作します。完全無料でオフライン作業にも対応し、docker compose downコマンド1発で環境を綺麗に破棄できます。 - デメリット: Community版では主要なコアサービスのみのサポートにとどまる点。また、LocalStackコンテナの稼働時に約1GB〜1.5GBのメモリ(RAM)を消費します。
なぜDocker ComposeとLocalStackの組み合わせが最適なのか?
8名の開発メンバーおよびCI/CDパイプラインに導入して半年が経過しましたが、この構成はコストと利便性の課題を見事に解決してくれました。新規参画メンバーもリポジトリをクローンしてDockerコマンドを1回実行するだけで、Web、API、ワーカーまでテスト可能な仮想AWSインフラが即座に整います。
Docker Composeを使ったLocalStackの構築手順
ステップ1:docker-compose.ymlの作成
プロジェクトのルートディレクトリにdocker-compose.ymlを作成し、以下の内容を記述します。
version: '3.8'
services:
localstack:
container_name: localstack-dev
image: localstack/localstack:latest
ports:
- "127.0.0.1:4566:4566" # すべてのAWSサービス共通のメインゲートウェイ
- "127.0.0.1:4510-4559:4510-4559" # 必要に応じた外部サービス用ポート範囲
environment:
- DEBUG=1
- DOCKER_HOST=unix:///var/run/docker.sock
- AWS_DEFAULT_REGION=ap-southeast-1
volumes:
- "./localstack_data:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
バックグラウンドでコンテナを起動します:
docker compose up -d
ステップ2:接続確認のためのAWS CLI設定
LocalStackは認証情報の妥当性を検証しないため、任意のダミー値を設定できます:
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=ap-southeast-1
export LOCALSTACK_URL=http://localhost:4566
ステップ3:S3・SQS・DynamoDBの実践的な操作
1. S3の操作:
# 新規バケットの作成
aws --endpoint-url=$LOCALSTACK_URL s3 mb s3://itfromzero-bucket
# S3にファイルをアップロード
echo "Hello from LocalStack" > test.txt
aws --endpoint-url=$LOCALSTACK_URL s3 cp test.txt s3://itfromzero-bucket/
# ファイル一覧の取得
aws --endpoint-url=$LOCALSTACK_URL s3 ls s3://itfromzero-bucket/
2. SQSの操作:
# SQSキューの作成
aws --endpoint-url=$LOCALSTACK_URL sqs create-queue --queue-name order-processing-queue
# キューにサンプルメッセージを送信
aws --endpoint-url=$LOCALSTACK_URL sqs send-message \
--queue-url http://localhost:4566/000000000000/order-processing-queue \
--message-body '{"order_id": 1024, "status": "pending"}'
# キューからメッセージを受信
aws --endpoint-url=$LOCALSTACK_URL sqs receive-message \
--queue-url http://localhost:4566/000000000000/order-processing-queue
3. DynamoDBの操作:
# Usersテーブルの作成
aws --endpoint-url=$LOCALSTACK_URL dynamodb create-table \
--table-name Users \
--attribute-definitions AttributeName=UserId,AttributeType=S \
--key-schema AttributeName=UserId,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
# テーブルにレコードを追加
aws --endpoint-url=$LOCALSTACK_URL dynamodb put-item \
--table-name Users \
--item '{"UserId": {"S": "usr_01"}, "Name": {"S": "Nguyen Van A"}}'
# 全データのスキャンクエリ
aws --endpoint-url=$LOCALSTACK_URL dynamodb scan --table-name Users
ターミナルでの迅速なJSONデバッグのTips
CLI経由でDynamoDBやSQSを操作する際、ターミナルには未整形のJSON文字列が出力され、視認性が悪くなりがちです。ターミナル上でjqを直接利用するか、レスポンスをtoolcraft.app/ja/tools/developer/json-formatterに貼り付けて整形・データ構造の確認を行うと効率的です。
ステップ4:バックエンドアプリケーションとの接続(Python / Boto3の例)
アプリケーション側では、開発環境であることを判定し、AWSクライアントの初期化時にendpoint_urlパラメータを指定するだけで連携できます:
import os
import boto3
is_dev = os.getenv("ENV", "development") == "development"
endpoint_url = "http://localhost:4566" if is_dev else None
# S3クライアントの初期化
s3_client = boto3.client(
"s3",
endpoint_url=endpoint_url,
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID", "test"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", "test"),
region_name="ap-southeast-1"
)
# バケット一覧の取得
response = s3_client.list_buckets()
print("既存のバケット一覧:", [b['Name'] for b in response.get('Buckets', [])])
実践運用のポイントと注意点
- 起動時のリソース自動作成: シェルスクリプトを格納したディレクトリをコンテナ内の
/etc/localstack/init/ready.d/にマウントします。LocalStackの準備が整い次第スクリプトが自動実行されるため、upするたびに手動でコマンドを叩かなくてもバケットやキューを自動生成できます。 - ボリューム容量の管理:
./localstack_dataディレクトリにより再起動後も状態が永続化されます。ただし、大容量ファイルのテストを数週間続けると容量が数GBに肥大化することがあります。コンテナの起動が遅くなった場合は適宜削除してください。 - ポート4566への統合: 最新のLocalStackではEdge Routerを介して全サービスがポート
4566に統一されています。旧バージョンのように4572や4576といった個別ポートを覚える必要はありません。

