1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LocalstackとDockerつかってAWSの実リソースをLocalで再現しながらテストする

Last updated at Posted at 2025-10-31

なぜやるのか

仕事で、あるDynamdDBのtableの全itemについて特定のattrを追加することになった

人力でやるには数がおおく、ヒューマンエラーも生じうるので、botoを使ってやることにした

特に複雑なロジックもないのでサクッとつくってPRをだしたところ

"更新によって他のattrを破壊しないかテストしてください"

といわれた

client.update_item()を使用していたので、そこまでする必要あるの?と思ったが、
勉強になりそうだしやってみるか。と思い実装にいたる

ディレクトリ構成

root
- src/ # botoでitemにattrを追加するコード
- tests/
- docker-compose.yml
- Dockerfile
- pyprojects.toml
- uv.lock

実装方針

まずはLocalstackを使ってmockを構築することにした
さらにせっかくテストするなら本番と同じ条件でテストしたい
テストコードはなるべくスリムにしたい
などいろいろ考えるうちに、以下のような構成でテスト環境を構築することにした

container名 役割
Localstack ローカル環境にAWSのリソースを構築する
schema_fetcher 実リソースからスキーマを取得する。ReadOnlyの認証情報を与える
initializer スキーマを用いて、検証用のリソースをLocalstackに作成する
test-runner-dev テストコードの開発用
test-runner-ci テストコード実行用

環境設定ファイルは以下の通り

docker-compose.yml

services:
	localstack
		container_name: "localstack"
		image: localstack:localstack
		ports: 
			- "127.0.0.1:4566:4566"
			- "127.0.0.1:4510-4599":4510-4599"
	    environment:
		    - DEBUG=${DEBUG:-0}
		      
	schema-fetcher:
		container_name: "schema-fetcher"
		build:
			dockerfile: Dockerfile
			target: schema-fetcher
		env_file:
			- .env
		volumes
			- ./shared:/shared
		environments
		  - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
		  - AWS_SECRET_ACESS_KEY=${AWS_SECRET_ACCESS_KEY}
		  - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
		depends_on:
			- localstack
	
	initializer:
		container_name: "initializer"
		build:
			dockerfile: Dockerfile
			target: initializer
		environment:
			- LOCALSTACK_ENDPOINT=http://localstack:4566
		    - AWS_ACCESS_KEY_ID=test
		    - AWS_SECRET_ACESS_KEY=test
		    - AWS_DEFAULT_REGION=ap-northeast-1
		volumes:
			- ./shared:/shared
		depends_on:
			- schema-fetcher
			  
	test-runner-dev:
		container_name: "test-runner-dev"
		build:
			dockerfile: Dockerfile
		environment:
			- LOCALSTACK_ENDPOINT=http://localstack:4566
			- AWS_ACCESS_KEY_ID=test
		    - AWS_SECRET_ACESS_KEY=test
		    - AWS_DEFAULT_REGION=ap-northeast-1
			- PYTHONPATH=/app/src
		volumes:
			- .:/app
		depends_on:
			- initializer
		command: ["tail", "-f", "/dev/null"]
		

	test-runner-ci:
		container_name: "test-runner-ci"				  
		build:
			dockerfile: Dockerfile
			target: test-runner-ci
		environment:
			- LOCALSTACK_ENDPOINT=http://localstack:4566
			- AWS_ACCESS_KEY_ID=test
		    - AWS_SECRET_ACESS_KEY=test
		    - AWS_DEFAULT_REGION=ap-northeast-1
			- PYTHONPATH=/app/src
	    depends_on:
		    - initializer
			  

Dockerfile

FROM python:3.13-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen

FROM builder AS schema-fetcher
COPY scripts/ ./scripts
CMD ["uv", "run", "scripts/fetch_table.py"]

FROM builder AS initializer
COPY scripts/ ./scripts/
CMD ["uv", "run", "scripts/create_table.py"]

FROM builder AS test-runner-ci
COPY src/ ./src/
COPY tests/ ./tests/
CMD ["uv", "run", "-m", "pytest", "tests/", "-v", "-s"]

また、この構築を通じてDockerの豆知識(というよりは基礎かも)
をいくつか学んだので、別のところで追記しておく

AWSのリソースを触るコンテナについては、
実リソースを触るschema-fetcher以外は明示的に認証情報をtestと宣言することで
(心理的に)安全になるようにしている

テストする

以下のMakefileを作成

test:
	docker compose up locelstack -d
	sleep 3
	docker compose run --rm --build schema-fetcher
	docker compose run --rm --build initializer
	docker compose run --rm --build test-runner-ci
	docker compose down -v

schema-fetcherとinitializerは実行するスクリプトが変更される可能性がある+たまにリソースの作成に失敗したので都度ビルドするようしにしている

より正確にいうと、docker compose run --rm --build test-runner-ciだけ最初は書いていたが、initializerで作成したリソースがないといわれる場合があったので、律儀にうえから書いた

また、sleep 3 はローカルスタックが準備完了前にinitializerがリソースを作らない用の暫定対応。へルスチェックを用意したほうがいいよとLLMに言われたが気力が追い付かずあきらめた

とりあえず、上記のコードで再現性のあるテストはできたのでよしとする

その他

最初schema-fetcherinitializerはawsciで書いていたが、

どうしてもshのコードに苦手意識があるのと、
botoで書いてもバージョンを固定すれば再現性担保できるとのことなのでbotoにした

また、dockerの環境構築と同じぐらい、pythonのmoduleのimportできません問題に苦労した。

srcを起点とした絶対imporを軸として

src/main.py

import src.config as config
import src.services.dynamodb as dynamodb
from src.logger import setup_logger
...

src/services/dynamodb.py

from src.logger import setup_logger
...

tests/test_main.py

import src.config as config
from src.main import main
...

と設定

  • mainの実行: uv run -m src.main
  • tesaの実行: uv run -m pytest tests/ -v -s

とすることで動作はできてるが、
ちゃんとベストプラクティスに則っているかはわからない
↑LLMに聞いたら大丈夫らしい

あと、冒頭のテストは

  • tebleをscanした際のitemsをsnapshotとする
  • 処理前後のsnapshotについて、追加したattr以外に差分がない

というロジックで実行した

まとめ

シンプルな構成ではあるが、
実リソースをベースにしたのsandbox(Localstack)構築+テストを実装することができた

dockerの基礎やpythonの絶対importも学ぶことができて、
つぶしが効く経験になったと思う

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?