Att köra en skalbar Go-server med .NET Aspire

dotnet aspire?
dotnet aspire är ett verktyg som skapades för att hjälpa utvecklare med utveckling och konfiguration av molnnativa lösningar, i takt med att molnnativa miljöer blir allt vanligare. Verktyget gör det möjligt för .NET-utvecklare att enkelt distribuera .NET-projekt, olika molnnativa infrastrukturer samt tjänster på andra språk, containrar och liknande.
Naturligtvis lanseras och drivs allt från Docker till K8s, och många områden, branscher och utvecklare flyttar från traditionella lokala (on-premises) miljöer till molnnativa miljöer, och har redan gjort det. Detta är numera ett moget område. Därför anser jag att det inte behövs någon förklaring av de tidigare besvärligheterna med värdnamn, portkonfiguration, brandväggar, mätvärdeshantering och liknande.
Utifrån ovanstående beskrivningar kommer du troligen inte alls att kunna föreställa dig vad dotnet aspire är. Detta beror på att inte ens Microsoft ger en exakt definition. Därför kommer inte heller jag att ge någon egen definition. Jag kommer dock att använda de grundläggande funktioner i dotnet aspire som jag själv har förstått i denna artikel, så du kan utgå från dem när du bildar din egen uppfattning.
Projektkonfiguration
Skapa ett dotnet aspire-projekt
Om mallen för dotnet aspire inte finns tillgänglig måste du först installera mallen. Installera mallen med följande kommando. Om .NET inte finns installerat får du installera det själv.
1dotnet new install Aspire.ProjectTemplates
Skapa sedan en ny lösning i en lämplig mapp.
1dotnet new sln
Kör därefter följande kommando i lösningsmappen för att skapa ett projekt med mallen aspire-apphost.
1dotnet new aspire-apphost -o AppHost
Då skapas ett aspire-apphost-projekt som endast innehåller en enkel kod för konfiguration.
Lägga till Valkey
Nu ska vi snabbt lägga till Valkey.
Innan vi lägger till det, bör du veta att dotnet aspire via community hosting tillhandahåller många olika tredjepartslösningar.
Naturligtvis kan Valkey också dra nytta av detta communitystöd, och det är enkelt att använda via följande NuGet-paket.
1dotnet add package Aspire.Hosting.Valkey
Det finns även många andra integrerade hostingalternativ, som du kan se här.
Låt oss återgå till Valkey: öppna filen Program.cs i AppHost-projektet och ändra den enligt följande.
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 är en implementation av gränssnittet IResourceBuilder som innehåller information för att bygga en Valkey-tjänst.WithDataVolume skapar en volym för att lagra cachedata, och WithPersistence gör det möjligt att kontinuerligt spara cachedata.
Utifrån detta verkar det utföra en uppgift liknande volumes i docker-compose.
Naturligtvis kan ni också skapa detta utan större svårigheter.
Men eftersom detta ligger utanför ramen för denna artikel tar vi inte upp det här.
Skapa en Go-ekoserver
Låt oss nu lägga till en enkel Go-server.
Skapa först ett Go-workspace i lösningsmappen med go work init.
För .NET-utvecklare kan man se Go-workspace som något liknande en lösning.
Skapa sedan en mapp med namnet EchoServer, gå in i den och kör go mod init EchoServer.
Med detta kommando skapas en Go-modul. För .NET-utvecklare kan man se en modul som något liknande ett projekt.
Skapa sedan filen main.go och skriv följande.
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}
Denna server läser porten från miljövariabeln PORT, som Aspire AppHost injicerar när den körs, och startar servern på den porten.
Den är en enkel server som tar emot frågeparametern name och returnerar Hello, {name}.
Låt oss nu lägga till denna server i dotnet aspire.
Lägga till ekoservern i Aspire
Gå tillbaka till Aspire AppHost-projektet där vi lade till Valkey och lägg till community hosting för Go.
1dotnet add package CommunityToolkit.Aspire.Hosting.Golang
Öppna sedan filen Program.cs och lägg till följande rader.
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();
Här är echoServer en implementation av gränssnittet IResourceBuilder som innehåller information för att bygga en Go-server.
Den nyss tillagda metoden AddGolangApp är en tilläggsmetod för en anpassad host som lägger till en Go-server.
Du kan se att den använder port 3000 fast och injicerar miljövariabeln PORT.
Slutligen gör WithExternalHttpEndpoints att servern kan nås utifrån.
För att testa, gå till http://localhost:3000/?name=world, så kommer du att se att Hello, world skrivs ut.
Men för närvarande finns det en tung nackdel för projekt som inte är .NET-projekt i dotnet aspire.
Den nackdelen är just...
Utöka projektet
Hur gör man då horisontell skalning?
För närvarande tillhandahåller dotnet aspire alternativet WithReplica endast för builders för .NET-projekt som lagts till med metoden AddProject.
För Go-värdar eller externa projekt som läggs till med AddContainer erbjuds dock inte detta alternativ.
Därför måste man implementera det själv med en separat load balancer eller reverse proxy.
Men om reverse proxy:n i sig blir en SPOF kan det bli ett problem, så det är bra om reverse proxy:n i sin tur kan använda alternativet WithReplica.
Då måste reverse proxy:n nödvändigtvis vara ett .NET-projekt.
Hittills har jag använt metoder som nginx, Traefik och egen implementation för att lösa detta, men när det finns begränsningen att det måste vara ett .NET-projekt fanns det inget direkt alternativ för mig.
Därför sökte jag efter en reverse proxy implementerad i .NET, och lyckligtvis fanns YARP som ett alternativ.
YARP är en reverse proxy implementerad i .NET, som även kan fungera som load balancer och erbjuder många funktioner, så jag bedömde att det var ett bra val.
Låt oss nu lägga till YARP.
Konfigurera reverse proxy med YARP
Skapa först ett projekt för att använda YARP.
1dotnet new web -n ReverseProxy
Gå sedan till projektet och installera YARP.
1dotnet add package Yarp.ReverseProxy --version 2.2.0
När installationen är klar, öppna filen Program.cs och skriv följande.
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"}");
Denna kod är grundläggande kod för att använda YARP.routes innehåller ruttinformation som reverse proxy:n använder, och clusters innehåller klusterinformation som reverse proxy:n använder.
Denna information laddas in i reverse proxy:n med metoden LoadFromMemory.
Slutligen använder vi metoden MapReverseProxy för att mappa och köra reverse proxy:n.
För faktisk användning ska vi sedan lägga till reverse proxy-projektet som referens i Aspire AppHost-projektet och lägga till eller ändra följande rader i 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();
Nu kan reverse proxy:n referera till ekoservern.
Inkommande externa förfrågningar tas emot av reverse proxy:n och vidarebefordras till ekoservern, och strukturen har ändrats till detta.
Ändra reverse proxy
Först måste vi ändra lyssnaradressen för projektet som används som reverse proxy.
Ta bort applicationUrl i filen Properties/launchSettings.json.
Öppna sedan filen Program.cs och ändra den omfattande enligt nedan.
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"}");
Först ändrar vi informationen om routes och clusters.
Vi lägger till echo-route respektive echo-cluster och konfigurerar dem så att förfrågningar skickas till ekoservern.
Dessutom ändrar vi koden så att ekoserverns adress läses från miljövariabler och används.
Regeln för denna adress är services__{service-name}__http__{index}.
För ekoservern är tjänstenamnet echo-server och eftersom det är en enda instans använder vi 0 som index.
Om vi lägger till en ASP.NET Core-server kan flera instanser skapas via WithReplica, och då ökar vi bara indexet.
Det undantagshanterade värdet http://localhost:8080 är ett meningslöst skräpvärde.
Kör nu projektet och gå till http://localhost:3000/?name=world, så kommer du fortfarande att se att Hello, world skrivs ut.
Idéer för utökning
Nu har vi bekräftat att en Go-server lagts till i dotnet aspire och att förfrågningar vidarebefordras via reverse proxy:n.
Då kan vi utöka detta så att processen kan implementeras programmatiskt.
Till exempel kan vi skapa flera instanser genom att lägga till nummer efter tjänstenamnet för ekoservern, och automatiskt lägga till konfigurationer för reverse proxy:n.
Ändra koden som använder reverse proxy och ekoservern i filen Program.cs i Aspire AppHost-projektet enligt följande.
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}
Ändra sedan filen Program.cs i reverse proxy-projektet enligt följande.
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};
Vi lägger till målkonfigurationer för de 8 ekoserverinstanserna.
Nu kan reverse proxy:n använda målinformationen för de utökade ekoservrarna och vidarebefordra förfrågningar till dem.
Om du går till det tidigare http://localhost:3000/?name=world kommer du fortfarande att se att Hello, world skrivs ut.
Avslutning
I denna artikel har vi beskrivit hur man lägger till en Go-server i dotnet aspire och vidarebefordrar förfrågningar via en reverse proxy.
Vi har dock ännu inte skrivit klart allt om utökningen, men vi har lagt en separat repository med ett exempel där man kan implementera det mer programmatiskt via miljövariabler.
För detaljerad projektstruktur och kod, se snowmerak/AspireStartPack.
Personligen förväntar jag mig att dotnet aspire kan spela sin egen roll både som ett alternativ till docker compose och som ett verktyg för molndistribution.
Eftersom det redan finns generatorer som skapar docker compose- eller k8s-manifest, tror jag att tillgängligheten till infrastrukturverktyg har blivit bättre för vanliga utvecklare.