GoSuda

Rularea unui server Go în mod scalabil în .NET Aspire

By snowmerak views ...

dotnet aspire?

dotnet aspire este un instrument creat pentru a ajuta dezvoltatorii în dezvoltarea și configurarea aplicațiilor cloud native, pe măsură ce mediile cloud native se extind. Acest instrument permite dezvoltatorilor .NET să implementeze cu ușurință proiecte .NET și diverse infrastructuri cloud native, precum și servicii sau containere scrise în alte limbaje.

Desigur, de la Docker până la K8s, tehnologiile sunt lansate și operate, iar în multe domenii, industrii și rândul multor dezvoltatori, trecerea de la mediile on-premises tradiționale la mediile cloud native este în curs sau deja finalizată. Acum este un domeniu matur. Prin urmare, cred că nu este necesar să explic inconvenientele existente legate de numele gazdelor, configurarea porturilor, firewall-ul, gestionarea metricilor etc.

Așadar, chiar și din explicațiile de mai sus, este probabil că nu aveți încă o idee clară despre ce este dotnet aspire. Acest lucru se datorează faptului că nici Microsoft nu oferă o definiție precisă. Din acest motiv, nici eu nu voi oferi o definiție proprie. Totuși, în acest articol voi utiliza funcționalitățile de bază ale dotnet aspire pe care le-am înțeles, astfel încât puteți consulta acestea și vă puteți stabili propria poziție.

Configurarea proiectului

Crearea unui proiect dotnet aspire

Dacă șablonul dotnet aspire nu este disponibil, trebuie mai întâi să îl instalați. Instalați șablonul cu următoarea comandă. Dacă nu aveți .NET instalat, instalați-l pe cont propriu.

1dotnet new install Aspire.ProjectTemplates

Apoi creați o soluție nouă într-un folder adecvat.

1dotnet new sln

După aceea, în folderul soluției, executați următoarea comandă pentru a crea un proiect din șablonul aspire-apphost.

1dotnet new aspire-apphost -o AppHost

Va fi creat un proiect aspire-apphost care conține doar un cod simplu pentru configurare.

Adăugarea Valkey

Acum vom adăuga Valkey, pe scurt.

Înainte de a-l adăuga, trebuie menționat că dotnet aspire oferă diverse soluții terțe prin community hosting. Desigur, Valkey beneficiază și el de acest suport din partea community hosting și poate fi utilizat cu ușurință prin următorul pachet NuGet.

1dotnet add package Aspire.Hosting.Valkey

Pe lângă acestea, sunt disponibile și alte integrări de hosting, care pot fi consultate aici. Revenind la Valkey, deschideți fișierul Program.cs din proiectul AppHost și modificați-l astfel:

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 este o implementare a interfeței IResourceBuilder, care conține informațiile necesare pentru a construi serviciul Valkey.WithDataVolume creează un volum pentru stocarea datelor din cache, iar WithPersistence asigură salvarea continuă a datelor din cache. Până aici, se pare că aceste metode au un rol similar cu volumes din docker-compose. Desigur, și dumneavoastră puteți realiza acest lucru fără dificultate. Totuși, aceasta depășește scopul acestui articol, așa că nu voi intra în detalii acum.

Crearea unui server Echo în Go

Acum vom adăuga un server simplu în Go. Mai întâi, în folderul soluției, creați un workspace utilizând go work init. Pentru dezvoltatorii .NET, workspace-ul Go poate fi considerat asemănător unei soluții.

Apoi creați un folder numit EchoServer, accesați-l și executați go mod init EchoServer. Această comandă creează un modul Go. Modulul poate fi considerat, pentru dezvoltatorii .NET, similar unui proiect. Apoi creați fișierul main.go și scrieți următorul cod:

 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}

Acest server citește portul din variabila de mediu PORT, pe care Aspire AppHost o injectează atunci când rulează, și pornește serverul pe acel port. Este un server simplu care primește parametrul de interogare name și returnează Hello, {name}.

Acum vom adăuga acest server în dotnet aspire.

Adăugarea serverului Echo în Aspire

Reveniți la proiectul Aspire AppHost, în care ați adăugat Valkey, și instalați suportul community hosting pentru Go.

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

Apoi deschideți fișierul Program.cs și adăugați următoarea secțiune:

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

Aici, echoServer este o implementare a interfeței IResourceBuilder, care conține informațiile necesare pentru a construi serverul Go. Metoda AddGolangApp, pe care tocmai am adăugat-o, este o metodă de extensie a unui host personalizat, destinată adăugării unui server Go. Serverul utilizează un port fix, 3000, și se poate observa că variabila de mediu PORT este injectată. În final, WithExternalHttpEndpoints permite accesul din exterior.

Pentru testare, accesați http://localhost:3000/?name=world și veți putea observa că se afișează Hello, world.

Totuși, în prezent, dotnet aspire impune o penalizare considerabilă pentru proiectele care nu sunt .NET. Aceasta este, mai exact...

Extinderea proiectului

Cum se realizează scalarea orizontală?

