S3を使用する単体試験(Java)をlocalstackで行う方法

1418
NO IMAGE

AWSのリソース(S3、SQS etc.)を使用した試験を行う際には、実際のAWSを使用するよりもLocalstackを使用した方が不要な課金も発生せず、また試験環境の準備も簡単に行う事が出来ます。この記事では、Localstackを使用してS3を使用する単体試験を行う方法について説明します。この記事で使用したソースコードはレポジトリに置いてあります。

環境

Java

JDK17

Gradle

7系

Gradleの依存関係


dependencies {
    implementation group: 'software.amazon.awssdk', name: 's3', version: '2.20.78'
}

Localstackの準備

Localstackの公式のやり方に従ってLocalstackを準備します。今回は、docker-composeを使用しました。

version: "3.8"

services:
  localstack:
    image: localstack/localstack
    ports:
      - 4566:4566 # LocalStack Gateway
    environment:
      - DEBUG=1
      - SERVICES=s3
      - DOCKER_HOST=unix:///var/run/docker.sock

AWSクライアントの準備

ローカル端末のAWS SDKからS3にアクセスするためには、accessKeyとsecretKeyが./aws/credentialsに指定されている必要があります。
Javaコードから指定する事も可能ですが、実際にしようする際にはインスタンスロールでアクセスすることになるため、Javaコードで直接指定する事はありません。

aws cliのインストール

aws configureの実行

localstack用の設定はaccesskeyとsecretkeyは任意の文字列(dummy等)で問題ありません(設定が存在する必要はあります)。

S3へのアクセス形式の違い

現在のAWS SDKはS3にアクセスする際に標準ではvirtual-host styleを使用するようです。Localstackは標準ではpath style想定しているために、AWS SDK標準の設定でS3にアクセスするとエラーになります。
path styleとvirtual-host styleについては、Amazon S3 path-style 廃止予定 – それから先の話 –を参照
path styleは実際のAWSでは既にサポートされていないようなので、Localstackにもvirtual-host styleでアクセスを行う必要があります。

Localstackをvirtual-host styleで使用する方法

Localstackの公式サイトの説明によると、virtual-host styleを使用するためには、endpointUriを<bucket>.s3.<region>.localhost.localstack.cloudにする必要があるようです(<~>はオプション)。

コードによる説明

path styleによるアクセス

以下のコードのようにS3Clientの生成時に、forcePathStyleを指定するとpath styleでのアクセスも可能です。

テストコード

package sample.aws.s3;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;

public class S3LocalStackTest {

    private static final Logger log = LoggerFactory.getLogger(S3LocalStackTest.class);

    @Test
    public void testPathStyle() {
        final var bucketName = "sample-bucket-p";
        var client = S3Client.builder().endpointOverride(URI.create("http://localhost:4566")).forcePathStyle(true)
                .build();
        this.createBucket(client, bucketName);
        this.putObject(client, bucketName, "test.txt", "test for path style");
        var stored = this.getObject(client, bucketName, "test.txt");
        log.info("testPathStyle stored=<{}>", stored);
    }
}

    private void createBucket(S3Client client, String bucketName) {
        client.createBucket(b -> b.bucket(bucketName));
    }

    private void putObject(S3Client client, String bucketName, String key, String value) {
        client.putObject(b -> {
            b.bucket(bucketName).key(key);
        }, RequestBody.fromString(value));
    }

    private String getObject(S3Client client, String bucketName, String key) {
        try (var out = new ByteArrayOutputStream()) {
            client.getObject(b -> {
                b.bucket(bucketName).key(key);
            }).transferTo(out);
            out.flush();
            return out.toString();
        } catch (IOException e) {
            throw new IllegalArgumentException("Failed to get object for <" + bucketName + ":" + key + ">.");
        }
    }

実行結果

無事に通ります。

08:37:40.556 [main] INFO  sample.aws.s3.S3LocalStackTest - testPathStyle stored=<test for path style>

