💭

LocalStackでAWSをローカル再現してみた

に公開

はじめまして。エンジニア歴2ヶ月、WEELでFastAPIを用いたAIアプリケーション開発に取り組んでいる瀬戸口です。

今回は、私が初めての実務経験の中で触れた技術のひとつ、「LocalStack」についてご紹介します。

AWSの各種サービスをローカルで再現できる便利なツールで、開発中に助けられた場面がありました。同じような課題を抱えている方の参考になれば幸いです。

AWSの挙動、ローカルで手軽に確認できたら…

開発中に「S3の動作を試したい」「Lambda関数の挙動を検証したい」など、AWS周りの挙動をローカル環境で気軽に確認したいと思ったことはありませんか?

私が担当していたあるプロジェクトでも、まさにそんな課題に直面しました。クライアント側のAWSアカウントを利用してS3を操作する必要があったのですが、セッショントークンやアクセスキーが一定時間ごとに自動で更新される仕様だったため、ローカルテストのたびに環境変数を手動で更新する必要があり、開発効率が非常に悪い状態でした。

そこで、私たちのプロジェクトではLocalStackを導入することにしました。今回はその経験をもとに、LocalStackの概要や導入方法、実際の構成についてご紹介していきます。

localstackとは

LocalStackは、AWSのクラウドサービスをローカル環境で擬似的に再現できるツールです。

本番のAWS環境に依存せず、S3 や Lambda、DynamoDB などの挙動をローカルで再現できるため、テストの高速化やクラウド利用コストの削減といったメリットがあります。

対応しているサービスも多く、S3、DynamoDB、Lambda、SNS、SQS など、幅広いAWS機能を模擬的に利用できます。

https://www.localstack.cloud

導入方法

https://docs.localstack.cloud/aws/getting-started/installation/#docker-compose

この記事では、実際のプロジェクトと同様に、Docker環境上にLocalStackを立ち上げて、Python公式のAWS SDK「boto3」からS3操作を行う構成を紹介します。

なお、LocalStack の Community版(無料版)では、データの永続化がサポートされておらず、コンテナを再起動するたびに作成した S3 バケットやオブジェクトなどのリソースはすべて失われます。

そのため、毎回の起動時に S3 バケットを自動作成できるように、初期化スクリプト(init-s3.sh)を用意し、それを実行する専用コンテナ localstack-init を構成に追加しています。

ディレクトリ構造

.
├── backend
│   ├── Dockerfile
│   └── upload_file.py
├── docker-compose.yml
├── images
│   └── 今回はここにs3に保存したい画像を入れます。
└── localstack-init
    └── init-s3.sh
  • backend/:boto3を使ったファイルアップロードスクリプトを含むPythonアプリケーション
  • localstack-init/:S3バケット初期化用スクリプト
  • docker-compose.yml:LocalStackおよび初期化・バックエンドコンテナを定義

docker-compose.yml

