---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Executando um servidor Go de forma escalável com .NET Aspire
description: 'Como executar servidores Go de forma escalável com o .NET Aspire: exemplo de configuração de escalonamento horizontal e automação com o proxy reverso YARP.'
language: pt
date: 2025-01-05T05:04:22.744778105Z
path: /blog/posts/exploring-the-scalable-execution-of-go-servers-in-net-aspire-z42f3b6ee
category: network
---

## dotnet aspire?

dotnet aspire é uma ferramenta criada para ajudar os desenvolvedores na construção e configuração de aplicações nativas da nuvem, à medida que os ambientes nativos da nuvem se expandem. Esta ferramenta permite que desenvolvedores .NET implantem com facilidade projetos em .NET, diversas infraestruturas nativas da nuvem, além de serviços em outras linguagens ou contêineres.

Naturalmente, desde o docker até o k8s, a tecnologia já está lançada e em operação, e muitos campos, setores e desenvolvedores, a partir dos ambientes locais (on-premises) tradicionais, estão migrando, ou já migraram, para ambientes nativos da nuvem. Hoje, trata-se de um campo maduro. Por isso, acredito que não há necessidade de explicar os incômodos já existentes em relação a nome de host, configuração de portas, firewall, gerenciamento de métricas e outros aspectos.

Assim, mesmo pelas explicações acima, é provável que você ainda não tenha uma ideia clara do que é o dotnet aspire. Isso ocorre porque nem mesmo a Microsoft oferece uma definição precisa. Por isso, também não vou apresentar uma definição própria. Entretanto, neste texto utilizarei as funcionalidades básicas do dotnet aspire conforme eu as compreendi, então, use-as como referência para estabelecer o seu próprio entendimento.

## Configuração do projeto

### Criação de um projeto dotnet aspire

Caso o template do dotnet aspire não esteja disponível, é necessário instalá-lo primeiro. Instale o template com o seguinte comando. Caso não tenha o .NET instalado, instale-o por conta própria.

```bash
dotnet new install Aspire.ProjectTemplates
```

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

```bash
dotnet new sln
```

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

```bash
dotnet new aspire-apphost -o AppHost
```

Será criado um projeto aspire-apphost que contém apenas um código simples para configuração.

### Adicionando o Valkey

Agora, vamos adicionar o Valkey de forma simples.

Antes de adicioná-lo diretamente, vale mencionar que o dotnet aspire oferece diversas soluções de terceiros por meio de community hosting.  
Naturalmente, o valkey também pode receber suporte desse community hosting, e pode ser facilmente utilizado por meio do seguinte pacote NuGet.

```bash
dotnet add package Aspire.Hosting.Valkey
```

Além disso, há diversas outras integrações de hosting disponíveis, que podem ser consultadas [aqui](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview).  
Voltando ao valkey, abra o arquivo Program.cs no projeto AppHost e modifique-o da seguinte forma.

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddValkey("cache")
    .WithDataVolume(isReadOnly: false)
    .WithPersistence(interval: TimeSpan.FromMinutes(5),
        keysChangedThreshold: 100);

builder.Build().Run();
```

`cache` é uma implementação da interface `IResourceBuilder` que contém as informações necessárias para construir o serviço valkey.  
`WithDataVolume` cria um volume para armazenar os dados do cache, e `WithPersistence` permite que os dados do cache sejam armazenados continuamente.  
Até aqui, parece que ele desempenha um papel semelhante ao de `volumes` no `docker-compose`.  
Naturalmente, isso também pode ser feito sem muita dificuldade por vocês.  
Contudo, isso foge do escopo deste texto, então não entrarei nesse assunto agora.

### Criação de um servidor de eco em Go

Agora, vamos adicionar um servidor simples em Go.  
Primeiro, na pasta da solução, crie um workspace por meio de `go work init`.  
Para desenvolvedores .NET, o workspace do Go pode ser entendido como algo semelhante a uma solução.

Em seguida, crie uma pasta chamada EchoServer, entre nela e execute `go mod init EchoServer`.  
Com esse comando, é criado um módulo Go. Para desenvolvedores .NET, o módulo pode ser entendido como algo semelhante a um projeto.  
Depois, crie o arquivo `main.go` e escreva o seguinte.

```go
package main

