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

[fake-s3](https://github.com/jubos/fake-s3) や [LocalStack](https://github.com/localstack/localstack) を使って、S3 の動作確認をローカルで行うことができますが、AWS SDK for .NET を使った場合には少々面倒な部分があったので記録しておきます。

## 設定に関する資料

- [AWS 認証情報の設定 - AWS SDK for .NET](https://docs.aws.amazon.com/ja_jp/sdk-for-net/v3/developer-guide/net-dg-config-creds.html#creds-assign)
- [AWS リージョンの選択 - AWS SDK for .NET](https://docs.aws.amazon.com/ja_jp/sdk-for-net/v3/developer-guide/net-dg-region-selection.html)
- [他のアプリケーションパラメータの設定 - AWS SDK for .NET](https://docs.aws.amazon.com/ja_jp/sdk-for-net/v3/developer-guide/net-dg-config-other.html)
- [AWS SDK for .NET - 開発者ガイド](https://docs.aws.amazon.com/ja_jp/sdk-for-net/v3/developer-guide/aws-sdk-net-v3-dg.pdf)
- [Access Key Management for .NET Applications – Part 1](https://aws.amazon.com/jp/blogs/developer/access-key-management-for-net-applications-part-1/)
  - ちょっと古いです。

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

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

```yaml
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 なら例えば以下で接続できます。

```shell
$ 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# のコードで設定を書く場合

```csharp
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

```csharp
  // 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.json` は `V2` で読み込まれます。一方で、リンク先の `endpoints.json`は `V3` 用のものになっています。

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

```json: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` だけを書き換えます。

```diff
{
  "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` での接続になってしまうので、コードは以下のようにする必要があります。

```csharp
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 の場合は調べておらず）

```csharp
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