GoSuda

Executando um servidor Go de forma extensível no dotnet aspire

By snowmerak
views ...

O que é dotnet aspire?

O dotnet aspire é uma ferramenta criada para auxiliar os desenvolvedores no desenvolvimento e configuração de ambientes cloud-native, à medida que estes se tornam mais prevalentes. Esta ferramenta permite que os desenvolvedores .NET implantem facilmente projetos .NET, diversas infraestruturas cloud-native e serviços ou contêineres em outras linguagens.

Naturalmente, do Docker ao k8s, com o lançamento e operação, muitos setores, indústrias e desenvolvedores têm migrado ou já migraram do ambiente on-premise tradicional para o ambiente cloud-native. É um campo amadurecido agora. Portanto, acredito que não há necessidade de explicar as inconveniências anteriores em relação a nomes de host, configuração de portas, firewalls e gerenciamento de métricas.

Portanto, mesmo com as explicações acima, você provavelmente não terá uma ideia clara do que é o dotnet aspire. Isso ocorre porque nem mesmo a Microsoft forneceu uma definição precisa. Por isso, também não farei uma definição específica. No entanto, usarei as funcionalidades básicas do dotnet aspire que entendi neste artigo, então você pode usá-lo como referência para definir sua própria posição.

Estrutura do Projeto

Criação de um Projeto dotnet aspire

Se você não tiver o modelo dotnet aspire, precisará instalá-lo primeiro. Instale o modelo com o seguinte comando. Se você não tiver o .NET, instale-o por conta própria.

1dotnet new install Aspire.ProjectTemplates

Em seguida, crie uma nova solução em uma pasta apropriada.

1dotnet new sln

Depois, na pasta da solução, execute o seguinte comando para criar um projeto do modelo aspire-apphost.

1dotnet new aspire-apphost -o AppHost

Isso criará um projeto aspire-apphost contendo apenas um código simples para configuração.

Adicionar Valkey

Vamos adicionar o Valkey de forma simples.

Antes de adicionar, o dotnet aspire oferece diversas soluções de terceiros por meio de community hosting. Naturalmente, o Valkey também pode ser suportado por este community hosting e pode ser facilmente utilizado através do seguinte pacote NuGet.

1dotnet add package Aspire.Hosting.Valkey

Além disso, oferece uma variedade de hosting integrados, que podem ser verificados aqui. Voltando ao Valkey, abra o arquivo Program.cs no projeto AppHost e modifique-o da seguinte forma:

1var builder = DistributedApplication.CreateBuilder(args);
2
3var cache = builder.AddValkey("cache")
4    .WithDataVolume(isReadOnly: false)
5    .WithPersistence(interval: TimeSpan.FromMinutes(5),
6        keysChangedThreshold: 100);
7
8builder.Build().Run();

cache é uma implementação da interface IResourceBuilder que contém informações para construir o serviço Valkey.WithDataVolume cria um volume para armazenar dados de cache, e WithPersistence permite que os dados de cache sejam armazenados persistentemente. Até aqui, parece ter uma função semelhante aos volumes do docker-compose. Naturalmente, vocês também podem criá-los sem dificuldade. No entanto, isso excede o escopo deste artigo, então não discutiremos isso agora.

Criação de um Servidor Go Echo

Vamos adicionar um servidor Go simples. Primeiro, na pasta da solução, crie um workspace com go work init. Para um desenvolvedor .NET, um workspace Go é semelhante a uma solução.

Em seguida, crie uma pasta chamada EchoServer, mova-se para ela e execute go mod init EchoServer. Este comando cria um módulo Go. Para um desenvolvedor .NET, um módulo pode ser entendido como semelhante a um projeto. Depois, crie um arquivo main.go e escreva o seguinte:

 1package main
 2
 3import (
 4	"log"
 5	"net/http"
 6	"os"
 7)
 8
 9func main() {
10	addr := os.Getenv("PORT")
11	log.Printf("Server started on %s", addr)
12
13	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
14		name := request.URL.Query().Get("name")
15		writer.Write([]byte("Hello, " + name))
16	})
17
18	http.ListenAndServe(":"+addr, nil)
19}

