When developing applications that integrate with cloud services, I find it to be a more pleasant experience if I can raise, debug and destroy instances of these services locally as I build my application. In the case of Amazon S3, I might have a number of files I need to be moving around during development, and I don’t want to wait while transferring these files between my local machine and the cloud.

LocalStack to the rescue

LocalStack offers a number of Amazon APIs that can be brought up on your local machine. Importantly, it also comes in a container and so Amazon APIs on our local are only a docker-compose away. Here’s what it looks like;

version: '3.2'
services:
  localstack:
    image: localstack/localstack:latest
    container_name: localstack
    ports:
      - 4566:4566
    environment:
      - SERVICES=s3
      - DEBUG=1
      - DATA_DIR=/tmp/localstack/data
    volumes:
      - ./.localstack:/tmp/localstack
      - /var/run/docker.sock:/var/run/docker.sock

Running docker-compose up on the above will bring the services (in this case just S3) up and if you browse to http://localhost:4566 you’ll get the following message, which means you’re ready to go;

{"status": "running"}

Note that LocalStack used to have a web UI, but it’s deprecated, doesn’t seem to do much, and you probably don’t need it. It is still available via localstack/localstack-full:latest, but I wouldn’t bother.

Setting up a bucket

Now that LocalStack is running, we can use the AWS CLI tool to configure a bucket;

aws --endpoint-url=http://localhost:4566 s3 mb s3://my-bucket
aws --endpoint-url=http://localhost:4566 s3api put-bucket-acl --bucket my-bucket --acl public-read
aws --endpoint-url=http://localhost:4566 s3 cp ~/Desktop/some-image.jpg s3://my-bucket/image.jpg

The lines above create the bucket, set a public-read ACL on it so subsequently uploaded files will be accessible, and, the last line copies a file into the bucket.

At this point we can now pop http://localhost:4566/my-bucket/image.jpg into our browser and we should be able to see the image we uploaded. If we didn’t execute the second line, or, we executed it after uploading the file, browsing to that key will return HTTP Status Code 403 (Access Denied).

Integrating with .NET Core

With the service running and the bucket configured we’re ready to integrate. We can add AWSSDK.S3 to our .NET Core project with the following;

dotnet add package AWSSDK.S3

Using native dependency injection, and prior to pointing at our local, we might configure IAmazonS3 as follows;

services
    .AddSingleton<IAmazonS3>(p => {
        var config = new AmazonS3Config
        {
            RegionEndpoint = RegionEndpoint.USWest2,
        };
        return new AmazonS3Client(myAccessKey, mySecret, config);
    });

To point this configuration instead at our LocalStack instance we’ll edit the config object we pass to Amazons3Client with the following;

if (p.GetService<IHostEnvironment>().IsDevelopment())
{
    config.ForcePathStyle = true;
    config.ServiceURL = "http://localhost:4566";
}

We need to set ServiceURL to LocalStack and set ForcePathStyle to true. Additionally, we do this only for the Development environment, meaning when we publish as Production, we’ll point at Amazon S3 rather than LocalStack. The completed code looks like this;

services
    .AddSingleton<IAmazonS3>(p => {
        var config = new AmazonS3Config
        {
            RegionEndpoint = RegionEndpoint.USWest2,
        };
        if (p.GetService<IHostEnvironment>().IsDevelopment())
        {
            config.ForcePathStyle = true;
            config.ServiceURL = options.Url;
        }
        return new AmazonS3Client(myAccessKey, mySecret, config);
    });

Regardless of whether we are integrating with Amazon S3 or LocalStack on our local, the mechanism to transfer files remains unchanged;

public async Task<IActionResult> Upload([FromServices] IAmazonS3 s3)
{
    await new TransferUtility(s3)
        .UploadAsync(source, "my-bucket", "some/path/image.jpg");
    return Ok();
} 

Conclusion

Wrapping it up, I’ve used LocalStack specifically for S3 a couple of times and the points are to ignore the web UI - you don’t need it - and make sure to set ForcePathStyle and the ServiceURL when integrating with .NET.