
DynamoDB local使ったローカル開発・テスト環境を構築してみた
はじめに
開発中のシステムでAmazon DynamoDBを使用しているのですが、ローカル環境でのテストやデバッグの際に毎回AWSの開発環境に接続するのは手間がかかりますし、テストデータの管理も煩雑になります。
そこで今回は、AWSが公式に提供しているDynamoDBのローカルエミュレータ「DynamoDB local」を使って、ローカル環境でDynamoDBの開発・テストを行う方法を試してみた備忘録となります。
本記事では以下の2つの使い方を試しています。
- Docker Composeを使ったローカル開発環境の構築(API起動 + データ閲覧)
- Testcontainersを使った自動テスト環境の構築
サンプルプロジェクトはSpring Boot + Java + Gradleの構成で、書籍管理のAPIを題材にしています。
参考リンク
- DynamoDB local(ダウンロード可能バージョン)のデプロイ – AWS公式ドキュメント
- amazon/dynamodb-local – Docker Hub
- Testcontainers公式サイト
- Improved Testcontainers Support in Spring Boot 3.1 – Spring公式ブログ
- Testcontainers – Spring Boot公式ドキュメント
- LocalStack公式サイト
- NoSQL Workbench for Amazon DynamoDB – AWS公式ドキュメント
DynamoDBのテストについて
DynamoDBのローカルでのテスト方法をいくつかピックアップしてみました。
| 方式 | Docker | 特徴 |
|---|---|---|
| Docker Compose + DynamoDB local | 要 | ローカル開発向き。本番と同じAPIで動作し、データの永続化も可能 |
| Testcontainers + DynamoDB local | 要 | 自動テスト向き。本番と同じAPIで動作し、テスト実行時にコンテナを自動起動・破棄 |
| Testcontainers + LocalStack | 要 | DynamoDB以外のAWSサービス(SQS, S3等)もまとめてテスト可能。一部挙動が異なる場合がある |
| モック(Mockito等) | 不要 | 高速だが、実際のDynamoDB APIを呼ばないため本番との互換性は検証できない |
DynamoDB localとは
DynamoDB localはAWSが公式に提供しているDynamoDBのローカルエミュレータです。Dockerイメージ(amazon/dynamodb-local)として配布されており、Dockerさえあればローカル環境でDynamoDBと同等のAPIを利用できます。
Testcontainersとは
Testcontainersは、テスト実行時にDockerコンテナのライフサイクル(起動・破棄)を自動管理してくれるOSSのライブラリです。AWS公式ではありませんが、Spring Boot 3.1以降で公式にサポートされています。DynamoDB local以外にも様々なDockerイメージと組み合わせて使用できます。
Docker Composeとの使い分けは主に以下の通りです。
| Docker Compose | Testcontainers | |
|---|---|---|
| 用途 | ローカル開発 | 自動テスト |
| コンテナ起動 | 手動起動(docker-compose up) | テスト実行時に自動起動 |
| コンテナ停止 | 手動停止(docker-compose down) | テスト終了時に自動破棄 |
| ポート | 固定(8000) | ランダム(競合しない) |
| データ | 永続化可能 | テスト終了で消失 |
両者は別のDockerコンテナとして起動するため、テスト実行がローカル開発用のデータに影響することはありません。
LocalStackとは
LocalStackはAWSの各種サービスをローカルでエミュレートするOSSのAWSエミュレータです。DynamoDBだけでなくSQS、S3、SNSなど多くのAWSサービスをサポートしており、これらの機能をまとめてテストしたい場合に有力な選択肢となります。
有償のPro版もあり、より多くのAWSサービスのAPIや高度な機能をサポートするみたいです。
今回の検証ではDynamoDB単体のテストが目的のため対象外としました。
サンプルプロジェクトの構成
今回はDynamoDB localをDocker Compose(ローカル開発用)とTestcontainers(自動テスト用)の2通りで使用します。
Spring Boot + Java + Gradleで書籍管理のCRUD APIを作成しました。プロジェクト構成は以下の通りです。
dynamodb-local-sample/
├── build.gradle.kts
├── docker-compose.yml ← ローカル開発用
├── src/main/java/com/example/dynamodbsample/
│ ├── Application.java
│ ├── config/DynamoDbConfig.java ← DynamoDB接続設定
│ ├── model/Book.java ← モデル
│ ├── dao/BookMapper.java ← CRUD操作
│ ├── controller/BookController.java ← REST API
│ └── init/DynamoDbTableInitializer.java ← テーブル自動作成
├── src/main/resources/
│ ├── application.yaml ← デフォルト設定
│ └── application-local.yaml ← ローカル開発用設定
└── src/test/java/com/example/dynamodbsample/
├── DynamoDbTestConfig.java ← Testcontainers設定
├── DynamoDbTableHelper.java ← テスト用ヘルパー
└── dao/BookMapperTest.java ← テストクラス
このあと、DynamoDB localに関連するファイルについて説明していきます。
※DynamoDB localに関係無いコントローラやロジックについては説明を省略します。
ローカル開発環境の構築(Docker Compose)
まずはDocker Composeを使って、ローカルでDynamoDB localを起動し、Spring BootのAPIからアクセスできるようにします。
docker-compose.yml
services:
dynamodb-local:
image: amazon/dynamodb-local:latest
command: "-jar DynamoDBLocal.jar -sharedDb -inMemory"
ports:
- "8000:8000"
-inMemoryオプションを指定すると、データはメモリ上にのみ保持され、コンテナを停止するとデータが消えます。
オプションを変更することでデータ永続化の設定をすることも可能です。
Spring Bootの接続設定
Spring Bootのプロファイル機能を使って、ローカル開発時はDynamoDB localに、本番・STG環境では実際のAWS DynamoDBに接続するよう切り替えます。
application.yaml(デフォルト設定)
aws:
credentials:
access-key: "※実際のAWS環境のaccess-keyを設定"
secret-key: "※実際のAWS環境のsecret-keyを設定"
region: "※実際のAWS環境のregionを設定"
application-local.yaml(ローカル開発用)
aws:
dynamodb-local:
endpoint: http://localhost:8000
credentials:
access-key: dummy
secret-key: dummy
DynamoDbConfig.javaでは、dynamodb-localのエンドポイントが指定されている場合にローカル接続に切り替えています。
※このサンプルでは逆に実際のDynamoDB環境へ接続しての動作確認を行っていないため、実装が不足しているかもしれないです。
@Configuration
public class DynamoDbConfig {
@Value("${aws.dynamodb-local.endpoint:}")
private String endpoint;
@Value("${aws.credentials.access-key:}")
private String accessKey;
@Value("${aws.credentials.secret-key:}")
private String secretKey;
@Value("${aws.region:ap-northeast-1}")
private String region;
@Bean
public DynamoDbClient dynamoDbClient() {
var builder = DynamoDbClient.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)));
// DynamoDB localのエンドポイントが指定されている場合はローカル環境へ接続
if (endpoint != null && !endpoint.isEmpty()) {
builder.endpointOverride(URI.create(endpoint));
}
return builder.build();
}
}
この設定によりdynamoDBの接続先がDynamoDB localに切り替わるため、あとは通常通りDynamoDBを使用するロジックを実装すればOKです。
(今回は説明を省略します)
起動手順
# 1. DynamoDB local 起動
docker-compose up -d
# 2. API起動
gradlew bootRun --args="--spring.profiles.active=local"
起動後、APIを実行するとAWS環境のDynamoDBではなく、ローカル環境のDynamoDB localにアクセスしてデータの登録や取得が実行されます。
GUIでのデータ閲覧(NoSQL Workbench)
DynamoDB localに登録したデータをGUIで確認したい場合は、AWSが公式に提供しているNoSQL Workbenchが便利です。無料で利用でき、テーブルの閲覧・データ編集・クエリ実行などが可能です。
自動テスト環境の構築(Testcontainers)
次に、Testcontainersを使って自動テストでDynamoDB localを利用する方法についてです。
依存関係の追加
build.gradle.ktsにTestcontainersの依存を追加します。
dependencies {
// Testcontainers
testImplementation("org.testcontainers:testcontainers:2.0.4")
testImplementation("org.testcontainers:junit-jupiter:1.21.4")
}
テスト用のDynamoDB設定
テスト時にDynamoDB localのDockerコンテナを自動起動し、そこに接続するDynamoDbClientを提供する設定クラスを作成します。
/**
* Testcontainers + DynamoDB Local のテスト用設定。
* テスト時に自動でDockerコンテナを起動し、テスト用のDynamoDBクライアントを提供する。
*/
@TestConfiguration
public class DynamoDbTestConfig {
// DynamoDB Local のDockerコンテナ定義
private static final GenericContainer> DYNAMO_DB_CONTAINER =
new GenericContainer<>(DockerImageName.parse("amazon/dynamodb-local:latest"))
.withExposedPorts(8000)
.withCommand("-jar DynamoDBLocal.jar -inMemory -sharedDb");
// クラスロード時にコンテナを起動(テストクラス間で使い回す)
static {
DYNAMO_DB_CONTAINER.start();
}
@Bean(destroyMethod = "close")
@Primary
public DynamoDbClient dynamoDbClient() {
// Testcontainersが割り当てたランダムポートでエンドポイントを構築
String endpoint = "http://" + DYNAMO_DB_CONTAINER.getHost()
+ ":" + DYNAMO_DB_CONTAINER.getFirstMappedPort();
// DynamoDB Localへの接続のため、認証情報はダミー値でOK
return DynamoDbClient.builder()
.endpointOverride(URI.create(endpoint))
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("dummy", "dummy")))
.build();
}
@Bean
@Primary
public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) {
return DynamoDbEnhancedClient.builder()
.dynamoDbClient(dynamoDbClient)
.build();
}
}
getFirstMappedPort()でTestcontainersがランダムに割り当てたポートを取得してエンドポイントを構築するため、Docker Composeで起動するdockerコンテナには影響がありません。
また、今回はテスト実行時にDynamoDB Localのコンテナを1回だけ起動して、全テストで使いまわしています。
テストクラスごと、あるいはテストメソッドごとにコンテナの起動、破棄をすることも可能ですが、テストが増えてくるとその分時間が掛かってしまうため今回は使い回すようにしています。
テーブル作成・データクリアのヘルパー
テスト用にテーブル作成とデータクリアを行うヘルパークラスを用意します。
public class DynamoDbTableHelper {
// テーブルが存在しなければ作成する
public static void createTableIfNotExist(DynamoDbClient client) {
var existingTables = client.listTables().tableNames();
if (existingTables.contains(Book.TABLE_NAME)) {
return;
}
client.createTable(/* テーブル定義 */);
}
// 全データを削除する(テーブルは残す)
public static void truncateTable(DynamoDbClient client) {
var scanResult = client.scan(ScanRequest.builder()
.tableName(Book.TABLE_NAME).build());
for (var item : scanResult.items()) {
client.deleteItem(DeleteItemRequest.builder()
.tableName(Book.TABLE_NAME)
.key(Map.of(Book.BOOK_ID, item.get(Book.BOOK_ID)))
.build());
}
}
}
データの削除ですが、DynamoDbTestConfigの説明に記載した通り、全テストでコンテナを使いまわしているため前のテストのデータが残ってしまいます。
RDBのテストでは@Transactionalアノテーションを付けることでテスト後に自動ロールバックできますが、DynamoDBにはそのような仕組みがありません。そのため、各テストの実行前にscan → deleteItemで全データを削除し、テスト間のデータ独立性を確保しています。
テストクラス
@SpringBootTest(classes = {DynamoDbTestConfig.class, BookMapper.class})
class BookMapperTest {
@Autowired
BookMapper bookMapper;
@Autowired
DynamoDbClient dynamoDbClient;
@BeforeEach
void setUp() {
DynamoDbTableHelper.createTableIfNotExist(dynamoDbClient);
DynamoDbTableHelper.truncateTable(dynamoDbClient);
}
@Test
void create_書籍を登録して取得できる() {
var book = new Book("book-001", "Spring入門", "山田太郎", "技術書", 3000);
bookMapper.create(book);
var result = bookMapper.findById("book-001");
assertNotNull(result);
assertEquals("Spring入門", result.getTitle());
}
}
@BeforeEachアノテーションを使用してsetUp()でDynamoDbTableHelperのtruncateTable()を実行することで各テストメソッドの実行前に毎回データをクリーンしています。
テスト実行
gradlew test
Docker Composeの事前起動は不要ですが、Docker Desktopは起動している必要があります。
その状態でテストを実行すると、DynamoDB Localのコンテナが起動して、テストが完了すると自動でコンテナが削除されます。
おわりに
今回はDynamoDB localを使ったローカル開発環境とテスト環境の構築方法を試しました。
Docker Composeを使えばローカルでDynamoDBのデータを確認しながら開発できますし、Testcontainersを使えばgradlew testだけでDynamoDBを使ったテストを実行できます。どちらもDockerさえあれば導入できるので、DynamoDBを使った開発をされている方はぜひ試してみてください。
