Featured image of post .NET and AWS S3 with LocalStack: How to develop with local S3 buckets

.NET and AWS S3 with LocalStack: How to develop with local S3 buckets

Simplifying cloud testing with LocalStack

Follow me
This post is part of a series:
Part  1  -  .NET and AWS S3 with LocalStack: How to develop with local S3 buckets

Introduction Link to this section

LocalStack is an open-source framework that allows us to emulate the major AWS services locally, making it easier to develop and test cloud applications without incurring the cost and complexity of deploying to a real cloud environment.

In this post, I’ll show how to configure it to emulate S3 buckets and how to interact with those buckets from a C# application.

Running LocalStack Link to this section

LocalStack can be run as a CLI or using a container. In this post, I’ll explain how to run it in a container with docker run and docker-compose.

ℹ️ If you don’t have docker or other container runtime installed, click here for Docker installation instructions or here for Podman installation instructions.

Container Link to this section

To start a container with a LocalStack instance, run the following command:

docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 -e EXTRA_CORS_ALLOWED_ORIGINS=https://app.localstack.cloud. localstack/localstack:1.3.1

Note that it is exposing port 4566 and ports 4510 to 4559 and allowing CORS access from https://app.localstack.cloud. (to allow access from the LocalStack dashboard).

🚨 It’s recommended to use a specific version instead of latest to avoid problems with new versions updated automatically.

Docker compose Link to this section

Starting LocalStack from docker compose is just as easy. Just add the localstack service, as below, to a docker-compose.yaml file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
version: "3.8"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"            # LocalStack Gateway
      - "127.0.0.1:4510-4559:4510-4559"  # external services port range
    environment:
      - DEBUG=${DEBUG-}
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
      - DOCKER_HOST=unix:///var/run/docker.sock
      - EXTRA_CORS_ALLOWED_ORIGINS=https://app.localstack.cloud. # Enable access from the dashboard
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

Then run the following command:

docker-compose up

🚨 It’s recommended to use a specific version instead of latest to avoid problems with new versions updated automatically.

⚠️ I’ve added the environment variable EXTRA_CORS_ALLOWED_ORIGINS with the value https://app.localstack.cloud. to allow access from the LocalStack dashboard.

💡 The updated docker-compose.yaml file can be found here, in the LocalStack repo.

LocalStack Dashboard Link to this section

LocalStack has a web-based dashboard that allows us to manage and configure its services and visualize its logs.

Access https://app.localstack.cloud. and it will connect to the LocalStack instance running locally.

It runs in the browser, so there is no need to expose any ports to the internet.

LocalStack dashboard showing the services statuses

🚨 If the dashboard says “Please start LocalStack to check the System Status” and the container log shows Blocked CORS request from forbidden origin https://app.localstack.cloud., the EXTRA_CORS_ALLOWED_ORIGINS environment variable was not correctly set to https://app.localstack.cloud.. See here.

Interacting with LocalStack using the AWS CLI Link to this section

We’ll use the AWS CLI to interact with LocalStack. If you don’t have the AWS CLI, look here for instructions on how to install it.

Configuring a profile for LocalStack Link to this section

The AWS CLI requires credentials when running. LocalStack doesn’t validate the credentials by default, so will create a profile with anything as access key and secret key just to make the CLI happy.

  1. In a terminal, type aws configure --profile localstack;
  2. For AWS Access Key ID [None]:, type anything;
  3. For AWS Secret Access Key [None]:, type anything;
  4. For Default region name [None]:, type the region you prefer (for example, us-east-1);
  5. For Default output format [None]:, type json.

How to create a bucket using the AWS CLI Link to this section

To create a S3 bucket in LocalStack, we’ll use the aws s3 mb command (mb is short for Make Bucket).

The command below will create a bucket with the name local-bucket-name using the AWS CLI profile we previously created with the name localstack. It’s important to pass the --endpoint parameter or else it will try to create the bucket in AWS.

aws s3 mb s3://local-bucket-name --endpoint http://localhost:4566 --profile localstack

Looking at LocalStack dashboard we can see the bucket was created:

LocalStack dashboard showing the local buckets

How to list a bucket contents using the AWS CLI Link to this section

To look the contents of a bucket, we can use the aws s3 ls command:

aws s3 ls s3://local-bucket-name --endpoint http://localhost:4566 --profile localstack

Or use the LocalStack dashboard:

LocalStack dashboard showing a bucket content

Accessing LocalStack from .NET Link to this section

