なぜやるのか
仕事で、ある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-fetcherとinitializerは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も学ぶことができて、
つぶしが効く経験になったと思う