--- title: MongoDB互換のDocumentDBにSpring Boot + Testcontainersでアクセスするメモ tags: ["Spring Boot", "DocumentDB", "MongoDB", "Testcontainers", "Java", "FerretDB"] categories: ["Programming", "Java", "org", "springframework", "data", "mongodb"] date: 2025-09-03T04:31:45Z updated: 2025-09-03T04:31:45Z --- [前の記事](/entries/862)では、MongoDB互換のOSSデータベースである[FerretDB](https://docs.ferretdb.io/)を紹介しましたが、 Documentdbのバックエンドに使われている[DocumentDB](https://github.com/documentdb/documentdb)自体がMongoDB APIをサポートしたので、FerretDBは不要になりました。 本稿では、Spring Boot + Spring Data MongoDBを使ってDocumentDBにアクセスする方法をメモします。 といってもアプリケーション観点ではMongoDBと同じで、Testcontainersを使った設定だけが変わります。 以下は基本的には前回の記事の焼き直しです。 ### プロジェクトの雛形の作成 Spring Initializrを使って、Spring Bootプロジェクトの雛形を作成します。 ```bash curl -s https://start.spring.io/starter.tgz \ -d artifactId=demo-documentdb \ -d name=demo-documentdb \ -d baseDir=demo-documentdb \ -d packageName=com.example \ -d dependencies=web,data-mongodb,actuator,configuration-processor,prometheus,native,testcontainers \ -d type=maven-project \ -d applicationName=DemoDocumentDbApplication | tar -xzvf - cd demo-documentdb ``` ### サンプルアプリの作成 非常にシンプルな、メッセージを保存・取得するアプリケーションを作成します。 ```java cat < src/main/java/com/example/Message.java package com.example; public record Message(String id, String text) { } EOF ``` ```java cat < src/main/java/com/example/HelloController.java package com.example; import java.util.List; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { private final MongoTemplate mongoTemplate; public HelloController(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } @PostMapping(path = "/messages") public Message postMessage(@RequestBody String text) { return mongoTemplate.save(new Message(null, text)); } @GetMapping(path = "/messages") public List getMessages() { return mongoTemplate.findAll(Message.class); } } EOF ``` Spring InitializrでTestcontainersを追加しているので、次のファイルがプロジェクトに含まれます。 ```java package com.example; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.testcontainers.containers.MongoDBContainer; import org.testcontainers.utility.DockerImageName; @TestConfiguration(proxyBeanMethods = false) class TestcontainersConfiguration { @Bean @ServiceConnection MongoDBContainer mongoDbContainer() { return new MongoDBContainer(DockerImageName.parse("mongo:latest")); } } ``` Testcontainersを使ってアプリを起動しましょう。`src/test/java/com/example/TestDemoDocumentDbApplication.java`を実行するか、次のコマンドを実行します: ```bash ./mvnw spring-boot:test-run ``` アプリケーションおよびMongoDBが起動したら、次のようにメッセージをPOST & GETしてみましょう。 ```bash $ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello MongoDB\!" {"id":"6879a6f3ba99e4ec5c9419fd","text":"Hello MongoDB!"} $ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello DocumentDB\!" {"id":"6879a6f8ba99e4ec5c9419fe","text":"Hello DocumentDB!"} $ curl -s http://localhost:8080/messages | jq . [ { "id": "6879a6f3ba99e4ec5c9419fd", "text": "Hello MongoDB!" }, { "id": "6879a6f8ba99e4ec5c9419fe", "text": "Hello DocumentDB!" } ] ``` この動作確認に相当するテストコードは次のようになります。 ```java cat < src/test/java/com/example/DemoDocumentDbApplicationTests.java package com.example; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @Import(TestcontainersConfiguration.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class DemoDocumentDbApplicationTests { RestClient restClient; @BeforeEach void setUp(@LocalServerPort int port, @Autowired RestClient.Builder restClientBuilder) { this.restClient = restClientBuilder.defaultStatusHandler(statusCode -> true, (req, res) -> { /* NO-OP */}).baseUrl("http://localhost:" + port).build(); } @Test void contextLoads() { { ResponseEntity res = this.restClient.post() .uri("/messages") .contentType(MediaType.TEXT_PLAIN) .body("Hello MongoDB!") .retrieve() .toEntity(Message.class); assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); Message message = res.getBody(); assertThat(message).isNotNull(); assertThat(message.text()).isEqualTo("Hello MongoDB!"); assertThat(message.id()).isNotNull(); } { ResponseEntity res = this.restClient.post() .uri("/messages") .contentType(MediaType.TEXT_PLAIN) .body("Hello DocumentDB!") .retrieve() .toEntity(Message.class); assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); Message message = res.getBody(); assertThat(message).isNotNull(); assertThat(message.text()).isEqualTo("Hello DocumentDB!"); assertThat(message.id()).isNotNull(); } { ResponseEntity> res = this.restClient.get() .uri("/messages") .retrieve() .toEntity(new ParameterizedTypeReference<>() { }); assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); List messages = res.getBody(); assertThat(messages).isNotNull(); assertThat(messages).hasSize(2); assertThat(messages).map(Message::id).allSatisfy(id -> assertThat(id).isNotNull()); assertThat(messages).map(Message::text).containsExactly("Hello MongoDB!", "Hello DocumentDB!"); } } } EOF ``` テストでもTestcontainersでMongoDBが起動します。次のコマンドでテストを実行できます: ```bash ./mvnw clean test ``` ### DocumentDBへの入れ替え MongoDBからDocumentDBに入れ替えます。アプリケーションのコードはそのままでよく、Testcontainersの設定だけを変更します。 Documentdbのコンテナイメージは`MongoDBContainer`との互換性がなかったので、`GenericContainer`を使います。 そのため、ServiceConnectionは使わず、`DynamicPropertyRegistrar`を使ってMongoDBの接続情報を動的に登録します。 Documentdbはデフォルトで認証が有効になっているので、ユーザー名とパスワードも含めて`spring.data.mongodb.uri`を設定します。 `TestcontainersConfiguration`を次のように変更します: ```java cat < src/test/java/com/example/TestcontainersConfiguration.java package com.example; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.test.context.DynamicPropertyRegistrar; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; import org.testcontainers.utility.DockerImageName; @TestConfiguration(proxyBeanMethods = false) class TestcontainersConfiguration { @Bean GenericContainer documentdbContainer() { return new GenericContainer<>(DockerImageName.parse("ghcr.io/microsoft/documentdb/documentdb-local:latest")) .withExposedPorts(10260 /* MongoDB Port */, 9712 /* PostgreSQL port */) .withEnv("USERNAME", "user") .withEnv("PASSWORD", "password") .withEnv("ENFORCE_SSL", "false") .waitingFor(new HostPortWaitStrategy().forPorts(10260, 9712)); } @Bean DynamicPropertyRegistrar dynamicPropertyRegistrar(GenericContainer documentdbContainer) { return registry -> registry.add("spring.data.mongodb.uri", () -> "mongodb://user:password@%s:%d/test" .formatted(documentdbContainer.getHost(), documentdbContainer.getMappedPort(10260))); } } EOF ``` `TestcontainersConfiguration`変更後も同じテストが通るはずです: ```bash ./mvnw clean test ``` `src/test/java/com/example/TestDemoDocumentDbApplication.java`を再実行するか、次のコマンドを再実行します: ```bash ./mvnw spring-boot:test-run ``` 先ほどと同じように、メッセージをPOST & GETしてみましょう。 ```bash $ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello MongoDB\!" {"id":"6879af654c503243968ecba0","text":"Hello MongoDB!"} $ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello DocumentDB\!" {"id":"6879af6a4c503243968ecba1","text":"Hello DocumentDB!"} $ curl -s http://localhost:8080/messages | jq . [ { "id": "6879af654c503243968ecba0", "text": "Hello MongoDB!" }, { "id": "6879af6a4c503243968ecba1", "text": "Hello DocumentDB!" } ] ``` 特に問題なくMongoDBからDocumentdbに切り替わることが確認できました。 ### スタンドアローンで実行 Testcontainersを使わずに、アプリケーションをスタンドアローンで実行してみます。 Documentdbは次の`docker run`コマンドで起動します: ```bash docker run --rm --name documentdb -p 10260:10260 -e USERNAME=user -e PASSWORD=password -e ENFORCE_SSL=false ghcr.io/microsoft/documentdb/documentdb-local:latest ``` 次のコマンドで実行可能なjarファイルを作成します: ```bash ./mvnw clean package ``` 実行時に`spring.data.mongodb.uri`を指定して、Documentdbに接続します。 ```bash java -jar target/demo-documentdb-0.0.1-SNAPSHOT.jar --spring.data.mongodb.uri=mongodb://user:password@localhost:10260/test ``` 先ほどと同じように、メッセージをPOST & GETできるでしょう。 ### DocumentDBへのTLS接続を有効にする `ghcr.io/microsoft/documentdb/documentdb-local`のDockerイメージはデフォルトでTLSが有効になっています。 先の例では環境変数`ENFORCE_SSL=false`でTLSを無効にしていましたが、この設定を削除するとTLS接続が必要になります。 テスト中にMongoDBへのTLS接続を有効にするにはDocumentDBに設定されたTLS証明書のCA証明書をTrustStoreに登録する必要があります。 今回は[netty-pkitesting](https://central.sonatype.com/artifact/io.netty/netty-pkitesting)を使用して、テスト用に自己署名証明書を作成して、動的にこの証明書を使用するように設定します。 `pom.xml`に次の依存関係を追加します: ```xml io.netty netty-pkitesting 4.2.4.Final test ``` `TestcontainersConfiguration`を次のように変更します: ```java cat < src/test/java/com/example/TestcontainersConfiguration.java package com.example; import io.netty.pkitesting.CertificateBuilder; import io.netty.pkitesting.X509Bundle; import java.io.File; import org.slf4j.LoggerFactory; import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.pem.PemSslStoreBundle; import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.test.context.DynamicPropertyRegistrar; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; import org.testcontainers.utility.DockerImageName; @TestConfiguration(proxyBeanMethods = false) class TestcontainersConfiguration { @Bean X509Bundle selfSignedCertificate() throws Exception { return new CertificateBuilder().subject("CN=localhost").setIsCertificateAuthority(true).buildSelfSigned(); } @Bean GenericContainer documentdbContainer(X509Bundle selfSignedCertificate) throws Exception { File tempCertChainPem = selfSignedCertificate.toTempCertChainPem(); File tempPrivateKeyPem = selfSignedCertificate.toTempPrivateKeyPem(); return new GenericContainer<>(DockerImageName.parse("ghcr.io/microsoft/documentdb/documentdb-local:latest")) .withExposedPorts(10260, 9712) .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("documentdb"))) .waitingFor(new HostPortWaitStrategy().forPorts(10260, 9712)) .withFileSystemBind(tempCertChainPem.getAbsolutePath(), "/tmp/cert.pem", BindMode.READ_ONLY) .withFileSystemBind(tempPrivateKeyPem.getAbsolutePath(), "/tmp/private.key", BindMode.READ_ONLY) .withEnv("CERT_PATH", "/tmp/cert.pem") .withEnv("KEY_FILE", "/tmp/private.key") .withEnv("USERNAME", "user") .withEnv("PASSWORD", "password"); } @Bean DefaultSslBundleRegistry sslBundles(X509Bundle selfSignedCertificate) { DefaultSslBundleRegistry bundles = new DefaultSslBundleRegistry(); bundles.registerBundle("self-signed", SslBundle.of(new PemSslStoreBundle(null, PemSslStoreDetails.forCertificate(selfSignedCertificate.getRootCertificatePEM())))); return bundles; } @Bean DynamicPropertyRegistrar dynamicPropertyRegistrar(GenericContainer documentdbContainer) { return registry -> { registry.add("spring.data.mongodb.uri", () -> "mongodb://user:password@%s:%d/test" .formatted(documentdbContainer.getHost(), documentdbContainer.getMappedPort(10260))); registry.add("spring.data.mongodb.ssl.enabled", () -> "true"); registry.add("spring.data.mongodb.ssl.bundle", () -> "self-signed"); }; } } EOF ``` この設定に変更してもテストが成功することを確認してください: ```bash ./mvnw clean test ``` 先ほどと同様に、Testcontainersを使わずに、アプリケーションをスタンドアローンで実行してみます。 次のコマンドで自己署名証明書を作成します: ```bash DIR=/tmp/self-signed mkdir -p ${DIR} # Create CA certificate openssl req -new -nodes -out ${DIR}/ca.csr -keyout ${DIR}/ca.key -subj "/CN=@making/O=LOL.MAKI/C=JP" chmod og-rwx ${DIR}/ca.key cat < ${DIR}/ext_ca.txt basicConstraints=CA:TRUE keyUsage=digitalSignature,keyCertSign EOF openssl x509 -req -in ${DIR}/ca.csr -days 3650 -signkey ${DIR}/ca.key -out ${DIR}/ca.crt -extfile ${DIR}/ext_ca.txt cat < ${DIR}/ext.txt basicConstraints=CA:FALSE keyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement extendedKeyUsage=serverAuth,clientAuth EOF # Create Server certificate signed by CA openssl req -new -nodes -out ${DIR}/server.csr -keyout ${DIR}/server.key -subj "/CN=localhost" chmod og-rwx ${DIR}/server.key openssl x509 -req -in ${DIR}/server.csr -days 3650 -CA ${DIR}/ca.crt -CAkey ${DIR}/ca.key -CAcreateserial -out ${DIR}/server.crt -extfile ${DIR}/ext.txt ``` Documentdbは次の`docker run`コマンドで起動します: ```bash docker run --rm --name documentdb -p 10260:10260 -v /tmp/self-signed:/tmp/self-signed -e USERNAME=user -e PASSWORD=password -e CERT_PATH=/tmp/self-signed/server.crt -e KEY_FILE=/tmp/self-signed/server.key ghcr.io/microsoft/documentdb/documentdb-local:latest ``` 次のコマンドで実行可能なjarファイルを作成します: ```bash ./mvnw clean package ``` 実行時に`spring.data.mongodb.uri`を指定して、Documentdbに接続します。 ```bash java -jar target/demo-documentdb-0.0.1-SNAPSHOT.jar --spring.data.mongodb.uri=mongodb://user:password@localhost:10260/test --spring.ssl.bundle.pem.self-signed.truststore.certificate=file:/tmp/self-signed/ca.crt --spring.data.mongodb.ssl.enabled=true --spring.data.mongodb.ssl.bundle=self-signed ``` 先ほどと同じように、メッセージをPOST & GETできるでしょう。 動作確認したソースコードは[こちら](https://github.com/making/demo-documentdb)です。 --- DocumentdbをSpring Boot + Spring Data MongoDB + Testcontainersで使う方法を紹介しました。 SSPLなMongoDBの代替としてDocumentDBを使うことで、ライセンスの問題を回避しつつ、MongoDB互換の機能を利用できそうです。 その他の情報は[ドキュメント](https://documentdb.io/)を参照してください。