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を使用する事が出来ました。