Featured image of post .NET e AWS S3 com LocalStack: Como desenvolver com buckets S3 locais

.NET e AWS S3 com LocalStack: Como desenvolver com buckets S3 locais

Simplificando o teste na nuvem com LocalStack

Esse post é parte de uma série:
Parte  1  -  .NET e AWS S3 com LocalStack: Como desenvolver com buckets S3 locais

Introdução Link to this section

LocalStack é uma estrutura de código aberto que nos permite emular os principais serviços da AWS localmente, facilitando o desenvolvimento e o teste de aplicativos na nuvem sem incorrer no custo e na complexidade da implantação em um ambiente de nuvem real.

Neste post, mostrarei como configurá-lo para emular buckets S3 e como interagir com esses buckets a partir de um aplicativo C#.

Executando o LocalStack Link to this section

O LocalStack pode ser executado como uma CLI ou usando um container. Neste post, explicarei como executá-lo em um container com docker run e docker-compose.

ℹ️ Se você não tiver o docker ou outro tempo de execução de container instalado, clique aqui para obter instruções de instalação do Docker ou aqui para obter instruções de instalação do Podman.

Container Link to this section

Para iniciar um container com uma instância do LocalStack, execute o seguinte comando:

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

Observe que ele está expondo a porta 4566 e as portas 4510 a 4559 e permitindo o acesso CORS de https://app.localstack.cloud. (para permitir o acesso do painel do LocalStack).

🚨 É recomendável usar uma versão específica em vez de latest para evitar problemas com novas versões atualizadas automaticamente.

Docker compose Link to this section

Iniciar o LocalStack a partir do docker compose é tão fácil quanto. Basta adicionar o serviço localstack, como abaixo, a um arquivo docker-compose.yaml:

 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"            # Gateway LocalStack
      - "127.0.0.1:4510-4559:4510-4559"  # intervalo de portas de serviços externos
    environment:
      - DEBUG=${DEBUG-}
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
      - DOCKER_HOST=unix:///var/run/docker.sock
      - EXTRA_CORS_ALLOWED_ORIGINS=https://app.localstack.cloud. # Habilita o acesso do painel
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

Em seguida, execute o seguinte comando:

docker-compose up

🚨 É recomendável usar uma versão específica em vez de latest para evitar problemas com novas versões atualizadas automaticamente.

⚠️ Adicionei a variável de ambiente EXTRA_CORS_ALLOWED_ORIGINS com o valor https://app.localstack.cloud. para permitir o acesso do painel do LocalStack.

💡 O arquivo docker-compose.yaml atualizado pode ser encontrado aqui, no repositório LocalStack.

Painel do LocalStack Link to this section

O LocalStack tem um painel baseado na web que nos permite gerenciar e configurar seus serviços e visualizar seus logs.

Acesse https://app.localstack.cloud. e ele se conectará à instância do LocalStack em execução localmente.

Ele é executado no navegador, portanto, não há necessidade de expor nenhuma porta à Internet.

Painel do LocalStack mostrando os status dos serviços

🚨 Se o painel disser “Inicie o LocalStack para verificar o status do sistema” e o log do container mostrar Solicitação CORS bloqueada da origem proibida https://app.localstack.cloud., a variável de ambiente EXTRA_CORS_ALLOWED_ORIGINS não foi definida corretamente como https://app.localstack.cloud.. Veja aqui.

Interagindo com o LocalStack usando a AWS CLI Link to this section

Usaremos a AWS CLI para interagir com o LocalStack. Se você não tiver a AWS CLI, consulte aqui para obter instruções sobre como instalá-la.

Configurando um perfil para o LocalStack Link to this section

A AWS CLI requer credenciais ao ser executada. O LocalStack não valida as credenciais por padrão, portanto, criará um perfil com qualquer coisa como chave de acesso e chave secreta apenas para deixar a CLI feliz.

  1. Em um terminal, digite aws configure --profile localstack;
  2. Para AWS Access Key ID [None]:, digite qualquer coisa;
  3. Para AWS Secret Access Key [None]:, digite qualquer coisa;
  4. Para Default region name [None]:, digite a região que você preferir (por exemplo, us-east-1);
  5. Para Default output format [None]:, digite json.

Como criar um bucket usando a AWS CLI Link to this section

Para criar um bucket S3 no LocalStack, usaremos o comando aws s3 mb (mb é a abreviação de Make Bucket).

O comando abaixo criará um bucket com o nome local-bucket-name usando o perfil da AWS CLI que criamos anteriormente com o nome localstack. É importante passar o parâmetro --endpoint ou então ele tentará criar o bucket na AWS.

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

Olhando para o painel do LocalStack, podemos ver que o bucket foi criado:

Painel do LocalStack mostrando os buckets locais

Como listar o conteúdo de um bucket usando a AWS CLI Link to this section

Para ver o conteúdo de um bucket, podemos usar o comando aws s3 ls:

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

Ou use o painel do LocalStack:

Painel do LocalStack mostrando o conteúdo de um bucket

Acessando o LocalStack do .NET Link to this section

Para acessar um bucket S3 do LocalStack no .NET, usamos as mesmas bibliotecas que usamos para acessá-lo na AWS.

Neste exemplo, usarei os pacotes NuGet AWSSDK.S3 e AWSSDK.Extensions.NETCore.Setup, ambos da AWS.

Como o AmazonS3Client obtém os dados de acesso da AWS? Link to this section

Ao ser executado na AWS, o AmazonS3Client obterá seus dados de acesso da função IAM anexada ao serviço que o executa. Ao ser executado localmente, ele obterá do perfil da AWS CLI chamado default ou das configurações que passamos para ele.

No código abaixo, estou verificando uma seção de configuração com o nome AWS, que não está presente no ambiente de produção. Se for encontrado, defino as propriedades Region, ServiceURL e ForcePathStyle do AmazonS3Config e passo-o para a criação do 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);
            }
        });
    }
}

O appsettings.Development.json tem as configurações apontando para a instância do LocalStack:

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"
  }
}

⚠️ O ForcePathStyle força o uso de URLs estilizadas https://s3.amazonaws.com/<bucket-name>/<object-key> em vez de URLs https://<bucket-name>.s3.amazonaws.com/<object-key>.

O ForcePathStyle precisa ser definido como true para que o AmazonS3Client funcione com o LocalStack.

Enviar uma imagem para o bucket S3 Link to this section

Usando APIs mínimas, criei um endpoint que recebe um arquivo e o salva no bucket S3.

O código é direto:

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($"O bucket {bucketName} não existe.");
    }

    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($"Arquivo {file.FileName} enviado para o S3 com sucesso!");
});

Agora, podemos testá-lo no Postman:

Envio de arquivo do Postman

Obter uma imagem do bucket S3 Link to this section

Também criei um endpoint que retorna o arquivo com a chave passada por parâmetro do bucket S3:

 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($"O bucket {bucketName} não existe.");
    }

    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();
    }
});

Testando no Postman, podemos ver a imagem enviada anteriormente:

Obtendo o arquivo do Postman

Código fonte completo Link to this section

Repositório GitHub

💬 Like or have something to add? Leave a comment below.
Ko-fi
GitHub Sponsor
Licensed under CC BY-NC-SA 4.0
Criado com Hugo
Tema Stack desenvolvido por Jimmy