virtual-host styleによるアクセス

forcePathStyleを外して実行する

Javaコード

// (省略)

public class S3LocalStackTest {

// (省略)

    @Test
    public void testVirtualHostStyleFail() {
        var exception = Assertions.assertThrows(Exception.class, () -> {
            final var bucketName = "sample-bucket-v";
            var client = S3Client.builder().endpointOverride(URI.create("http://localhost:4566")).forcePathStyle(false)
                    .build();
            this.createBucket(client, bucketName);
            this.putObject(client, bucketName, "test.txt", "test for virutal-host style");
            var stored = this.getObject(client, bucketName, "test.txt");
            log.info("testVirtualHostStyleFail stored=<{}>", stored);
        });
        log.info("testVirtualHostStyleFail ERROR", exception);
    }
}

実行結果

ホスト(sample-bucket-v.localhost)が見つからないというエラーになります。これは、virutal-host styleのアクセスのために、<bucket名>.<endpointのホスト>のような形のアドレスにアクセスしようとしたためです。

08:43:45.724 [main] INFO  sample.aws.s3.S3LocalStackTest - testVirtualHostStyleFail ERROR
software.amazon.awssdk.core.exception.SdkClientException: Received an UnknownHostException when attempting to interact with a service. See cause for the exact endpoint that is failing to resolve. If this is happening on an endpoint that previously worked, there may be a network connectivity issue or your DNS cache could be storing endpoints for too long.
    at software.amazon.awssdk.core.exception.SdkClientException$BuilderImpl.build(SdkClientException.java:111)
    (省略)
    at Caused by: java.net.UnknownHostException: sample-bucket-v1.localhost
    (省略)
... 98 common frames omitted

/etc/hostsに必要なホストを追加して実行する

/etc/hosts(Windowsの場合には、C:\Windows\System32\drivers\etc\hosts)に以下を追加します。

127.0.0.1 sample-bucket-v1.localhost

再実行の結果

以下のようにunknown operationになります。これは、path styleを期待しているlocalstackにvirtual-host styleの要求を送っために、パスにバケット名が含まれなかったためです。

09:00:59.476 [main] INFO  sample.aws.s3.S3LocalStackTest - testVirtualHostStyleFail ERROR
software.amazon.awssdk.services.s3.model.S3Exception: exception while calling s3 with unknown operation: Traceback (most recent call last):
  File "/opt/code/localstack/localstack/aws/protocol/parser.py", line 557, in parse
    operation, uri_params = self._operation_router.match(request)
  File "/opt/code/localstack/localstack/aws/protocol/op_router.py", line 317, in match
    rule, args = matcher.match(path, method=method, return_rule=True)
  File "/opt/code/localstack/.venv/lib/python3.10/site-packages/werkzeug/routing/map.py", line 652, in match
    raise NotFound() from None
(省略)

endpointUriを<bucket>.s3.<region>.localhost.localstack.cloudにする。

/etc/hostsに以下を追加します

# リージョンは省略可能
127.0.0.1 sample-bucket-v.s3.localhost.localstack.cloud

Javaコード

    @Test
    public void testVirtualHostStyleSuccess() {
        final var bucketName = "sample-bucket-v";
                var client = S3Client.builder().endpointOverride(URI.create("http://s3.localhost.localstack.cloud:4566"))
                .forcePathStyle(false).build();
        this.createBucket(client, bucketName);
        this.putObject(client, bucketName, "test.txt", "test for virtual-host style");
        var stored = this.getObject(client, bucketName, "test.txt");
        log.info("testVirtualHostStyleSuccess stored=<{}>", stored);
    }

実行結果

無事に実行出来ました。

09:08:17.457 [main] INFO  sample.aws.s3.S3LocalStackTest - testVirtualHostStyleSuccess stored=<test for virtual-host style>

結論

/etc/hostsに必要なバケット分の設定が必要ではありますが、LocalstackでもS3のアクセスにvirtual-host styleを使用する事が出来ました。