[AWS SDK for Java] AmazonS3 Clientで異なるRegionのBucketのObjectへアクセスする

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

(タイトルで「の」が連続してます、と怒られそう)

小ネタです。

AmazonS3は古参のサービスなのであまり詰まることはないのかもしれません。今回のオプションは日本語の情報がほぼありませんでした。たまにハマることがあるのできっとそういう人には有用な記事となることを期待します。

前提

Spring Bootで利用することを前提としています。

そのため、Spring Boot特有のアノテーションを多用しますがご了承ください。今回はAmazonS3 Client InterfaceのBeanを登録し、利用する側が @Autowired することでS3 ClientをInjectionできることを想定しています。

S3Client Interfaceの生成

JavaのAWSライブラリにはいくつかS3へのアクセスのインタフェースや実装クラスやBuilderがあります。

AWSのJava SDK(v1)のドキュメントにあるように AmazonS3ClientBuilder を利用してAmazonS3へ接続するClientを作成する方法で行きます。

AmazonS3ClientBuilderを利用する

AmazonS3ClientBuilder を利用してClientを生成するコードは以下のようになります。standard() を利用することでCredential情報やRegion情報をChainしながらAmazonS3のClientを生成します。

AWSのドキュメントに例として載っているdefaultClient()はAWS ProfilesのDefaultに相当する情報を利用してClientを生成するようです。

今回は東京リージョンでProfileやRegionを指定するS3Clientを生成します。Bean登録するためにConfigurationクラスを作成します。

@Configuration
@RequiredArgsConstructor
public class AwsConfiguration {

    private final AwsConfigurationProperties properties;

    @Bean
    public AmazonS3 amazonS3() {
        AWSCredentialsProvider provider = new ProfileCredentialsProvider(properties.getProfileName());
        return AmazonS3ClientBuilder
                .standard()
                .withRegion(properties.getRegionName())
                .withCredentials(provider)
                .build();
    }
}

AwsConfigurationProperties は以下の通り。

@Data
@Component
@ConfigurationProperties(prefix = "sample.aws.s3")
public class AwsConfigurationProperties implements InitializingBean {

    private String regionName;

    private String profileName;

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.state(regionName != null,
                "Property 'sample.aws.s3.region_name' is required.");
        Assert.state(profileName != null,
                "Property 'sample.aws.s3.profile_name' is required.");
    }
}

sample.aws.s3.region_name, sample.aws.s3.profile_name を必須にしてます。

profile_nameは本来必須ではないですが(IAMロール等で制御できるため)、今回はローカルから実行する関係上必須としました。

application.properties には以下のように設定。 <YOUR PROFILE> は適宜変更してください。

sample.aws.s3.region_name=ap-northeast-1
sample.aws.s3.profile_name=<YOUR PROFILE>

AmazonS3 Clientは ap-northeast-1 でClientが生成されます。

Amazon S3 Clientを呼び出す

適当なServiceを作ってAmazonS3 Clientを @Autowired して利用します。

@Service
@AllArgsConstructor
public class S3Service {

    private final AmazonS3 amazonS3;

    /**
     * Get S3 Contents As String
     * @param s3path S3の接続先URL(パス形式)
     * @return コンテンツの内容をStringで出力
     */
    public String getString(String s3path) {
        AmazonS3URI amazonS3URI = new AmazonS3URI(s3path);
        return amazonS3.getObjectAsString(
                amazonS3URI.getBucket(),
                amazonS3URI.getKey());
    }
}

getString()のパラメータ s3path はパス形式のAmazonS3のオブジェクトを指定します。AmazonS3URIでパースされそれぞれBucketやKeyに分解されます。取得したデータは文字列として返却します。

S3のBucketを用意する

さてClientを使ってObjectを取得するために、S3Bucketを用意しましょう。ap-northeast-1(Tokyo) にBucketを作成します。おっと手が滑ってus-east-1(N.Virginia) にも間違えてBucketを作ってしまいました。折角なのでBucketは2つ使います。