services:
  localstack:
    container_name: localstack
    image: localstack/localstack
    ports:
      - "4566:4566"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    healthcheck:
      test: ["CMD", "curl", "http://localhost:4566/_localstack/health"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 15s

  localstack_init:
    image: amazon/aws-cli
    container_name: localstack-init
    depends_on:
      - localstack
    volumes:
      - ./localstack-init:/scripts
      - ./localstack-init/.localstack.env:/var/.localstack.env
    working_dir: /scripts
    entrypoint: >
      sh -c "
        echo 'Running init-s3.sh...' &&
        chmod +x ./init-s3.sh &&
        ./init-s3.sh &&
        echo 'Done.'
      "
    tty: true

  backend:
    build: ./backend
    container_name: backend
    volumes:
      - ./backend:/app
      - ./images:/images
    working_dir: /app
    depends_on:
      - localstack
    tty: true

localstack : AWS の代替(S3)をローカルに立てる
localstack_init : S3バケットを自動作成する初期化処理
backend : boto3 を使ってファイルアップロードなどを行うPythonアプリ

backend

backend/Dockerfile

FROM python:3.9-slim

WORKDIR /app

RUN pip install --no-cache-dir boto3

COPY . .

requirements.txt は使わず、Dockerfile 内で直接 boto3 をインストールしています。

backend/upload_file.py

import boto3

# S3クライアントをLocalStackに接続
s3 = boto3.client(
    "s3",
    aws_access_key_id="dummy",
    aws_secret_access_key="dummy",
    endpoint_url="http://localstack:4566",
    region_name="ap-northeast-1"
)

bucket_name = "test-bucket"
upload_file = "ねこ.jpeg"

# ファイルをアップロード
s3.upload_file(f"/images/{upload_file}", bucket_name, upload_file)
print(f"ファイルをアップロードしました: /images/{upload_file}{bucket_name}/{upload_file}")

# バケット内のファイル一覧を表示
print(f"\nバケット「{bucket_name}」の中身:")

response = s3.list_objects_v2(Bucket=bucket_name)
if "Contents" in response:
    for obj in response["Contents"]:
        print(f" - {obj['Key']}")
else:
    print("ファイルは見つかりませんでした。")

LocalStack の S3 バケット(bucket_name)に、upload_file をアップロードし、そのバケットの中身を一覧表示します。

今回は例としてねこちゃんの画像をアップロードしているため ねこ.jpeg という名前にしています。各自、任意の画像を images ディレクトリに配置し、ファイル名を変更してください。

通常 boto3 はAWSクラウドに接続しますが、ここでは LocalStackを使っています。

aws_access_key_id / aws_secret_access_key は LocalStack では使われないので 適当な文字列(dummy) を入れています。

localstack-init

localstack-init/init-s3.sh

#!/bin/sh
set -e

echo "init-s3.sh 開始"

if [ -f /var/.localstack.env ]; then
    echo "環境変数を読み込み中..."
    set -o allexport
    . /var/.localstack.env
    set +o allexport
else
    echo ".localstack.env が見つかりません"
    exit 1
fi

echo "S3バケット名: $AWS_S3_BUCKET_NAME"
echo "エンドポイントURL:  $AWS_ENDPOINT_URL"

if [ -z "$AWS_S3_BUCKET_NAME" ]; then
    echo "バケット名が設定されていません"
    exit 1
fi

echo "バケットの存在を確認中..."

if aws s3 ls "s3://$AWS_S3_BUCKET_NAME" --endpoint-url=$AWS_ENDPOINT_URL --no-sign-request 2>&1 | grep -q 'NoSuchBucket'; then
    echo "バケットを作成中..."
    aws s3 mb "s3://$AWS_S3_BUCKET_NAME" --endpoint-url=$AWS_ENDPOINT_URL --region ap-northeast-1 --no-sign-request
else
    echo "バケットは既に存在します"
fi

echo "バケット一覧を表示:"
aws s3 ls --endpoint-url=$AWS_ENDPOINT_URL --no-sign-request

echo "init-s3.sh 完了"

localstack-init/.localstack.env

AWS_S3_BUCKET_NAME=test-bucket
AWS_ENDPOINT_URL=http://localstack:4566

localstack-init が立ち上がった際に、自動で localstack-init/init-s3.sh を実行し、指定された S3 バケットを作成します。

実行手順

# コンテナを起動(S3バケットが自動作成される)
docker compose up -d --build

# Pythonスクリプトでファイルをアップロード
docker compose exec backend python upload_file.py

でupload_file.pyを実行してみましょう!

ファイルをアップロードしました: /images/ねこ.jpeg → test-bucket/ねこ.jpeg

バケット「test-bucket」の中身:
 - ねこ.jpeg

が表示されたら成功です!

導入した結果

LocalStack を導入したことで、AWS の認証情報を毎回セットする手間がなくなり、開発効率は大幅に改善されました。

手元のローカル環境で S3 に画像をアップロード → 中身を確認 → 処理を繰り返すという一連の流れが、一切クラウドに接続せず完結できるようになったのは、非常に快適でした。

つまずきポイント

一方で、導入にあたってはつまずきポイントもありました。

実際のプロジェクトでは、boto3 を使って、環境によって接続先を切り替える必要がありました。

具体的には、本番環境では本物のAWS、開発環境では LocalStack に接続するという要件です。

しかし、boto3 の設定は環境変数や接続先 URL を明示的に指定する必要があるため、その切り替え処理を毎回コードに書くのは非効率で、バグの温床にもなりやすいと感じました。

そこで以下のようなコードを用意し、.env や設定ファイルで環境ごとの設定を切り替える方法を採用しました。

if settings.env == "dev":
    self.client = boto3.client(
        "s3",
        endpoint_url=settings.aws_endpoint_url,
    )
else:
    self.client = boto3.client("s3")

開発環境では.env ファイルなどで環境変数に”dev”を設定しaws_endpoint_url に LocalStack のエンドポイント(http://localstack:4566)を設定します。

本番環境では boto3 のデフォルト設定に任せることで、AWS の S3 に自動的に接続されます。

このようにすることで同じコードベースで環境を柔軟に切り替えられ、環境ごとの設定も .env や設定クラスで一元管理できるように解決しました。

まとめ

今回は、AWSをローカル環境で手軽に試せるLocalStack を活用した構成をご紹介しました。

localstackを使うことで本物のAWSに依存せず、認証情報の設定やセキュリティを気にせずかつコストゼロでAWSサービスをローカルで模擬的に扱うことができます。

今回の構成が、同様の課題に直面している方の一助になれば幸いです。

WEELテックブログ

Discussion