---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Esecuzione di un server Go scalabile con .NET Aspire
description: 'Come eseguire server Go in modo scalabile con .NET Aspire: esempio di configurazione per la scalabilità orizzontale e l''automazione con il reverse proxy YARP.'
language: it
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 è uno strumento creato per assistere gli sviluppatori nello sviluppo e nella configurazione di applicazioni cloud native, man mano che gli ambienti cloud native si diffondono. Questo strumento consente agli sviluppatori .NET di distribuire facilmente progetti .NET, varie infrastrutture cloud native, nonché servizi o container scritti in altri linguaggi.

Naturalmente, dal docker fino a k8s, questi ambienti vengono rilasciati e gestiti, e molti settori, industrie e sviluppatori stanno migrando dagli ambienti on-premise tradizionali verso ambienti cloud native, e in molti casi la migrazione è già completata. Ormai si tratta di un campo maturo. Pertanto, ritengo non sia necessario spiegare le difficoltà già note relative alla gestione dei nomi host, alla configurazione delle porte, ai firewall, alla gestione delle metriche e simili.

Quindi, anche solo alla luce delle spiegazioni precedenti, sarà probabilmente difficile farsi un'idea di cosa sia dotnet aspire. Il motivo è che nemmeno Microsoft fornisce una definizione precisa di questo strumento. Pertanto, anch'io non fornirò una definizione particolare. Tuttavia, in questo articolo utilizzerò le funzionalità di base di dotnet aspire così come le ho comprese, quindi potete farvi riferimento per definire la vostra posizione personale.

## Configurazione del progetto

### Creazione di un progetto dotnet aspire

Se il template di dotnet aspire non è disponibile, è necessario installarlo innanzitutto. Installate il template con il seguente comando. Se .NET non è installato, provvedete voi stessi a installarlo.

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

Poi create una nuova soluzione in una cartella appropriata.

```bash
dotnet new sln
```

Successivamente, eseguite il seguente comando dalla cartella della soluzione per creare un progetto basato sul template aspire-apphost.

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

Verrà così creato un progetto aspire-apphost contenente solo un codice semplice per la configurazione.

### Aggiunta di Valkey

Ora aggiungiamo rapidamente Valkey.

Prima di procedere, occorre sapere che dotnet aspire fornisce diverse soluzioni di terze parti tramite il community hosting.
Naturalmente, anche Valkey può beneficiare di questo supporto di community hosting, ed è facilmente utilizzabile tramite il seguente pacchetto NuGet.

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

Sono disponibili anche altre integrazioni di hosting, che potete consultare [qui](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview).
Tornando a Valkey, aprite il file Program.cs nel progetto AppHost e modificatelo come segue.

```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` è un'implementazione dell'interfaccia `IResourceBuilder` che contiene le informazioni necessarie per costruire il servizio Valkey.
`WithDataVolume` crea un volume in cui archiviare i dati della cache, mentre `WithPersistence` consente di salvare in modo persistente i dati della cache.
Da quanto visto finora, sembra svolgere un ruolo simile a `volumes` di `docker-compose`.
Naturalmente, anche voi potete realizzarlo senza difficoltà.
Tuttavia, ciò esula dallo scopo di questo articolo, quindi non ne parlerò ora.

### Creazione di un server echo in Go

Ora aggiungiamo un semplice server in linguaggio Go.
Innanzitutto, nella cartella della soluzione creiamo un workspace tramite `go work init`.
Per gli sviluppatori .NET, il workspace di Go può essere considerato simile a una soluzione.

Poi create una cartella chiamata EchoServer, spostatevi al suo interno ed eseguite `go mod init EchoServer`.
Questo comando crea un modulo Go. Per gli sviluppatori .NET, il modulo è assimilabile a un progetto.
Quindi create il file `main.go` e scrivetelo come segue.

```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)
}
```

Questo server, quando viene eseguito da Aspire AppHost, legge la porta sulla quale deve restare in ascolto dalla variabile d'ambiente `PORT` iniettata e avvia il server su tale porta.
Si tratta di un server che riceve il parametro di query `name` e restituisce `Hello, {name}`.

Ora aggiungiamo questo server a dotnet aspire.

### Aggiunta del server echo ad Aspire

Tornate al progetto Aspire AppHost in cui avete aggiunto Valkey e installate il community hosting per Go.

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

Poi aprite il file Program.cs e aggiungete la seguente istruzione.

```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();
```

Qui `echoServer` è un'implementazione dell'interfaccia `IResourceBuilder` che contiene le informazioni necessarie per costruire il server Go.
Il metodo `AddGolangApp`, appena aggiunto, è un metodo di estensione dell'host personalizzato per aggiungere un server in linguaggio Go.
Viene utilizzata in modo fisso la porta 3000, e si può notare che viene iniettata la variabile d'ambiente `PORT`.
Infine, `WithExternalHttpEndpoints` consente l'accesso dall'esterno.

Per verificare, provate ad accedere a `http://localhost:3000/?name=world`: dovreste vedere comparire `Hello, world`.

Tuttavia, l'attuale dotnet aspire comporta una pesante penalizzazione per i progetti non-dotnet.
Si tratta proprio di...

## Estensione del progetto

### Come si gestisce allora la scalabilità orizzontale?