以下のテキストファイルを各Bucketに配置しておきます。Keyは contents/sample-japanese-text.txt としました。このテキストはこちら を使って生成しました。 *1

 第二章 第九章 第四章 第十章 第六章. 復讐者」. .手配書 第十六章 第十一章 第十三章 第十九章. 復讐者」 .復讐者」 伯母さん . 復讐者」. 第八章 第六章 第二章 第五章. .手配書 第十六章 第十三章.伯母さん 復讐者」. 復讐者」 伯母さん. .手配書 第十二章 第十六章 第十九章. 復讐者」 . 第十二章 第十九章 第十七章 第十一章 第十三章 第十六章. 第十三章 第十七章 第十八章 手配書. 伯母さん 復讐者」. 復讐者」 伯母さん. 復讐者」 伯母さん. 伯母さん 復讐者」. 

復讐者」. 復讐者」. 復讐者」. 第十八章 第十二章 第十五章 第十六章 第十三章 第十七章. 復讐者」. 第十二章 第十三章 第十六章. 第十二章 第十一章 第十八章. 復讐者」 伯母さん .復讐者」 伯母さん. .手配書 第十七章 第十三章 第十五章 第十二章 第十一章. 

復讐者」 伯母さん . 第四章 第七章 第三章 第九章. 第八章 第七章 第十章 第九章 第四章 第六章. 第十四章 第十二章 第十三章 第十八章. 伯母さん 復讐者」. 第十七章 第十九章 第十五章 第十八章 第十三章 第十六章. .復讐者」 伯母さん. 復讐者」. .伯母さん 復讐者」. 第四章 第六章 第十章 第五章 第九章 第七章. 第四章 第八章 第三章 第九章 第五章. .伯母さん 復讐者」 . 第十四章 第十六章 第十五章 手配書 第十七章 第十一章. 第十四章 第十二章 手配書. 伯母さん 復讐者」. 

 第十四章 手配書 第十三章 第十七章 第十一章 第十五章. .復讐者」 伯母さん . 復讐者」. 第十六章 第十四章 第十二章 手配書 第十八章. 第十九章 第十七章 手配書 第十二章 第十四章. 第十章 第三章 第八章 第七章 第九章. .伯母さん 復讐者」. 第十六章 第十四章 第十二章 第十七章 第十九章 第十八章. 

第六章 第十章 第四章 第九章 第三章 第二章. 復讐者」 . 第八章 第七章 第九章 第六章 第十章 第二章. 復讐者」. 手配書 第十一章 第十七章 第十四章 第十三章 第十六章. 復讐者」. .伯母さん 復讐者」. 第十七章 第十九章 手配書 第十四章 第十二章 第十五章. 復讐者」. 第十五章 第十九章 第十三章 第十六章 手配書 第十七章. 第三章 第七章 第八章 第五章 第四章. 復讐者」 . 

 復讐者」 伯母さん. 復讐者」. 第十五章 第十七章 第十九章 第十一章 第十三章 第十二章 . 第四章 第三章 第八章. 第十三章 第十八章 第十一章 第十五章 手配書 第十七章. 
....

S3は他のサービスと異なりRegionという概念が異なります。そのため、コンソール上はGlobalで見えつつ、各BucketがどのRegionにあるかが表示されています(Regionを変えないと見えないといったことがない)

Objectを取得

配置したオブジェクトを取得します。

String S3_PATH = "https://s3-region.amazonaws.com/bucket_name/key";

// execute
String result = sut.getString(S3_PATH);

// verify
assertThat(result).isNotEmpty();

問題がなければ取得したテキストが表示されます。

ap-northeast-1

private static final String TOKYO_REGION_S3_PATH = "https://s3-ap-northeast-1.amazonaws.com/xxxxxxxxxx-bucket-tokyo/contents/sample-japanese-text.txt";

@Test
public void testS3_GetContents_From_Tokyo() {
    String result = sut.getString(TOKYO_REGION_S3_PATH);
    assertThat(result).isNotEmpty();
}

Regionに ap-northeast-1 を指定しているので東京リージョンの xxxxxxxxxx-bucket-tokyo というバケット名の中のKeyで指定されたオブジェクトを取得します。

us-east-1

private static final String NVIRGINIA_REGION_S3_PATH = "https://s3-ue-east-1.amazonaws.com/xxxxxxxxxx-bucket-nvirginia/contents/sample-japanese-text.txt";

