GoSuda

Experimentando a execução escalável de um servidor Go no .NET Aspire

By snowmerak
views ...

dotnet aspire?

dotnet aspire é uma ferramenta criada para auxiliar desenvolvedores no desenvolvimento e configuração de ambientes nativos da nuvem, à medida que esses ambientes se tornam cada vez mais comuns. Esta ferramenta permite que desenvolvedores .NET implantem facilmente projetos .NET, diversas infraestruturas nativas da nuvem e outros serviços ou contêineres em outras linguagens.

Naturalmente, o lançamento e a operação vão do docker ao k8s, e muitos setores, indústrias e desenvolvedores estão migrando, e já migraram, de ambientes on-premise existentes para ambientes nativos da nuvem. Agora é um campo maduro. Portanto, não acho necessário explicar os inconvenientes anteriores em relação a nomes de host, configurações de porta, firewalls e gerenciamento de métricas.

Portanto, mesmo com as explicações acima, é provável que você não tenha ideia do que seja o dotnet aspire. Isso ocorre porque a Microsoft não está fornecendo uma definição precisa. Portanto, também não darei uma definição específica. No entanto, como neste artigo usarei as funcionalidades básicas do dotnet aspire que entendi, você pode usá-las como referência para definir sua própria posição.

Configuração do Projeto

Criando um projeto dotnet aspire

Se você não tiver o modelo dotnet aspire, primeiro precisará instalar o modelo. Instale o modelo com o seguinte comando. Se você não tiver o .net, instale-o você mesmo.

1dotnet new install Aspire.ProjectTemplates

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

1dotnet new sln

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

1dotnet new aspire-apphost -o AppHost

Isso cria um projeto aspire-apphost com apenas um código simples para configuração.

Adicionando Valkey

Agora, vamos adicionar o Valkey de forma simples.

Antes de adicionar, dotnet aspire oferece diversas soluções de terceiros por meio do community hosting. Claro, o valkey também pode receber suporte deste community hosting, e pode ser facilmente usado através do seguinte pacote nuget.

1dotnet add package Aspire.Hosting.Valkey

Além disso, ele oferece diversas hospedagens integradas, que podem ser encontradas 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 um serviço valkey.WithDataVolume cria um volume para armazenar dados em cache, e WithPersistence permite que os dados em cache sejam armazenados de forma persistente. Até aqui, parece que está desempenhando um papel semelhante ao volumes do docker-compose. Naturalmente, você também pode criar isso facilmente. No entanto, isso está além do escopo deste artigo, então não falarei sobre isso agora.

Criando um servidor Echo em Go

Agora, vamos adicionar um servidor simples na linguagem Go. Primeiro, crie um espaço de trabalho com go work init na pasta da solução. Para um desenvolvedor .Net, um espaço de trabalho Go é semelhante a uma solução.

Em seguida, crie uma pasta chamada EchoServer, mova-se para ela e execute go mod init EchoServer. Isso cria um módulo Go. Para um desenvolvedor .Net, um módulo é semelhante a um projeto. Em seguida, 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, ao ser executado pelo Aspire AppHost, injeta a variável de ambiente PORT, que ele deve escutar, lê a porta e executa o servidor. Este é um servidor que recebe uma query name e retorna Hello, {name}.

Agora, vamos adicionar este servidor ao dotnet aspire.

Adicionando o servidor Echo ao aspire

Volte para o 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 o seguinte código:

 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 um servidor na linguagem Go. O método AddGolangApp que acabamos de adicionar é um método de extensão de um host personalizado para adicionar um servidor na linguagem Go. Ele usa fixamente a porta 3000 e injeta a variável de ambiente PORT. Finalmente, WithExternalHttpEndpoints permite o acesso externo.

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

No entanto, o dotnet aspire atualmente tem uma grande penalidade para projetos não .NET. Isso é...

Expansão do Projeto

Então, como faço a expansão horizontal?

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

Portanto, você deve implementá-lo diretamente usando um balanceador de carga ou um proxy reverso separado. No entanto, isso pode tornar o proxy reverso um SPOF, por isso é uma boa ideia que o proxy reverso forneça a opção WithReplica. Nesse caso, o proxy reverso deve ser necessariamente um projeto .NET.

Até agora, usei métodos como nginx, trafik ou implementação direta para esse problema, mas se houver uma limitação de um projeto .NET, não tenho uma maneira imediata de fazer isso. Então, procurei um proxy reverso implementado em .NET, e felizmente havia uma opção chamada YARP. YARP é um proxy reverso implementado em .NET, que também pode atuar como um balanceador de carga e oferece várias funcionalidades, por isso decidi que era 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, mova-se para 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 é o código básico para usar o YARP.routes contém informações de roteamento que o proxy reverso irá usar e clusters contém informações de cluster que o proxy reverso irá usar. Essas informações são carregadas no proxy reverso com o método LoadFromMemory. Finalmente, o proxy reverso é mapeado e executado usando o método MapReverseProxy.

Para uso real, adicione uma referência ao projeto do proxy reverso no projeto aspire apphost e adicione e modifique o seguinte código 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. A estrutura foi alterada para que solicitações externas sejam recebidas no proxy reverso e encaminhadas para o servidor echo.

Modificando o proxy reverso

Primeiro, o endereço de escuta do projeto atribuído ao proxy reverso precisa ser alterado. Remova 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, modifique as informações para routes e clusters. Adicione echo-route e echo-cluster, respectivamente, para que as solicitações sejam enviadas para o servidor echo. Em seguida, modifique para que o endereço do servidor echo seja lido da variável de ambiente e usado.

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, 0 é usado como índice. Se você adicionar um servidor asp .net core, várias instâncias podem ser criadas por meio de WithReplica, então você pode usar o índice incrementando-o. O valor lixo http://localhost:8080 que é tratado como uma exceção não tem significado.

Agora, se você executar o projeto e acessar http://localhost:3000/?name=world, você ainda deverá ver Hello, world impresso.

Ideia de expansão

Agora você viu como adicionar um servidor Go ao dotnet aspire e como encaminhar solicitações por meio de um proxy reverso. Então, agora, deve ser possível expandir isso para ser implementado de forma programática. Por exemplo, você pode criar várias instâncias adicionando uma numeração após o nome do serviço para o servidor echo e adicionar automaticamente configurações para o proxy reverso.

Modifique o código para usar o proxy reverso e o servidor echo no arquivo Program.cs do projeto aspire apphost 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}

Em seguida, modifique o arquivo Program.cs do projeto do proxy reverso 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};

Adicione configurações de destino para as 8 instâncias do servidor echo aumentadas. Agora, o proxy reverso tem informações de destino para os servidores echo aumentados e pode encaminhar solicitações. Se você acessar http://localhost:3000/?name=world novamente, você ainda deverá ver Hello, world impresso.

Concluindo

Neste artigo, expliquei o processo de adicionar um servidor Go ao dotnet aspire e encaminhar solicitações por meio de um proxy reverso. No entanto, nem tudo sobre a expansão foi escrito, e criei um exemplo que pode ser implementado de forma mais programática usando variáveis de ambiente em um repositório separado. Para obter informações detalhadas sobre a configuração do projeto e o código, consulte snowmerak/AspireStartPack.

Pessoalmente, espero que o dotnet aspire possa desempenhar seu próprio papel como uma alternativa ao docker compose e como uma ferramenta de implantação na nuvem. Já existe um gerador que cria docker compose ou k8s manifest, e acho que a acessibilidade das ferramentas de infraestrutura para desenvolvedores em geral melhorou.