GoSuda

Kjøre en skalerbar Go-server med .NET Aspire

By snowmerak views ...

dotnet aspire?

dotnet aspire er et verktøy som ble utviklet for å hjelpe utviklere med utvikling og konfigurasjon av skybaserte (cloud native) løsninger, i takt med at antallet skybaserte miljøer øker. Verktøyet gjør det enklere for .NET-utviklere å distribuere .NET-prosjekter og ulik cloud native-infrastruktur, samt tjenester skrevet på andre språk eller containere.

Det er selvsagt at løsninger lanseres og kjøres fra Docker til Kubernetes (k8s). Mange sektorer, bransjer og utviklere flytter fra tradisjonelle lokale (on-premises) miljøer til skybaserte miljøer, og mange har allerede gjort det. Dette er nå et modent område. Derfor antar jeg at det ikke er nødvendig å forklare de eksisterende utfordringene knyttet til vertsnavn, portkonfigurasjon, brannmurer, metrikkhåndtering og lignende.

Med bare forklaringene ovenfor er det sannsynlig at du fortsatt ikke har en god forståelse av hva dotnet aspire er. Dette skyldes at Microsoft heller ikke gir en presis definisjon av det. Derfor vil heller ikke jeg gi noen egen definisjon. I stedet vil jeg bruke de grunnleggende funksjonene i dotnet aspire slik jeg har forstått dem i denne artikkelen, og du kan bruke dette som utgangspunkt for å danne din egen oppfatning.

Prosjektoppsett

Opprette et dotnet aspire-prosjekt

Dersom dotnet aspire-malen ikke er tilgjengelig, må du først installere malen. Installer malen med følgende kommando. Dersom .NET ikke er installert, må du installere det selv.

1dotnet new install Aspire.ProjectTemplates

Deretter oppretter du en ny løsning (solution) i en passende mappe.

1dotnet new sln

Kjør så følgende kommando i løsningsmappen for å opprette et prosjekt basert på malen aspire-apphost.

1dotnet new aspire-apphost -o AppHost

Da opprettes et aspire-apphost-prosjekt som kun inneholder enkel kode for oppsett.

Legge til Valkey

La oss nå legge til Valkey på en enkel måte.

Før vi legger den til, bør du vite at dotnet aspire gjennom community hosting tilbyr ulike tredjepartsløsninger. Valkey støttes også av community hosting, og den kan enkelt tas i bruk med følgende NuGet-pakke.

1dotnet add package Aspire.Hosting.Valkey

I tillegg finnes det flere integrerte hostingløsninger, som du kan se her. Tilbake til Valkey: Åpne filen Program.cs i AppHost-prosjektet og endre den slik:

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 er en implementasjon av grensesnittet IResourceBuilder, som inneholder informasjon for å bygge en Valkey-tjeneste.WithDataVolume oppretter et volum for lagring av cachedata, og WithPersistence sørger for at cachedata lagres løpende. Sett fra dette perspektivet ser det ut til å ha en lignende rolle som volumes i docker-compose. Dette er selvsagt noe dere også kan lage uten store vanskeligheter. Men siden det ligger utenfor rammene for denne artikkelen, vil jeg ikke gå nærmere inn på det nå.

Opprette en Go-ekkoserver

La oss deretter legge til en enkel Go-server. Først oppretter du et Go-workspace i løsningsmappen med go work init. For .NET-utviklere kan man se på et Go-workspace som noe tilsvarende en løsning.

Deretter oppretter du en mappe kalt EchoServer, går inn i den og kjører go mod init EchoServer. Denne kommandoen oppretter en Go-modul. For .NET-utviklere kan en modul forstås som noe tilsvarende et prosjekt. Deretter oppretter du filen main.go og skriver følgende:

 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}

Denne serveren leser porten fra miljøvariabelen PORT, som Aspire AppHost injiserer når den kjøres, og starter serveren på denne porten. Den er en enkel server som tar imot spørringsparameteren name og returnerer Hello, {name}.

Nå skal vi legge denne serveren til i dotnet aspire.

Legge til ekkoserveren i aspire

Gå tilbake til AppHost-prosjektet der Valkey ble lagt til, og legg til community hosting for Go:

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

Åpne deretter Program.cs og legg til følgende kode:

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

Her er echoServer en implementasjon av grensesnittet IResourceBuilder, som inneholder informasjon for å bygge Go-serveren. Metoden AddGolangApp som vi nettopp la til, er en extension-metode for å legge til en Go-server via en egendefinert host. Den bruker fast port 3000, og vi ser at miljøvariabelen PORT injiseres. Til slutt sørger WithExternalHttpEndpoints for at serveren kan nås utenfra.

For å teste kan du åpne http://localhost:3000/?name=world, og du vil se at Hello, world skrives ut.

Men i dotnet aspire finnes det for øyeblikket en tung ulempe for prosjekter som ikke er .NET-prosjekter. Den er nemlig...