@Test
public void testS3_GetContents_From_NVirginia() {
    String result = sut.getString(NVIRGINIA_REGION_S3_PATH);
    assertThat(result).isNotEmpty();
}

Regionに us-east-1 を指定しているのでN.Virginiaリージョンの xxxxxxxxxx-bucket-nvirginia というバケット名の中のKeyで指定されたオブジェクトを取得します。

テストコード全体

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class S3ServiceTest {

    private static final String TOKYO_REGION_S3_PATH = "https://s3-ap-northeast-1.amazonaws.com/xxxxxxxxxx-bucket-tokyo/contents/sample-japanese-text.txt";

    private static final String NVIRGINIA_REGION_S3_PATH = "https://s3-ue-east-1.amazonaws.com/xxxxxxxxxx-bucket-nvirginia/contents/sample-japanese-text.txt";

    @Autowired
    private S3Service sut;

    @Test
    public void testS3_GetContents_From_NVirginia() {
        String result = sut.getString(NVIRGINIA_REGION_S3_PATH);

        assertThat(result).isNotEmpty();
    }

    @Test
    public void testS3_GetContents_From_Tokyo() {
        String result = sut.getString(TOKYO_REGION_S3_PATH);

        assertThat(result).isNotEmpty();
    }
}

エラー

上記テストコードを実行するとus-east-1 のBucketのObjectの取得に失敗します。

com.amazonaws.services.s3.model.AmazonS3Exception: The bucket is in this region: us-east-1. Please use this region to retry the request (Service: Amazon S3; Status Code: 301; Error Code: PermanentRedirect; Request ID: XXXXXXXXX; S3 Extended Request ID: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx)
, S3 Extended Request ID: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1639)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1304)
....

AmazonS3 ClientのRegionが ap-northeast-1 で生成されているため、Regionの異なるBucketへのアクセスはRedirectのレスポンスとなるようです。

AmazonS3ExceptionをCatchして解決する方法もありますが、S3Clientのオプションで便利なものがありました。あまりWeb上に情報がなかったのですが常識なのでしょうか・・・?

ForceGlobalBucketAccess

ForceGlobalBucketAccess というオプションがありこちらをClient作成時に有効にするだけで、Regionが異なっていてもアクセスすることができます。

.withForceGlobalBucketAccess(true) // If a bucket is in a different region, try again in the correct region

S3 Clientの作成時のコードを修正すると以下のようになります。

@Bean
public AmazonS3 amazonS3() {
    AWSCredentialsProvider provider = new ProfileCredentialsProvider(properties.getProfileName());
    return AmazonS3ClientBuilder
            .standard()
            .withRegion(properties.getRegionName())
            .withCredentials(provider)
            .withForceGlobalBucketAccessEnabled(true) // If a bucket is in a different region, try again in the correct region
            .build();
}

このオプションを有効にすると、先程エラーだった us-east-1 リージョンのオブジェクトもRedirectではなく正常に取得することができました。

まとめ

Regionの異なるBucketにアクセスする場合は、AmazonS3 Clientの生成時に .withForceGlobalBucketAccess(true) をつけておきましょう、という小ネタでした。

おまけ

AmazonS3Client のコンストラクタを呼び出してインスタンスの生成を行うと enableForceGlobalBucketAccess() が呼び出され内部変数の forceGlobalBucketAccessEnabled のフラグがtrueにセットされます。そのためこのインスタンスを作成すると自動的にRegionをまたいでアクセスするオプションが付与されていたようです。

しかし AmazonS3Client のコンストラクタ呼び出しは軒並みDeprecated指定されています。利用が推奨されないため *2AmazonS3ClientBuilder の呼び出しを利用しましょう。

ちなみに AmazonS3Client のデフォルトコンストラクタには以下のような記述があります。

* @deprecated use {@link AmazonS3ClientBuilder#defaultClient()}

なるほど、AmazonS3ClientBuilder.defaultClient()

実行してみると、こちらも内部で enableForceGlobalBucketAccess() の呼び出しが行われておりオプションが自動的に有効になっていました。

参照

脚注