To access an S3 bucket from LocalStack in .NET we use the same libraries we use to access it in AWS.

In this example, I’ll use the AWSSDK.S3 and the AWSSDK.Extensions.NETCore.Setup NuGet packages, both from AWS.

How does the AmazonS3Client get AWS access data? Link to this section

When running in AWS, the AmazonS3Client will get its access data from the IAM Role attached to the service running it. When running locally, it will get from the AWS CLI profile named default or from the settings we pass to it.

In the code below, I’m checking for a configuration section with the name AWS, that is not present in the production environment. If it’s found, I set the Region, ServiceURL and ForcePathStyle properties of the AmazonS3Config and pass it to the creation of the AmazonS3Client.

Program.cs Link to this section

1
2
3
4
5
var builder = WebApplication.CreateBuilder(args);

builder.AddAwsServices();

var app = builder.Build();

AwsExtensions.cs Link to this section

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class AwsExtensions
{
    public static void AddAwsServices(this WebApplicationBuilder builder)
    {
        builder.Services.AddSingleton<IAmazonS3>(sc =>
        {
            var configuration = sc.GetRequiredService<IConfiguration>();
            var awsConfiguration = configuration.GetSection("AWS").Get<AwsConfiguration>();

            if (awsConfiguration?.ServiceURL is null)
            {
                return new AmazonS3Client();
            }
            else
            {
                return AwsS3ClientFactory.CreateAwsS3Client(
                    awsConfiguration.ServiceURL,
                    awsConfiguration.Region, awsConfiguration.ForcePathStyle,
                    awsConfiguration.AwsAccessKey, awsConfiguration.AwsSecretKey);
            }
        });
    }
}

The appsettings.Development.json has the configurations pointing to the LocalStack instance:

appsettings.Development.json Link to this section

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "BucketName": "local-bucket-name",

  "AWS": {
    "Region": "us-east-1",
    "ServiceURL": "http://localhost:4566",
    "ForcePathStyle": "true",
    "AwsAccessKey": "test",
    "AwsSecretKey": "test"
  }
}

⚠️ The ForcePathStyle forces the use of https://s3.amazonaws.com/<bucket-name>/<object-key> styled URLs instead of https://<bucket-name>.s3.amazonaws.com/<object-key> URLs.

The ForcePathStyle needs to be set to true for the AmazonS3Client to work with LocalStack.

Upload an image to the S3 Bucket Link to this section

Using Minimal APIs, I created an endpoint that receives a file and saves it in the S3 bucket.

The code is straight forward:

Program.cs Link to this section

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
app.MapPost("/upload", async (IAmazonS3 s3Client, IFormFile file) =>
{
    var bucketName = builder.Configuration["BucketName"]!;

    var bucketExists = await s3Client.DoesS3BucketExistAsync(bucketName);

    if (!bucketExists)
    {
        return Results.BadRequest($"Bucket {bucketName} does not exists.");
    }

    using var fileStream = file.OpenReadStream();

    var putObjectRequest = new PutObjectRequest()
    {
        BucketName = bucketName,
        Key = file.FileName,
        InputStream = fileStream
    };

    putObjectRequest.Metadata.Add("Content-Type", file.ContentType);

    var putResult = await s3Client.PutObjectAsync(putObjectRequest);

    return Results.Ok($"File {file.FileName} uploaded to S3 successfully!");
});

Now, we can test it from Postman:

File upload from Postman

Get an image from the S3 Bucket Link to this section

I also created an endpoint that returns the file with the key passed by parameter from the S3 bucket:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
app.MapGet("/object/{key}", async (IAmazonS3 s3Client, string key) =>
{
    var bucketName = builder.Configuration["BucketName"]!;

    var bucketExists = await s3Client.DoesS3BucketExistAsync(bucketName);

    if (!bucketExists)
    {
        return Results.BadRequest($"Bucket {bucketName} does not exists.");
    }

    try
    {
        var getObjectResponse = await s3Client.GetObjectAsync(bucketName,
            key);

        return Results.File(getObjectResponse.ResponseStream,
            getObjectResponse.Headers.ContentType);
    }
    catch (AmazonS3Exception ex) when (ex.ErrorCode.Equals("NotFound", StringComparison.OrdinalIgnoreCase))
    {
        return Results.NotFound();
    }
});

Testing from Postman, we can see the image previously uploaded:

Getting the file from Postman

Full source code Link to this section

GitHub repository

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy