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.
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.
Desenvolva separado, e implante junto se quiser.
Se desenvolver junto, dificilmente implantará separado!
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.
0 comentários