C#
S3
AWS_SDK
.NETFramework
LocalStack

AWS SDK for .NET で S3 の動作確認をローカルの fake サービスに向けて行う

fake-s3LocalStack を使って、S3 の動作確認をローカルで行うことができますが、AWS SDK for .NET を使った場合には少々面倒な部分があったので記録しておきます。


設定に関する資料


LocalStack の S3 に接続するのに必要な情報

LocalStack を docker-compose で以下のように設定し立ち上げるとします

version: '3'

services:
localstack-s3:
image: atlassianlabs/localstack:latest
environment:
- SERVICES=s3:5000
- DEFAULT_REGION=us-east-1
- HOSTNAME=localstack-s3
ports:
- "5000:5000"

Credential は何でも良いので aws cli なら例えば以下で接続できます。

$ aws --endpoint-url=http://localhost:5000 --region=us-east-1 s3 ls --profile my-profile


もちろん profile ではなく accesskey, secret を指定しても良いです。


よって、以下を設定できれば C# のコードからも接続できそうです。


  • region

  • endpoint

  • accesskey / secret

また、注意すべきは、HTTP であるという点です。


C# のコードで設定を書く場合

var config = new AmazonS3Config {ServiceURL = "http://localhost:5000", AuthenticationRegion = "us-east-1"};

// accesskey/secret は foo/foo と適当に指定
var client = new AmazonS3Client("foo", "foo", config);

これだけで接続できます。


config で設定を書く場合


App.config

<?xml version="1.0"?>

<configuration>
<configSections>
<section name="aws" type="Amazon.AWSSection, AWSSDK.Core"/>
</configSections>
<aws region="us-east-1" endpointDefinition="endpoints.json">
</aws>
<appSettings>
<add key="AWSAccessKey" value="foo"/>
<add key="AWSSecretKey" value="foo"/>
</appSettings>
</configuration>

https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-other.html#config-setting-awsendpointdefinition

endpointDefinition に関しては上記(日本語版が古いと困るので一応英語版を参照)に記載があります。

ただ、そこからリンクされている endpoints.json の Github のリンク先にあるサンプルでは動作しません :cry:


endpoints.json のバージョンを下げる

以下は RegionEndpointProvider というまさに endpoints.json の内容を読み込むためのクラスですが、これには内部的に V2, V3 とバージョンが分かれています。

https://github.com/aws/aws-sdk-net/blob/41f550e99e4b69997fb0a70f5c081854abc97ea7/sdk/src/Core/RegionEndpoint/RegionEndpoint.cs#L189-L208

  // If the existing customer provided the endpoints.json file via

// <aws endpointDefinition=""/>, it's in v2 format. We we will create
// a v2 provider which does a fall through during LoadEndpointDefinitions()
// and loads from the override file provided by the user.
//
// Else, we are loading from the assembly resource. In which case we use the
// latest provider.
//
// It's actually a bug that _regionEndpointProvider is a static member variable
// since the IEndpointProvider should respect the AWSConfigs.EndpointDefinition
// _at_ the time of the service client instantiation. However, since this is
// the existing behavior with v2 endpoint file format, we will preserve this behavior as is.
if (!string.IsNullOrEmpty(AWSConfigs.EndpointDefinition))
{
_regionEndpointProvider = new RegionEndpointProviderV2();
}
else
{
_regionEndpointProvider = new RegionEndpointProviderV3();
}

コメントとコードから分かるように、ユーザーが設定する endpoints.jsonV2 で読み込まれます。一方で、リンク先の endpoints.jsonV3 用のものになっています。

https://github.com/aws/aws-sdk-net/blob/64b4d620417d0f69b84eda05d9e310f12a8af246/sdk/src/Core/endpoints.json#L2671


endpoints.json

  } ],

"version" : 3
}

よって、過去のコミットをたどって V2 用の endpoints.json を探します。

https://github.com/aws/aws-sdk-net/blob/99a52e3e5e8240fe1c68fdb062949556bca78dfb/sdk/src/Core/endpoints.json

上記を使って今回は us-east-1/s3 だけを書き換えます。

{

"version": 2,
"endpoints": {
"*/*": {
"endpoint": "{service}.{region}.amazonaws.com"
},
"cn-north-1/*": {
"endpoint": "{service}.{region}.amazonaws.com.cn",
"signatureVersion": "v4"
},
"us-gov-west-1/iam": {
"endpoint": "iam.us-gov.amazonaws.com"
},
"us-gov-west-1/s3": {
"endpoint": "s3-{region}.amazonaws.com"
},
"*/cloudfront": {
"endpoint": "cloudfront.amazonaws.com"
},
"*/iam": {
"endpoint": "iam.amazonaws.com"
},
"*/importexport": {
"endpoint": "importexport.amazonaws.com"
},
"*/route53": {
"endpoint": "route53.amazonaws.com"
},
"*/waf": {
"endpoint": "waf.amazonaws.com"
},
"us-east-1/sdb": {
"endpoint": "sdb.amazonaws.com"
},
"us-east-1/s3": {
- "endpoint": "s3.amazonaws.com"
+ "endpoint": "localhost:5000"
},
"us-west-1/s3": {
"endpoint": "s3-{region}.amazonaws.com"
},
"us-west-2/s3": {
"endpoint": "s3-{region}.amazonaws.com"
},
"eu-west-1/s3": {
"endpoint": "s3-{region}.amazonaws.com"
},
"ap-southeast-1/s3": {
"endpoint": "s3-{region}.amazonaws.com"
},
"ap-southeast-2/s3": {
"endpoint": "s3-{region}.amazonaws.com"
},
"ap-northeast-1/s3": {
"endpoint": "s3-{region}.amazonaws.com"
},
"sa-east-1/s3": {
"endpoint": "s3-{region}.amazonaws.com"
}
}
}

後はこれを実行ディレクトリにコピーされるようにしておきます。

ただ、これでは HTTPS での接続になってしまうので、コードは以下のようにする必要があります。

var client = new AmazonS3Client(new AmazonS3Config {UseHttp = true});

// or 設定の有り無しで切り替えるなら以下のような感じ
if (!string.IsNullOrEmpty(AWSConfigs.EndpointDefinition))
new AmazonS3Client(new AmazonS3Config {UseHttp = true});
else
new AmazonS3Client(); // おそらく production は EC2 のインスタンスメタデータ等から接続するはず


NUnit 等のテストランナーを使う場合

上記はコンソールアプリケーション等の実行ファイルと endpoints.json が同じ場所に存在する場合を想定していましたが、テストランナーが別の場所にあるケースがありえます。(というか、fake を使うのはそちらの方が多いと思います)

以下のように、なんらか SetUp フェーズで必要に応じて、テスト実行ディレクトリにパスを差し替えてあげるとうまくいきます。(xUnit の場合は調べておらず)

public void OneTimeSetUp()

{
if (!string.IsNullOrEmpty(AWSConfigs.EndpointDefinition))
{
if (AWSConfigs.EndpointDefinition.Equals(new FileInfo(AWSConfigs.EndpointDefinition).Name))
{
AWSConfigs.EndpointDefinition =
Path.Combine(TestContext.CurrentContext.TestDirectory, AWSConfigs.EndpointDefinition);
}
}
}


まとめ

今回は S3 を題材にしましたが、LocalStack 等を使えば DynamoDB や他の AWS Sevice の Fake もローカルで実行することができ、動作確認や自動テスト環境に便利に使えます。

今回のコードは以下に置いてあります。

https://github.com/dany1468/LocalAwsSample