fbpx
.NET ASPIRE – Criando databases no PostgreSQL e aplicando Migrations com FluentMigrator
Publicado em: quinta-feira, 26 de set de 2024
Categorias: .NET | Arquitetura

No .NET ASPIRE existe uma forma de você expressar para o Orquestrador, um database dentro do ASPIRE para que o serviço de service discovery entregue uma conexão corretamente para nossas aplicações.

O problema é que o .NET Aspire não cria esse banco de dados, então é isso que vamos abordar hoje.

Contexto

Aqui na Academia Dev estou trabalhando diariamente, de segunda-a-sexta para criar um Gateway de Pagamentos Fake, com a intenção de explorar aspectos de arquitetura de software, arquitetura de soluções e design de solução.

Transformei a criação desse projeto em uma jornada diária onde AO VIVO mostramos desde a criação do primeiro repositório GIT até sua entrada em produção. Desde a primeira à última linha de código até a criação da infra de CI/CD e os primeiros servidores.

Ao escolher uma implementação com multi-tenant com nível de maturidade SaaS 4, temos o desafio de criar novos recursos dinamicamente.

  • Servidores
  • Instâncias de banco
  • Bancos de dados
  • Objetos (tabelas sequences etc)

Mas antes de chegar nesse ponto, temos a demanda de criar o database principal, que não é dinâmico. Esse database é onde ficam as definições de workspace, informações de quais tenants estão em quais servidores, e dados administrativos sobre cada tenant.

Saiba mais sobre a Jornada Academia Pay

Entendendo o Aspire

O .NET Aspire oferece uma camada consistente porém simples de service discovery.

O orquestrador (ferramenta apenas de desenvolvimento) define quais são os recursos e ao executar os projetos ele injeta configurações.

A injeção de configurações acontece injetando variáveis de ambiente, que pela natureza do mecanismo de configuração do .NET, com múltiplas fontes, respeita a precedência e sobrescreve, em memória os dados de configuração que poderiam existir nos arquivos de configuração appsettings.json e similares, por aquilo que foi injetado via variável de ambiente.

Esse mecanismo usa apenas o que sempre existiu no .NET (Core). Assim para sobrescrever uma configuração { ConnectionStrings: { Key: "Value1" } } com Value2, basta adicionar como variável de ambiente ConnectionStrings__Key = Value2.

A criação do Servidor PostgreSQL

Um dos recursos principais que temos a demanda aqui é o banco de dados primário, aquele que armazena informações administrativas.

var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("mainpostgres")
    .WithPgAdmin();

var mainDB = postgres.AddDatabase("maindb");

builder.AddProject<Projects.AcademiaPay_Backoffice_WebApi>("academiapay-backoffice-webapi")
    .WithReference(mainDB);

builder.Build().Run();

Com essa configuração mínima somos capazes de criar um container do PostgreSQL (mainpostgres) com um PgAdmin do lado, também como container.

Fazemos isso nas linhas 3 e 4.

Na linha 6 definimos temos a chance de dizer que precisamos de um novo banco de dados dentro desse PostgreSQL. Fazemos isso chamando var mainDB = postgres.AddDatabase("maindb"); onde produzimos um resource que conseguimos referenciar na linha 9.

Parece ótimo certo?

A Api recebe na connectionstring o servidor correto com o database correto, entretanto o Aspire não cria o tal database.

A issue Aspire.Npgsql component does not create database #1170 descreve isso

E Damian Edwards confirma.


Original EN-US
Original EN-US

Tradução Google Tradutor PT-BR
Tradução Google Tradutor PT-BR


De fato é prudente para o Aspire não se meter nesse vespeiro. Não é responsabilidade do ASPIRE realizar esse tipo de operação, visto que, estamos diante de algo que pode ser complexo, e com muitas variáveis que fogem ao propósito do projeto.

  • Ao criar um database, com qual usuário?
  • Qual datafile ou tablespace?
  • Tem de criar usuário?
  • Tem de dar permissão?
  • Roles?

Não é papel nem escopo do ASPIRE lidar com essas coisas.

Mas nada impede que a comunidade faça o seu papel de comunidade e crie quem faça!

O principal ponto que debuta contra a criação de algo tão complexo com Aspire, é que o orquestrador é um componente que não será implantado com sua aplicação, portanto se você atribuir responsabilidades demais, ficará em maus lençóis pois não tem quem exerça esse papel dessa forma em produção.

E a solução não é pensar que o orquestrador do Aspire devesse ser um componente de produção.

Essa é uma ideia tão simplista quanto estúpida: Se o orquestrador do Aspire chegasse perto de ser um componente de produção, então ele precisaria de mais de 1 década para chegar perto do nível de maturidade necessário.

Então como lidar com a criação física desse database?

Projetos de bootstrap são a solução.

Nesse momento, em que estamos na segunda semana de trabalho, com aproximadamente 14 horas de trabalho, nosso Program.cs do orquestrador do Aspire está assim.

var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("mainpostgres")
    .WithPgAdmin();

var mainDB = postgres.AddDatabase("maindb");
var postgresDB = postgres.AddDatabase("postgres");

var cache = builder.AddRedis("cache")
       .WithRedisCommander();

var bootstrapProject = builder.AddProject<Projects.AcademiaPay_Backoffice_Bootstrap>("academiapay-backoffice-bootstrap")
    .WithReference(mainDB)
    .WithReference(postgresDB)
    .WithReference(cache);

builder.AddProject<Projects.AcademiaPay_Backoffice_WebApi>("academiapay-backoffice-webapi")
    .WithReference(bootstrapProject)
    .WithReference(mainDB)
    .WithReference(cache);

builder.AddProject<Projects.AcademiaPay_Backoffice_Worker>("academiapay-backoffice-worker")
    .WithReference(bootstrapProject)
    .WithReference(mainDB)
    .WithReference(cache);

builder.Build().Run();

Ainda falta

  • Keycloak para Identidade
  • RabbitMQ como Message Broker
  • Apache APISix como Api Gateway

nomes já são confirmados nessa festa!

Nem chegamos lá, e já temos alguns desafios, como por exemplo criar o bendito database.

Estruturando o pensamento ao redor da solução

Quais são as possibilidades?

1) Parametrizar a criação do PostgreSQL

Considerando que o Aspire usa a imagem default do PostgreSQL, lá do docker hub, sabemos que criar um único database com essa imagem é ridiculamente simples, basta parametrizar, como a documentação de uso da imagem descreve.

Assim a configuração abaixo resolveria:

var postgres = builder.AddPostgres("mainpostgres")
    .WithEnvironment("POSTGRES_DB", "maindb")
    .WithPgAdmin();

Entretanto, a imagem não oferece suporte para a criação de múltiplos bancos. O que é muito necessário em contexto de microsserviços.

2) Criar script SQL de Inicialização da Instância

Os principais bancos de dados containerizados, exceto o SQL Server, possuem uma pasta padrão do qual na primeira vez que inicializarem o banco, executarão seus scripts.

Essa padronização é útil para permitir configurações mais complexas de ambiente. Bastaria adicionar scripts SQL ou SH na pasta /docker-entrypoint-initdb.d e o próprio container faria esse trabalho na primeira execução (somente na primeira execução).

Esse foi o motivo pelo qual criei o projeto e a imagem luizcarlosfaria/mssql-server-linux:2019-latest em 2019.

Para dar ao SQL Server o mesmo comportamento que MariaDB, Postgres e MySQL possuem.

3) Criar um bootstrapper

Um projeto C# que criasse os recursos necessários para a execução da aplicação. Esse projeto criaria o banco e tudo mais que fosse necessário.

Escolhendo a solução