În prezent, dotnet aspire oferă opțiunea WithReplica doar pentru builderele proiectelor .NET adăugate prin metoda AddProject. Însă pentru hosturile Go sau pentru proiecte externe precum AddContainer, această opțiune nu este disponibilă.

Prin urmare, trebuie implementată manual o soluție, utilizând un load balancer sau un reverse proxy separat. Totuși, dacă reverse proxy-ul devine un SPOF, acesta ar trebui să beneficieze de opțiunea WithReplica. În acest caz, reverse proxy-ul trebuie, în mod inevitabil, să fie un proiect .NET.

Până acum, am utilizat nginx, Traefik sau implementări proprii pentru a rezolva această problemă, însă odată cu restricția de a folosi un proiect .NET, nu aveam, pe moment, o soluție la îndemână. Așa că am căutat un reverse proxy implementat în .NET și, din fericire, a existat opțiunea YARP. YARP este un reverse proxy implementat în .NET, care poate funcționa și ca load balancer și oferă numeroase funcționalități, motiv pentru care l-am considerat o alegere bună.

Acum vom adăuga YARP.

Configurarea reverse proxy-ului cu YARP

Mai întâi, creați un proiect pentru utilizarea YARP:

1dotnet new web -n ReverseProxy

Apoi accesați proiectul și instalați YARP:

1dotnet add package Yarp.ReverseProxy --version 2.2.0

După finalizarea instalării, deschideți fișierul Program.cs și scrieți următorul cod:

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

Acest cod reprezintă configurația de bază pentru utilizarea YARP.routes conține informațiile despre rutele pe care le va utiliza reverse proxy-ul, iar clusters conține informațiile despre clusterele utilizate de acesta. Aceste informații sunt încărcate în reverse proxy prin metoda LoadFromMemory. În final, metoda MapReverseProxy mapează și pornește reverse proxy-ul.

Pentru utilizarea efectivă, adăugați în proiectul aspire apphost o referință către proiectul reverse proxy și adăugați sau modificați în fișierul Program.cs următoarea secțiune:

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

Acum reverse proxy-ul poate face referire la echo server. Cererile externe sunt primite de reverse proxy și transmise către echo server, astfel că structura a fost modificată în acest sens.

Modificarea reverse proxy-ului

Mai întâi, trebuie modificată adresa de ascultare a proiectului atribuit reverse proxy-ului. Eliminați applicationUrl din fișierul Properties/launchSettings.json. Apoi deschideți fișierul Program.cs și modificați-l substanțial, după cum urmează:

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

Mai întâi modificăm informațiile despre routes și clusters. Am adăugat echo-route și echo-cluster, care configurează trimiterea cererilor către echo server. Apoi, adresa echo server-ului este citită din variabilele de mediu și utilizată în aplicație.

Regula pentru această adresă este services__{service-name}__http__{index}. În cazul echo server-ului, numele serviciului este echo-server, iar deoarece există o singură instanță, se utilizează indexul 0. Dacă adăugăm un server ASP.NET Core, pot fi create mai multe instanțe prin WithReplica, caz în care este suficient să se incrementeze indexul. Valoarea implicită de excepție http://localhost:8080 este o valoare fără niciun sens.

Acum, rulați proiectul și accesați http://localhost:3000/?name=world; veți putea observa că se afișează în continuare Hello, world.

Idei de extindere

Până acum, am confirmat că un server Go poate fi adăugat în dotnet aspire și că cererile pot fi transmise prin intermediul reverse proxy-ului. Pe această bază, acest proces poate fi extins astfel încât să fie implementat programatic. De exemplu, pentru echo server, se pot adăuga numere de serie după numele serviciului pentru a crea mai multe instanțe, iar configurația reverse proxy-ului poate fi adăugată automat.

Modificați fișierul Program.cs al proiectului aspire apphost, care utilizează reverse proxy și echo server, după cum urmează:

 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}

Apoi modificați fișierul Program.cs al proiectului reverse proxy astfel:

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

Adăugăm configurația de destinații pentru cele 8 instanțe de echo server. Acum, reverse proxy-ul dispune de informațiile despre destinațiile tuturor instanțelor echo server și poate transmite cererile corespunzător. Dacă accesați din nou http://localhost:3000/?name=world, veți putea observa că se afișează în continuare Hello, world.

Încheiere

În acest articol, am descris procesul de adăugare a unui server Go în dotnet aspire și de transmitere a cererilor prin intermediul unui reverse proxy. Totuși, extinderea nu a fost încă prezentată integral; un exemplu care implementează mai programatic, utilizând variabile de mediu, a fost publicat într-un repository separat. Pentru configurația detaliată a proiectului și codul complet, consultați snowmerak/AspireStartPack.

Personal, sper că dotnet aspire își va putea îndeplini propriul rol, atât ca alternativă la docker compose, cât și ca instrument de implementare în cloud. Există deja generatoare care creează manifeste docker compose sau k8s, iar acest lucru face ca instrumentele de infrastructură să fie mai accesibile pentru dezvoltatorii obișnuiți.