Este servidor, quando o Aspire AppHost é executado, injeta a variável de ambiente PORT na qual deve escutar, lê essa porta e inicia o servidor. É um servidor simples que recebe uma query name e retorna Hello, {name}.

Agora, vamos adicionar este servidor ao dotnet aspire.

Adicionando o Servidor Echo ao Aspire

Volte ao projeto Aspire AppHost, onde você adicionou o Valkey, e adicione o community hosting para a linguagem Go.

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

Em seguida, abra o arquivo Program.cs e adicione a seguinte instrução:

 1var builder = DistributedApplication.CreateBuilder(args);
 2
 3var cache = builder.AddValkey("cache")
 4    .WithDataVolume(isReadOnly: false)
 5    .WithPersistence(interval: TimeSpan.FromMinutes(5),
 6        keysChangedThreshold: 100);
 7
 8var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
 9    .WithHttpEndpoint(port: 3000, env: "PORT")
10    .WithExternalHttpEndpoints();
11
12builder.Build().Run();

Aqui, echoServer é uma implementação da interface IResourceBuilder que contém informações para construir o servidor Go. O método AddGolangApp que acabamos de adicionar é um método de extensão do host personalizado para adicionar um servidor Go. É possível verificar que ele usa a porta 3000 fixamente e injeta a variável de ambiente PORT. Finalmente, WithExternalHttpEndpoints permite o acesso externo.

Para testar, acesse http://localhost:3000/?name=world e você poderá ver Hello, world sendo exibido.

No entanto, atualmente o dotnet aspire impõe uma penalidade significativa a projetos non-dotnet. É exatamente isso...

Expansão do Projeto

Como realizar o dimensionamento horizontal?

Atualmente, o dotnet aspire oferece a opção WithReplica apenas para os construtores de projetos .NET adicionados com o método AddProject. No entanto, essa opção não é fornecida para hosts de linguagem Go ou projetos externos como AddContainer.

Portanto, é necessário implementar diretamente usando um balanceador de carga ou proxy reverso separado. No entanto, isso pode tornar o proxy reverso um SPOF, por isso é preferível que o proxy reverso ofereça a opção WithReplica. Isso implica que o proxy reverso precisa ser um projeto .NET.

Até agora, eu usei métodos como nginx, trafik ou implementação direta para resolver esse problema, mas com a restrição de ser um projeto .NET, eu não tinha uma solução imediata. Então, procurei por um proxy reverso implementado em .NET e, felizmente, havia a opção YARP. O YARP é um proxy reverso implementado em .NET, que também pode atuar como balanceador de carga e oferece uma variedade de recursos, o que o tornou uma boa escolha.

Agora, vamos adicionar o YARP.

Configurando o proxy reverso com YARP

Primeiro, crie um projeto para usar o YARP.

1dotnet new web -n ReverseProxy

Em seguida, navegue até o projeto e instale o YARP.

1dotnet add package Yarp.ReverseProxy --version 2.2.0

Após a instalação, abra o arquivo Program.cs e escreva o seguinte:

 1using Yarp.ReverseProxy.Configuration;
 2
 3var builder = WebApplication.CreateBuilder(args);
 4
 5var routes = new List<RouteConfig>();
 6var clusters = new List<ClusterConfig>();
 7
 8builder.Services.AddReverseProxy()
 9    .LoadFromMemory(routes, clusters);
10
11var app = builder.Build();
12
13app.MapReverseProxy();
14app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");

Este código é o básico para usar o YARP.routes contém informações de rota a serem usadas pelo proxy reverso, e clusters contém informações de cluster a serem usadas pelo proxy reverso. Essas informações são carregadas no proxy reverso pelo método LoadFromMemory. Finalmente, o método MapReverseProxy é usado para mapear e executar o proxy reverso.

Para uso prático, adicione o projeto de proxy reverso como referência no projeto aspire apphost e adicione e modifique as seguintes instruções no arquivo Program.cs:

1dotnet add reference ../ReverseProxy
1var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
2    .WithHttpEndpoint(env: "PORT");
3
4var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
5    .WithReference(echoServer)
6    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
7    .WithExternalHttpEndpoints();

Agora, o proxy reverso pode referenciar o servidor echo. As solicitações externas agora serão recebidas pelo proxy reverso e encaminhadas para o servidor echo.

Modificação do Proxy Reverso

