Een schaalbare Go-server uitvoeren in .NET Aspire

dotnet aspire?
dotnet aspire is een hulpmiddel dat is ontwikkeld om ontwikkelaars te ondersteunen bij de ontwikkeling en configuratie van cloud-native toepassingen, nu cloud-native omgevingen steeds vaker voorkomen. Met dit hulpmiddel kunnen .NET-ontwikkelaars eenvoudig .NET-projecten, diverse cloud-native infrastructuur, en services of containers in andere talen implementeren.
Uiteraard wordt alles van Docker tot k8s uitgerold en beheerd, en veel sectoren, industrieën en ontwikkelaars hebben hun on-premises omgevingen al overgezet naar cloud-native omgevingen, of zijn hier momenteel mee bezig. Dit is inmiddels een volwassen domein. Daarom denk ik dat het niet nodig is om uit te leggen wat het ongemak is van hostnamen, poortconfiguraties, firewalls, metrics-beheer en dergelijke.
Op basis van de bovenstaande uitleg zult u waarschijnlijk nog steeds niet goed weten wat dotnet aspire precies is. Dat komt doordat Microsoft zelf geen exacte definitie heeft gegeven. Daarom zal ik ook geen eigen definitie geven. In dit artikel gebruik ik echter de basisfuncties van dotnet aspire zoals ik die begrijp, dus u kunt dit als referentie gebruiken om uw eigen standpunt te bepalen.
Projectstructuur
Een dotnet aspire-project aanmaken
Als de dotnet aspire-template nog niet beschikbaar is, moet u eerst de template installeren. Installeer de template met het volgende commando. Als .NET nog niet geïnstalleerd is, dient u dit zelf te doen.
1dotnet new install Aspire.ProjectTemplates
Maak vervolgens in een geschikte map een nieuwe solution aan.
1dotnet new sln
Voer daarna in de solutionmap het volgende commando uit om een project van de aspire-apphost-template aan te maken.
1dotnet new aspire-apphost -o AppHost
Hiermee wordt een aspire-apphost-project aangemaakt dat alleen eenvoudige code bevat voor de configuratie.
Valkey toevoegen
Laten we nu eenvoudig Valkey toevoegen.
Voordat we dat doen: dotnet aspire biedt via community hosting diverse oplossingen van derden. Valkey wordt uiteraard ook door deze community hosting ondersteund en is eenvoudig te gebruiken via het volgende NuGet-pakket.
1dotnet add package Aspire.Hosting.Valkey
Daarnaast zijn er diverse andere geïntegreerde hostings beschikbaar, die u hier kunt bekijken. Terug naar Valkey: open het bestand Program.cs in het AppHost-project en pas het als volgt aan.
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 is een implementatie van de IResourceBuilder-interface die de informatie bevat om een Valkey-service te bouwen.WithDataVolume maakt een volume aan om cachegegevens op te slaan, en WithPersistence zorgt ervoor dat cachegegevens continu worden opgeslagen.
Op basis hiervan lijkt het een vergelijkbare rol te vervullen als volumes in docker-compose.
Uiteraard kunt u dit ook zonder veel moeite zelf maken.
Dit valt echter buiten het bestek van dit artikel, dus ik ga er nu niet verder op in.
Een Go-echoserver maken
Laten we nu een eenvoudige Go-server toevoegen.
Maak eerst in de solutionmap met go work init een workspace aan.
Voor .NET-ontwikkelaars kunt u een Go-workspace zien als vergelijkbaar met een solution.
Maak vervolgens een map aan met de naam EchoServer, ga ernaartoe en voer go mod init EchoServer uit.
Met dit commando maakt u een Go-module aan. Een module kunt u voor .NET-ontwikkelaars beschouwen als vergelijkbaar met een project.
Maak daarna het bestand main.go aan en schrijf het als volgt.
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}
Deze server leest, wanneer de Aspire AppHost wordt uitgevoerd, de poort uit de omgevingsvariabele PORT die wordt meegegeven, en start de server op die poort.
Het is een eenvoudige server die de query-parameter name ontvangt en Hello, {name} teruggeeft.
Laten we deze server nu aan dotnet aspire toevoegen.
De echoserver toevoegen aan Aspire
Ga terug naar het Aspire AppHost-project waarin we Valkey hebben toegevoegd, en voeg de community hosting voor Go toe.
1dotnet add package CommunityToolkit.Aspire.Hosting.Golang
Open vervolgens het bestand Program.cs en voeg de volgende regels toe.
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();
Hier is echoServer een implementatie van de IResourceBuilder-interface die de informatie bevat om een Go-serverservice te bouwen.
De zojuist toegevoegde methode AddGolangApp is een extensiemethode van een aangepaste host om een Go-server toe te voegen.
Er wordt een vaste poort 3000 gebruikt, en u kunt zien dat de omgevingsvariabele PORT wordt meegegeven.
Tot slot zorgt WithExternalHttpEndpoints ervoor dat de service van buitenaf bereikbaar is.
Om te testen kunt u naar http://localhost:3000/?name=world gaan, waarna u zult zien dat Hello, world wordt weergegeven.
Het huidige dotnet aspire kent echter een zware straf voor niet-dotnet-projecten. Die is namelijk...
Projectuitbreiding
Hoe zit het dan met horizontale schaling?
Momenteel biedt dotnet aspire de optie WithReplica alleen voor builders van .NET-projecten die zijn toegevoegd via de methode AddProject.
Voor Go-hosts of externe projecten zoals AddContainer wordt deze optie echter niet aangeboden.
Daarom moet u zelf een aparte load balancer of reverse proxy gebruiken om dit te realiseren.
Maar als die reverse proxy een SPOF wordt, is het verstandig om de reverse proxy ook de optie WithReplica te laten ondersteunen.
Dan moet de reverse proxy noodzakelijkerwijs een .NET-project zijn.
Tot nu toe heb ik dit probleem aangepakt met methoden zoals nginx, Traefik of een eigen implementatie, maar zodra de beperking tot .NET-projecten gold, had ik op dat moment geen oplossing. Daarom heb ik gezocht naar een reverse proxy die in .NET is geïmplementeerd, en gelukkig was YARP een optie. YARP is een in .NET geïmplementeerde reverse proxy die ook als load balancer kan fungeren en diverse functies biedt, dus ik vond het een goede keuze.
Laten we nu YARP toevoegen.
Reverse proxy opzetten met YARP
Maak eerst een project aan voor het gebruik van YARP.
1dotnet new web -n ReverseProxy
Ga daarna naar het project en installeer YARP.
1dotnet add package Yarp.ReverseProxy --version 2.2.0
Zodra de installatie is voltooid, opent u het bestand Program.cs en schrijft u het als volgt.
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"}");
Deze code is de basiscode om YARP te gebruiken.routes bevat de routeinformatie die de reverse proxy gebruikt, en clusters bevat de clusterinformatie die de reverse proxy gebruikt.
Deze informatie wordt via de methode LoadFromMemory in de reverse proxy geladen.
Ten slotte worden de reverse proxy gekoppeld en uitgevoerd met de methode MapReverseProxy.
Voeg vervolgens voor daadwerkelijk gebruik het reverse proxy-project als referentie toe aan het Aspire apphost-project, en voeg in het bestand Program.cs de volgende regels toe en pas ze aan.
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 de reverse proxy de echoserver als referentie gebruiken. De structuur is veranderd zodat inkomende externe verzoeken door de reverse proxy worden ontvangen en doorgegeven aan de echoserver.
De reverse proxy aanpassen
Allereerst moet het listening-adres van het project dat aan de reverse proxy is toegewezen worden gewijzigd.
Verwijder de applicationUrl in het bestand Properties/launchSettings.json.
Open daarna het bestand Program.cs en pas het ingrijpend aan als volgt.
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"}");
Allereerst wordt de informatie over routes en clusters aangepast.
Er worden respectievelijk echo-route en echo-cluster toegevoegd, zodat verzoeken naar de echoserver worden gestuurd.
Vervolgens wordt het adres van de echoserver uitgelezen uit een omgevingsvariabele en gebruikt.
De regel voor dit adres is services__{service-name}__http__{index}.
Voor de echoserver is de servicenaam echo-server en omdat er slechts één instantie is, gebruiken we index 0.
Als u een ASP.NET Core-server toevoegt, kunnen via WithReplica meerdere instanties worden aangemaakt, en dan verhoogt u de index om die te gebruiken.
De fallback http://localhost:8080 is een betekenisloze waarde zonder enige functie.
Start nu het project en ga naar http://localhost:3000/?name=world, waarna u opnieuw zult zien dat Hello, world wordt weergegeven.
Uitbreidingsideeën
Nu hebben we gezien hoe een Go-server aan dotnet aspire wordt toegevoegd en hoe verzoeken via de reverse proxy worden doorgestuurd. Vervolgens kan dit proces worden uitgebreid zodat het programmatisch kan worden geïmplementeerd. Zo kunt u bijvoorbeeld voor de echoserver een volgnummer achter de servicenaam toevoegen om meerdere instanties te maken, en de configuratie van de reverse proxy automatisch laten toevoegen.
Pas in het bestand Program.cs van het aspire apphost-project de code die de reverse proxy en de echoserver gebruikt als volgt aan.
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}
Pas daarna het bestand Program.cs van het reverse proxy-project als volgt aan.
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};
Hier worden bestemmingsinstellingen toegevoegd voor de acht echoserver-instanties.
De reverse proxy beschikt nu over de bestemmingsinformatie van de uitgebreide echoserver-instanties en kan verzoeken daarnaar doorgeven.
Als u naar het bekende http://localhost:3000/?name=world gaat, zult u opnieuw zien dat Hello, world wordt weergegeven.
Afsluiting
In dit artikel heb ik beschreven hoe een Go-server aan dotnet aspire wordt toegevoegd en hoe verzoeken via een reverse proxy worden doorgestuurd. De uitbreiding heb ik echter nog niet volledig uitgewerkt. Een voorbeeld dat laat zien hoe u dit met behulp van omgevingsvariabelen programmatischer kunt implementeren, heb ik wel in een aparte repository gezet. Raadpleeg voor de volledige projectstructuur en code snowmerak/AspireStartPack.
Persoonlijk verwacht ik dat dotnet aspire een eigen rol kan vervullen als alternatief voor docker compose en als hulpmiddel voor cloud-implementatie. Er bestaan al generatoren die docker compose- of k8s-manifesten maken, zoals deze, en ik denk dat het voor gewone ontwikkelaars daardoor makkelijker is geworden om toegang te krijgen tot infrastructuurtools.