Eseguire un server Go scalabile su dotnet aspire
dotnet aspire?
dotnet aspire è uno strumento creato per aiutare gli sviluppatori nello sviluppo e nella configurazione di applicazioni cloud-native, in concomitanza con la crescente diffusione di ambienti cloud-native. Questo strumento consente agli sviluppatori .NET di distribuire facilmente progetti .NET, diverse infrastrutture cloud-native e servizi o container scritti in altri linguaggi.
Naturalmente, viene rilasciato e gestito da docker a k8s, e un numero considerevole di settori, industrie e sviluppatori si stanno spostando, o si sono già spostati, verso ambienti cloud-native provenienti da ambienti on-premise tradizionali. Ormai è un settore maturo. Pertanto, ritengo superfluo spiegare gli inconvenienti tradizionali relativi a nomi host, configurazione delle porte, firewall e gestione delle metriche.
Pertanto, anche solo in base alle spiegazioni di cui sopra, probabilmente non avrete la minima idea di cosa sia dotnet aspire. Questo perché Microsoft stessa non fornisce una definizione precisa. Quindi, nemmeno io ne darò una. Tuttavia, in questo articolo utilizzerò le funzionalità di base di dotnet aspire così come le ho comprese, quindi potrete far riferimento a questo per definire la vostra posizione.
Struttura del progetto
Creazione di un progetto dotnet aspire
Se non avete il template dotnet aspire, dovete prima installarlo. Utilizzate il seguente comando per installare il template. Se non avete .NET, installatelo voi stessi.
1dotnet new install Aspire.ProjectTemplates
Quindi, create una nuova soluzione in una cartella appropriata.
1dotnet new sln
Successivamente, nella cartella della soluzione, eseguite il seguente comando per creare il progetto dal template aspire-apphost.
1dotnet new aspire-apphost -o AppHost
Verà creato un progetto aspire-apphost contenente solo un semplice codice per la configurazione.
Aggiunta di Valkey
Aggiungiamo semplicemente Valkey.
Prima di procedere all'aggiunta, dotnet aspire fornisce diverse soluzioni di terze parti tramite community hosting. Naturalmente, anche valkey può avvalersi di questo supporto di community hosting ed è facilmente utilizzabile tramite il seguente pacchetto NuGet.
1dotnet add package Aspire.Hosting.Valkey
Vengono offerti diversi hosting integrati, verificabili a questo indirizzo. Tornando a valkey, aprite il file Program.cs nel progetto AppHost e modificatelo come segue.
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
è un'implementazione dell'interfaccia IResourceBuilder
che contiene le informazioni per costruire il servizio valkey.WithDataVolume
crea un volume per memorizzare i dati della cache, mentre WithPersistence
consente di salvare i dati della cache in modo persistente.
A questo punto, sembra svolgere una funzione simile a volumes
in docker-compose
.
Naturalmente, anche voi potrete crearne uno senza difficoltà.
Tuttavia, questo esula dallo scopo di questo articolo, quindi non ne parlerò ora.
Creazione di un echo server in linguaggio Go
Aggiungiamo un semplice server in linguaggio Go.
Innanzitutto, create uno spazio di lavoro utilizzando go work init
nella cartella della soluzione.
Per gli sviluppatori .NET, uno spazio di lavoro Go è simile a una soluzione.
Quindi, create una cartella denominata EchoServer e, dopo esservi spostati al suo interno, eseguite go mod init EchoServer
.
Questo comando crea un modulo Go. Un modulo può essere considerato analogo a un progetto per gli sviluppatori .NET.
Quindi, create il file main.go
e scrivete quanto segue.
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}
Questo server, quando viene eseguito Aspire AppHost, legge la variabile di ambiente PORT
che specifica la porta su cui deve eseguire il listening e avvia il server su quella porta.
È un semplice server che riceve una query name
e restituisce Hello, {name}
.
Ora aggiungiamo questo server a dotnet aspire.
Aggiunta dell'echo server ad aspire
Tornate al progetto Aspire AppHost dove avete aggiunto Valkey e aggiungete il community hosting per il linguaggio Go.
1dotnet add package CommunityToolkit.Aspire.Hosting.Golang
Quindi, aprite il file Program.cs e aggiungete il seguente codice.
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();
Qui, echoServer
è un'implementazione dell'interfaccia IResourceBuilder
che contiene le informazioni per costruire il server in linguaggio Go.
Il metodo AddGolangApp
appena aggiunto è un metodo di estensione di un host personalizzato per aggiungere un server in linguaggio Go.
Si utilizza la porta 3000 in modo fisso e si verifica l'iniezione della variabile di ambiente PORT
.
Infine, WithExternalHttpEndpoints
consente l'accesso esterno.
Per effettuare un test, accedete a http://localhost:3000/?name=world
e dovreste vedere visualizzato Hello, world
.
Tuttavia, attualmente dotnet aspire presenta una pesante penalità per i progetti non .NET. Cioè...
Estensione del progetto
E allora, come si fa lo scaling orizzontale?
Attualmente, dotnet aspire fornisce l'opzione WithReplica
solo per il builder dei progetti .NET aggiunti con il metodo AddProject
.
Tuttavia, non fornisce questa opzione per progetti esterni come l'host in linguaggio Go o AddContainer
.
Pertanto, è necessario implementare direttamente un load balancer o un reverse proxy separato.
Tuttavia, ciò potrebbe rendere tale reverse proxy un singolo punto di errore (SPOF), quindi sarebbe opportuno che il reverse proxy fornisse l'opzione WithReplica
.
Di conseguenza, il reverse proxy dovrebbe necessariamente essere un progetto .NET.
Fino ad ora ho utilizzato metodi come nginx, trafik e implementazioni dirette per questo problema, ma la restrizione di dover utilizzare un progetto .NET mi ha bloccato. Ho quindi cercato un reverse proxy implementato in .NET e fortunatamente ho trovato YARP. YARP è un reverse proxy implementato in .NET, in grado di fungere anche da load balancer e di offrire diverse funzionalità, quindi l'ho considerato una buona scelta.
Ora aggiungiamo YARP.
Configurazione del reverse proxy con YARP
Innanzitutto, create un progetto per utilizzare YARP.
1dotnet new web -n ReverseProxy
Quindi, spostatevi nel progetto e installate YARP.
1dotnet add package Yarp.ReverseProxy --version 2.2.0
Al termine dell'installazione, aprite il file Program.cs e scrivete quanto segue.
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"}");
Questo codice è il codice di base per utilizzare YARP.routes
contiene le informazioni sul routing da utilizzare per il reverse proxy, mentre clusters
contiene le informazioni sui cluster da utilizzare per il reverse proxy.
Queste informazioni vengono caricate nel reverse proxy tramite il metodo LoadFromMemory
.
Infine, il reverse proxy viene mappato ed eseguito utilizzando il metodo MapReverseProxy
.
Per l'utilizzo effettivo, aggiungete il progetto reverse proxy come riferimento nel progetto aspire apphost e aggiungete e modificate il seguente codice nel file 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();
Ora il reverse proxy può fare riferimento all'echo server. La struttura è cambiata in modo che le richieste in arrivo dall'esterno vengano ricevute dal reverse proxy e inoltrate all'echo server.
Modifica del reverse proxy
Innanzitutto, è necessario 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 ampiamente come segue.
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"}");
Innanzitutto, modificate le informazioni su routes
e clusters
.
Aggiungete echo-route
e echo-cluster
per inviare le richieste all'echo server.
Quindi, modificate in modo da utilizzare l'indirizzo dell'echo server letto dalla variabile di ambiente.
La regola per questo indirizzo è services__{service-name}__http__{index}
.
Nel caso dell'echo server, il nome del servizio è echo-server
e, poiché si tratta di una singola istanza, viene utilizzato 0
come indice.
Se aggiungete un server asp .net core, potrete creare più istanze tramite WithReplica
, quindi potrete aumentare l'indice per utilizzarlo.
Il valore http://localhost:8080
gestito come eccezione è un valore di spazzatura senza alcun significato.
Ora eseguite il progetto e accedete a http://localhost:3000/?name=world
. Dovreste comunque vedere visualizzato Hello, world
.
Idee di estensione
Ora abbiamo verificato l'aggiunta di un server Go a dotnet aspire e l'inoltro delle richieste tramite un reverse proxy. Possiamo quindi estendere questo processo per consentirne un'implementazione programmatica. Ad esempio, possiamo aggiungere una numerazione dopo il nome del servizio per l'echo server per creare più istanze e aggiungere automaticamente la configurazione per il reverse proxy.
Modificate il codice che utilizza il reverse proxy e l'echo server nel file Program.cs del progetto aspire apphost come segue.
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}
Quindi, modificate il file Program.cs del progetto reverse proxy come segue.
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};
Aggiungete la configurazione della destinazione per le 8 istanze del server echo aumentate.
Ora il reverse proxy dispone delle informazioni sulla destinazione per i server echo aumentati e può inoltrare le richieste.
Accedendo a http://localhost:3000/?name=world
, dovreste comunque vedere visualizzato Hello, world
.
Conclusioni
In questo articolo, abbiamo illustrato il processo di aggiunta di un server Go a dotnet aspire e l'inoltro delle richieste tramite un reverse proxy. Tuttavia, non ho ancora completato l'estensione e ho creato un esempio separato nel repository per un'implementazione più programmatica tramite variabili di ambiente. Per informazioni dettagliate sulla struttura del progetto e sul codice, fare riferimento a snowmerak/AspireStartPack.
Personalmente, mi aspetto che dotnet aspire svolga il proprio ruolo come alternativa a docker compose e come strumento di distribuzione cloud. Esiste già un generatore che genera docker compose o manifest k8s, quindi ritengo che l'accessibilità agli strumenti di infrastruttura per gli sviluppatori generali sia migliorata.