Primeiro, é necessário alterar o endereço de escuta do projeto atribuído ao proxy reverso. Remova o applicationUrl dentro do arquivo Properties/launchSettings.json. Em seguida, abra o arquivo Program.cs e modifique-o extensivamente da seguinte forma:

 1using Yarp.ReverseProxy.Configuration;
 2
 3var builder = WebApplication.CreateBuilder(args);
 4
 5var routes = new List<RouteConfig>
 6{
 7    new RouteConfig
 8    {
 9        ClusterId = "cluster-echo",
10        RouteId = "route-echo",
11        Match = new RouteMatch
12        {
13            Path = "/"
14        }
15    }
16};
17
18var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";
19
20var clusters = new List<ClusterConfig>
21{
22    new ClusterConfig
23    {
24        ClusterId = "cluster-echo",
25        Destinations = new Dictionary<string, DestinationConfig>
26        {
27            { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
28        }
29    }
30};
31
32builder.Services.AddReverseProxy()
33    .LoadFromMemory(routes, clusters);
34
35var app = builder.Build();
36
37app.MapReverseProxy();
38app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");

Primeiro, modificamos as informações de routes e clusters. Adicionamos echo-route e echo-cluster respectivamente para configurar o envio de solicitações ao servidor echo. E modificamos para que o endereço do servidor echo seja lido de uma variável de ambiente.

A regra para este endereço é services__{service-name}__http__{index}. No caso do servidor echo, o nome do serviço é echo-server e, como é uma única instância, o índice 0 é usado. Se você adicionar um servidor ASP.NET Core, várias instâncias podem ser criadas via WithReplica, então o índice deve ser incrementado. O valor http://localhost:8080 que está em tratamento de exceção é um valor "lixo" sem significado.

Agora, execute o projeto e acesse http://localhost:3000/?name=world, e você ainda poderá ver Hello, world sendo exibido.

Ideias de Expansão

Agora que adicionamos um servidor Go ao dotnet aspire e confirmamos a passagem de solicitações através de um proxy reverso. Então, poderemos estender esse processo para implementá-lo de forma programática. Por exemplo, para o servidor echo, podemos adicionar numeração após o nome do serviço para criar várias instâncias e adicionar automaticamente configurações para o proxy reverso.

No arquivo Program.cs do projeto aspire apphost, modifique o código que usa o proxy reverso e o servidor echo da seguinte forma:

 1var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
 2    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
 3    .WithExternalHttpEndpoints();
 4
 5for (var i = 0; i < 8; i++)
 6{
 7    var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
 8        .WithHttpEndpoint(env: "PORT");
 9    reverseProxy.WithReference(echoServer);
10}

E no arquivo Program.cs do projeto de proxy reverso, modifique-o da seguinte forma:

 1var echoServerDestinations = new Dictionary<string, DestinationConfig>();
 2for (var i = 0; i < 8; i++)
 3{
 4    echoServerDestinations[$"destination-{i}"] = new DestinationConfig
 5    {
 6        Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
 7    };
 8}
 9
10var clusters = new List<ClusterConfig>
11{
12    new ClusterConfig
13    {
14        ClusterId = "cluster-echo",
15        Destinations = echoServerDestinations
16    }
17};

Adicionamos configurações de destino para as 8 instâncias de servidor echo aumentadas. Agora, o proxy reverso possui as informações de destino para os servidores echo aumentados e pode encaminhar as solicitações. Se você acessar o http://localhost:3000/?name=world existente, ainda poderá ver Hello, world sendo exibido.

Conclusão

Neste artigo, explicamos como adicionar um servidor Go ao dotnet aspire e como encaminhar solicitações através de um proxy reverso. No entanto, a expansão ainda não foi totalmente escrita, e um exemplo de como implementá-la de forma mais programática através de variáveis de ambiente foi preparado em um repositório separado. Para detalhes sobre a configuração do projeto e o código, consulte snowmerak/AspireStartPack.

Pessoalmente, espero que o dotnet aspire possa desempenhar seu papel como alternativa ao docker compose e como ferramenta de implantação na nuvem. Já existem geradores que criam manifestos docker compose ou k8s, o que, acredito, melhorou a acessibilidade das ferramentas de infraestrutura para o desenvolvedor comum.