Utvidelse av prosjektet

Hvordan gjør man horisontal skalering?

For øyeblikket tilbyr dotnet aspire alternativet WithReplica kun for builderen til .NET-prosjekter som er lagt til med metoden AddProject. For Go-verter og eksterne prosjekter, slik som de som legges til med AddContainer, tilbys ikke dette alternativet.

Derfor må man selv implementere dette med en egen load balancer eller reverse proxy. Men da kan reverse proxyen bli et SPOF (single point of failure), og det er derfor en god idé at reverse proxyen støtter WithReplica. Da må reverse proxyen nødvendigvis være et .NET-prosjekt.

Jeg har tidligere forsøkt å løse dette med nginx, Traefik og egne implementasjoner, men når kravet om .NET-prosjekt settes, fantes det ingen umiddelbar løsning for meg. Derfor så jeg etter reverse proxyer implementert i .NET, og heldigvis fant jeg YARP. YARP er en reverse proxy implementert i .NET. Den kan også fungere som load balancer og tilbyr mange funksjoner, så jeg vurderte den som et godt valg.

La oss nå legge til YARP.

Oppsett av reverse proxy med YARP

Først oppretter du et prosjekt som skal bruke YARP:

1dotnet new web -n ReverseProxy

Deretter går du inn i prosjektet og installerer YARP:

1dotnet add package Yarp.ReverseProxy --version 2.2.0

Når installasjonen er ferdig, åpner du Program.cs og skriver følgende:

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

Denne koden er et grunnleggende oppsett for bruk av YARP.routes inneholder rutinginformasjonen som reverse proxyen skal bruke, og clusters inneholder klyngeinformasjonen som reverse proxyen skal bruke. Denne informasjonen lastes inn i reverse proxyen med metoden LoadFromMemory. Til slutt registreres og kjøres reverse proxyen med MapReverseProxy.

For faktisk bruk legger du til reverse proxy-prosjektet som referanse i AppHost-prosjektet i aspire, og legger til eller endrer følgende 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();

Nå kan reverse proxyen referere til ekkoserveren. Innkommende forespørsler fra eksterne kilder mottas av reverse proxyen og videresendes til ekkoserveren.

Endring av reverse proxy

Først må du endre listening-adressen for prosjektet som er tildelt reverse proxyen. Fjern applicationUrl i filen Properties/launchSettings.json. Deretter åpner du Program.cs og endrer den omfattende slik:

 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 endrer du informasjonen om routes og clusters. Du legger til henholdsvis echo-route og echo-cluster, slik at forespørsler sendes til ekkoserveren. Deretter endrer du koden slik at adressen til ekkoserveren leses fra miljøvariabler.

Regelen for denne adressen er services__{service-name}__http__{index}. For ekkoserveren er tjenestenavnet echo-server, og siden det kun finnes én instans, brukes 0 som indeks. Dersom du legger til en ASP.NET Core-server, kan flere instanser opprettes med WithReplica, og da økes indeksen for å bruke dem. Verdien http://localhost:8080 i unntakshåndteringen er en meningsløs standardverdi.

Nå kan du kjøre prosjektet og åpne http://localhost:3000/?name=world, og du vil fortsatt se at Hello, world skrives ut.

Utvidelsesidéer

Nå har vi lagt til en Go-server i dotnet aspire og bekreftet at forespørsler videresendes via reverse proxyen. Da kan vi utvide prosessen slik at den kan implementeres programmatisk. For eksempel kan vi legge til løpenummer etter tjenestenavnet for ekkoserveren for å opprette flere instanser, og automatisk legge til konfigurasjon for reverse proxyen.

Endre koden i Program.cs i aspire AppHost-prosjektet slik at den bruker reverse proxyen og ekkoserveren:

 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}

Deretter endrer du Program.cs i reverse proxy-prosjektet slik:

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

Du legger til målkonfigurasjon for de åtte ekkoserverinstansene. Reverse proxyen har nå målinformasjon for alle ekkoserverne og kan videresende forespørsler til dem. Når du åpner http://localhost:3000/?name=world, vil du fortsatt se at Hello, world skrives ut.

Avslutning

I denne artikkelen har vi beskrevet hvordan man legger til en Go-server i dotnet aspire og videresender forespørsler via en reverse proxy. Utvidelsen er imidlertid ikke fullstendig dokumentert ennå. Jeg har laget et eksempel i et eget repository som viser hvordan dette kan implementeres mer programmatisk ved hjelp av miljøvariabler. For detaljert prosjektoppsett og kode, se snowmerak/AspireStartPack.

Personlig tror jeg at dotnet aspire kan spille sin egen rolle både som et alternativ til docker compose og som et verktøy for skyutrulling. Det finnes allerede generatorer som lager docker compose- eller k8s-manifester, og jeg mener dette har gjort infrastrukturverktøy mer tilgjengelige for vanlige utviklere.