import (
	"log"
	"net/http"
	"os"
)

func main() {
	addr := os.Getenv("PORT")
	log.Printf("Server started on %s", addr)

	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		name := request.URL.Query().Get("name")
		writer.Write([]byte("Hello, " + name))
	})

	http.ListenAndServe(":"+addr, nil)
}
```

Este servidor lê a porta indicada na variável de ambiente `PORT`, que o Aspire AppHost injeta quando é executado para que o servidor escute nela, e então inicia o servidor.  
Trata-se de um servidor simples que recebe o parâmetro de consulta `name` e retorna `Hello, {name}`.

Agora, vamos adicionar este servidor ao dotnet aspire.

### Adicionando o servidor de eco ao aspire

Volte ao projeto Aspire AppHost, onde o Valkey foi adicionado, e instale o community hosting para Go.

```bash
dotnet add package CommunityToolkit.Aspire.Hosting.Golang
```

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

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddValkey("cache")
    .WithDataVolume(isReadOnly: false)
    .WithPersistence(interval: TimeSpan.FromMinutes(5),
        keysChangedThreshold: 100);

var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
    .WithHttpEndpoint(port: 3000, env: "PORT")
    .WithExternalHttpEndpoints();

builder.Build().Run();
```

Aqui, `echoServer` é uma implementação da interface `IResourceBuilder` que contém as informações necessárias para construir o servidor em Go.  
O método `AddGolangApp`, que acabamos de adicionar, é um método de extensão do host personalizado para incluir um servidor em Go.  
Ele utiliza a porta 3000 de forma fixa e injeta a variável de ambiente `PORT`.  
Por fim, `WithExternalHttpEndpoints` permite que o serviço seja acessado externamente.

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

No entanto, atualmente o dotnet aspire impõe uma pesada penalidade a projetos que não são .NET.  
Essa penalidade é justamente...

## Extensão do projeto

### Então, como fazer a escalabilidade horizontal?

Atualmente, o dotnet aspire oferece a opção `WithReplica` apenas para o builder de projetos .NET adicionados pelo método `AddProject`.  
Porém, essa opção não é fornecida para hosts em Go nem para projetos externos como os adicionados por `AddContainer`.

Por isso, é necessário implementar manualmente, utilizando um load balancer ou reverse proxy separado.  
Contudo, como esse reverse proxy poderia se tornar um SPOF, o ideal é que ele também ofereça a opção `WithReplica`.  
Nesse caso, o reverse proxy necessariamente precisaria ser um projeto .NET.