Attualmente dotnet aspire fornisce l'opzione `WithReplica` solo per il builder dei progetti .NET aggiunti tramite il metodo `AddProject`.
Tuttavia, non la fornisce per gli host in linguaggio Go né per progetti esterni come quelli aggiunti tramite `AddContainer`.

Di conseguenza, occorre implementarla direttamente utilizzando un load balancer o un reverse proxy separato.
Tuttavia, in questo modo il reverse proxy potrebbe diventare un SPOF, quindi è opportuno che il reverse proxy stesso fornisca l'opzione `WithReplica`.
Ne consegue inevitabilmente che il reverse proxy debba essere un progetto .NET.

Finora, per affrontare questo problema, avevo utilizzato nginx, traefik, un'implementazione diretta e altri approcci, ma con il vincolo di dover usare un progetto .NET non avevo, al momento, soluzioni a portata di mano.
Pertanto ho cercato un reverse proxy implementato in .NET e, per fortuna, esisteva l'opzione [YARP](https://microsoft.github.io/reverse-proxy/).
YARP è un reverse proxy implementato in .NET che può svolgere anche la funzione di load balancer e offre numerose funzionalità, quindi ho ritenuto che fosse una buona scelta.

Ora aggiungiamo YARP.

### Configurazione di un reverse proxy con YARP

Innanzitutto, creiamo un progetto per utilizzare YARP.

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

Poi spostatevi nel progetto e installate YARP.

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

Al termine dell'installazione, aprite il file Program.cs e scrivete quanto segue.

```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"}");
```

Questo codice è la configurazione di base per utilizzare YARP.
`routes` contiene le informazioni sulle route che il reverse proxy utilizzerà, mentre `clusters` contiene le informazioni sui cluster che il reverse proxy utilizzerà.
Queste informazioni vengono caricate nel reverse proxy tramite il metodo `LoadFromMemory`.
Infine, il metodo `MapReverseProxy` consente di mappare ed eseguire il reverse proxy.

Per l'uso effettivo, nel progetto aspire apphost aggiungete un riferimento al progetto reverse proxy e modificate o aggiungete al file Program.cs la seguente istruzione.

```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();
```

Ora il reverse proxy può fare riferimento al server echo.
La struttura è stata modificata in modo che le richieste esterne vengano ricevute dal reverse proxy e poi inoltrate al server echo.

### Modifica del reverse proxy

Anzitutto occorre modificare l'indirizzo di ascolto del progetto assegnato al reverse proxy.
Rimuovete `applicationUrl` dal file `Properties/launchSettings.json`.
Quindi aprite il file Program.cs e modificatelo in modo sostanziale come segue.

```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"}");
```

Innanzitutto modifichiamo le informazioni relative a `routes` e `clusters`.
Aggiungiamo rispettivamente `echo-route` e `echo-cluster`, configurandoli in modo che le richieste vengano inviate al server echo.
Inoltre, modifichiamo il codice in modo che l'indirizzo del server echo venga letto dalle variabili d'ambiente e utilizzato.

La regola per questo indirizzo è `services__{service-name}__http__{index}`.
Nel caso del server echo, il nome del servizio è `echo-server` e, trattandosi di una singola istanza, si utilizza l'indice `0`.
Se si aggiunge un server ASP.NET Core, più istanze possono essere create tramite `WithReplica`, quindi è sufficiente incrementare l'indice per utilizzarle.
L'indirizzo `http://localhost:8080`, usato come gestione delle eccezioni, è un valore privo di significato.

Ora eseguite il progetto e accedete a `http://localhost:3000/?name=world`: dovreste vedere comparire ancora `Hello, world`.

### Idee per l'estensione

Abbiamo ora verificato che è possibile aggiungere un server Go a dotnet aspire e inoltrarvi le richieste tramite un reverse proxy.
A questo punto, il processo può essere esteso in modo da essere implementato in modo programmatico.
Ad esempio, si potrebbe aggiungere una numerazione al nome del servizio del server echo per creare più istanze e aggiungere automaticamente la configurazione al reverse proxy.

Modificate il file Program.cs del progetto aspire apphost, che utilizza il reverse proxy e il server echo, come segue.

```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);
}
```

Poi modificate il file Program.cs del progetto reverse proxy come segue.

```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
    }
};
```

Aggiungiamo la configurazione delle destinazioni per le otto istanze del server echo.
Ora il reverse proxy dispone delle informazioni sulle destinazioni di tutti i server echo aggiunti e può inoltrare le richieste.
Accedendo come prima a `http://localhost:3000/?name=world`, dovreste vedere comparire ancora `Hello, world`.

## Conclusioni

In questo articolo abbiamo illustrato come aggiungere un server Go a dotnet aspire e come inoltrare le richieste tramite un reverse proxy.
Tuttavia, non ho ancora scritto per intero la parte relativa all'estensione; ho invece preparato in un repository separato un esempio che consente di implementarla in modo più programmatico tramite le variabili d'ambiente.
Per la struttura dettagliata del progetto e il codice, consultate [snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack).

Personalmente ritengo che dotnet aspire possa svolgere un ruolo proprio come alternativa a docker compose e come strumento di distribuzione cloud. Esistono già [generatori](https://prom3theu5.github.io/aspirational-manifests/generate-command.html) in grado di produrre docker compose o manifest k8s, e penso che ciò abbia reso più accessibili gli strumenti infrastrutturali anche per gli sviluppatori generici.