Para esse projeto escolhi a criação de um bootstrapper por alguns motivos:

  • Para esse projeto, não basta criar o banco de dados, existem outras configurações que são dinâmicas, como o API Gateway que dinamicamente deve receber interações para a criação de credenciais de acesso. Portanto um projeto de bootstrap se faz necessário.
  • Dada a necessidade de mais tarde criar outros servidores, containers e bancos de dados dinamicamente, fazia sentido criar o próprio database via C#, permitindo ter um reaproveitamento desse código mais tarde, evitando duplicações.
  • Independente de usar um script ou criar um bootstrap para criar o database, um outro bootstrap deve ser criado para aplicar as migrations com o projeto. Demandando novamente um projeto de bootstrap.

Criar um bootstrapper normalmente não é a opção óbvia, mas nesse caso, nesse projeto, por conta das demais variáveis e necessidades, se tornou.

Execução

Via Aspire o projeto bootstrapper recebe como informação de conexão, 2 connectionstrings:

  • Uma para o Database postgres (default), pronta para uso.
  • Uma para o Database maindb (o Database que criaremos), ainda precisando que o Database seja criado para poder ser usada.

Na linha 6 definimos o banco de dados alvo, aquele que queremos criar.

Na linha 7 definimos o banco de dados padrão, aquele que permite conexões por ser o banco default do postgres.

Sob a ConnectionString postgres, o projeto Bootstrapper se conecta com sucesso ao banco e executa a criação do database maindb.

Já nossa Migration implementada com FluentMigrator executa a criação dos objetos na conexão maindb, que a partir do passo anterior já está pronta para ser usada.

private void ExecuteDDL()
{
    ArgumentNullException.ThrowIfNull(configuration);

    var connectionString = configuration.GetValue<string>("ConnectionStrings:maindb");

    ArgumentNullException.ThrowIfNullOrWhiteSpace(connectionString);

    var serviceProvider = new ServiceCollection().AddFluentMigratorCore()
        .ConfigureRunner(rb => rb
                .AddPostgres15_0()
                .WithGlobalConnectionString(connectionString)
                .ScanIn(typeof(BootstrapperSetup).Assembly).For.Migrations())
                .AddLogging(lb => lb.AddFluentMigratorConsole())
        .BuildServiceProvider(false);

    using (var scope = serviceProvider.CreateScope())
    {
        var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();

        runner.MigrateUp();
    }
}

Assim conseguimos lidar com essa inicialização sem muita dor de cabeça, já endereçando os próximos assuntos.

O Cloud Native .NET é meu principal projeto.

Onde empenho energia para ajudar, acompanhar, direcionar Desenvolvedores, Líderes Técnicos e jovens Arquitetos na jornada Cloud Native.

Conduzo entregando a maior e mais completa stack de tecnologias do mercado.

Ao trabalhar com desenvolvedores experientes, eu consigo usar seu aprendizado com .NET, banco de dados, e arquitetura para encurtar a jornada.

Ao restringir à desenvolvedores .NET eu consigo usar do contexto de tecnologias e problemas do seu dia-a-dia, coisas que você conhece hoje, como WCF, WebForms, IIS e MVC, por exemplo, para mostrar a comparação entre o que você conhece e o que está sendo apresentado.

É assim que construímos fundamentos sólidos, digerindo a complexidade com didática, tornando o complexo, simples.

É assim que conseguimos tornar uma jornada densa, em um pacote de ~4 meses.

Eu não acredito que um desenvolvedor possa entender uma tecnologia sem compreender seus fundamentos. Ele no máximo consegue ser produtivo, mas isso não faz desse desenvolvedor um bom tomador de decisões técnicas.

É preciso entender os fundamentos para conseguir tomar boas decisões.

0 comentários

Enviar um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Luiz Carlos Faria

Mensagem do Autor

Espero que goste desse post. Não deixe de comentar e falar o que achou.

Se acha que esse post pode ajudar alguém que você conheça, compartilhe!

 

Lives

Fique de olho nas lives

Fique de olho nas lives no meu canal do Youtube, no Canal .NET e nos Grupos do Facebook e Instagram.

Aceleradores

Existem diversas formas de viabilizar o suporte ao teu projeto. Seja com os treinamentos, consultoria, mentorias em grupo.