Até agora, para lidar com esse problema, utilizei abordagens como nginx, traefik e implementação direta, mas, diante da restrição de ser um projeto .NET, não tive, no momento, nenhuma solução viável.  
Assim, procurei um reverse proxy implementado em .NET e, felizmente, encontrei uma opção: o [YARP](https://microsoft.github.io/reverse-proxy/).  
O YARP é um reverse proxy implementado em .NET que pode atuar também como load balancer e oferece diversos recursos, por isso considerei uma boa escolha.

Agora, vamos adicionar o YARP.

### Configuração do reverse proxy com YARP

Primeiro, crie um projeto para utilizar o YARP.

```bash
dotnet new web -n ReverseProxy
```

Em seguida, acesse o projeto e instale o YARP.

```bash
dotnet add package Yarp.ReverseProxy --version 2.2.0
```

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

```csharp
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

var routes = new List<RouteConfig>();
var clusters = new List<ClusterConfig>();

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

var app = builder.Build();

app.MapReverseProxy();
app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");
```

Este código é a configuração básica para utilizar o YARP.  
`routes` contém as informações de rota que o reverse proxy utilizará, e `clusters` contém as informações de cluster que o reverse proxy utilizará.  
Essas informações são carregadas no reverse proxy pelo método `LoadFromMemory`.  
Por fim, o método `MapReverseProxy` mapeia e executa o reverse proxy.

Além disso, para o uso real, adicione o projeto do reverse proxy como referência no projeto aspire apphost e inclua ou modifique o seguinte trecho no arquivo Program.cs.

```bash
dotnet add reference ../ReverseProxy
```

```csharp
var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
    .WithHttpEndpoint(env: "PORT");

var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
    .WithReference(echoServer)
    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
    .WithExternalHttpEndpoints();
```

Agora, o reverse proxy pode referenciar o servidor de eco.  
A estrutura está sendo alterada para que as requisições externas sejam recebidas pelo reverse proxy e encaminhadas ao servidor de eco.

### Modificação do reverse proxy

Primeiro, é preciso alterar o endereço de escuta do projeto atribuído ao reverse proxy.  
Remova o `applicationUrl` no arquivo `Properties/launchSettings.json`.  
Depois, abra o arquivo Program.cs e modifique-o amplamente como abaixo.

```csharp
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

var routes = new List<RouteConfig>
{
    new RouteConfig
    {
        ClusterId = "cluster-echo",
        RouteId = "route-echo",
        Match = new RouteMatch
        {
            Path = "/"
        }
    }
};

var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";

var clusters = new List<ClusterConfig>
{
    new ClusterConfig
    {
        ClusterId = "cluster-echo",
        Destinations = new Dictionary<string, DestinationConfig>
        {
            { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
        }
    }
};

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

var app = builder.Build();

app.MapReverseProxy();
app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");
```

Primeiro, modifique as informações de `routes` e `clusters`.  
Adicione, respectivamente, `echo-route` e `echo-cluster`, configurando-os para encaminhar requisições ao servidor de eco.  
Além disso, altere para que o endereço do servidor de eco seja lido a partir de uma variável de ambiente.

A regra desse endereço é `services__{service-name}__http__{index}`.  
No caso do servidor de eco, o nome do serviço é `echo-server` e, como se trata de uma instância única, utiliza-se `0` como índice.  
Caso um servidor ASP.NET Core seja adicionado, múltiplas instâncias poderão ser criadas por meio de `WithReplica`, bastando incrementar o índice para utilizá-las.  
O endereço `http://localhost:8080`, usado como tratamento de exceção, é um valor sem nenhum significado.

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

### Ideias de extensão

Agora, verificamos que é possível adicionar um servidor Go ao dotnet aspire e encaminhar requisições por meio de um reverse proxy.  
A partir daí, é possível expandir o processo para implementá-lo de forma programática.  
Por exemplo, pode-se adicionar uma numeração ao nome do serviço do servidor de eco para criar múltiplas instâncias e configurar automaticamente o reverse proxy.

Modifique o arquivo Program.cs do projeto aspire apphost, que utiliza o reverse proxy e o servidor de eco, da seguinte forma.

```csharp
var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
    .WithExternalHttpEndpoints();

for (var i = 0; i < 8; i++)
{
    var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
        .WithHttpEndpoint(env: "PORT");
    reverseProxy.WithReference(echoServer);
}
```

Em seguida, modifique o arquivo Program.cs do projeto do reverse proxy da seguinte maneira.

```csharp
var echoServerDestinations = new Dictionary<string, DestinationConfig>();
for (var i = 0; i < 8; i++)
{
    echoServerDestinations[$"destination-{i}"] = new DestinationConfig
    {
        Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
    };
}

var clusters = new List<ClusterConfig>
{
    new ClusterConfig
    {
        ClusterId = "cluster-echo",
        Destinations = echoServerDestinations
    }
};
```

Adicione as configurações de destino para as 8 instâncias do servidor de eco.  
Agora, o reverse proxy possui as informações de destino de todos os servidores de eco ampliados e pode encaminhar as requisições.  
Ao acessar `http://localhost:3000/?name=world`, como antes, você verá `Hello, world` sendo exibido.

## Considerações finais

Neste texto, foi descrito o processo de adicionar um servidor Go ao dotnet aspire e encaminhar requisições por meio de um reverse proxy.  
Contudo, a parte de extensão ainda não foi totalmente escrita, e preparei, em um repositório separado, um exemplo que permite implementar de forma mais programática por meio de variáveis de ambiente.  
Para a estrutura completa do projeto e o código, consulte [snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack).

Pessoalmente, espero que o dotnet aspire possa desempenhar um papel próprio como alternativa ao docker compose e como ferramenta de implantação em nuvem.  
Já existem [geradores](https://prom3theu5.github.io/aspirational-manifests/generate-command.html) que criam manifests do docker compose ou do k8s, e acredito que isso tornou as ferramentas de infraestrutura mais acessíveis para desenvolvedores comuns.
