GoSuda

Uruchamianie serwerów Go w dotnet Aspire z możliwością rozszerzenia

By snowmerak
views ...

dotnet aspire?

dotnet aspire to narzędzie stworzone w celu wspierania deweloperów w tworzeniu i konfiguracji aplikacji Cloud Native, w związku z rosnącą popularnością środowisk Cloud Native. Narzędzie to umożliwia programistom .NET łatwe wdrażanie projektów .NET oraz różnorodnej infrastruktury Cloud Native, a także usług napisanych w innych językach lub kontenerów.

Oczywiście, od Docker do k8s, wiele branż, sektorów i deweloperów przeszło lub przechodzi z tradycyjnych środowisk on-premises do środowisk Cloud Native. Jest to już dojrzała dziedzina. Dlatego uważam, że nie ma potrzeby wyjaśniać dotychczasowych niedogodności związanych z zarządzaniem nazwami hostów, konfiguracją portów, zaporami ogniowymi czy metrykami.

Dlatego, nawet na podstawie powyższych wyjaśnień, trudno jest zrozumieć, czym dokładnie jest dotnet aspire. Dzieje się tak, ponieważ nawet Microsoft nie przedstawił precyzyjnej definicji. W związku z tym, ja również nie będę formułował żadnej konkretnej definicji. Jednakże, w tym artykule będę korzystał z podstawowych funkcji dotnet aspire, które rozumiem, więc proszę o zapoznanie się z nimi i określenie własnego stanowiska.

Konfiguracja projektu

Tworzenie projektu dotnet aspire

Jeśli nie masz szablonu dotnet aspire, musisz go najpierw zainstalować. Zainstaluj szablon za pomocą następującego polecenia. Jeśli nie masz zainstalowanego .net, zrób to samodzielnie.

1dotnet new install Aspire.ProjectTemplates

Następnie utwórz nowe rozwiązanie w odpowiednim folderze.

1dotnet new sln

Następnie, w folderze rozwiązania, wykonaj następujące polecenie, aby utworzyć projekt na podstawie szablonu aspire-apphost.

1dotnet new aspire-apphost -o AppHost

Spowoduje to utworzenie projektu aspire-apphost zawierającego jedynie prosty kod do konfiguracji.

Dodawanie Valkey

Spróbujmy teraz dodać Valkey.

Zanim dodamy, warto wspomnieć, że dotnet aspire oferuje różne rozwiązania stron trzecich poprzez community hosting. Valkey oczywiście może korzystać z wsparcia tego community hosting i jest łatwo dostępne za pośrednictwem następującego pakietu nuget.

1dotnet add package Aspire.Hosting.Valkey

Dostępne są również inne zintegrowane hostowania, które można sprawdzić tutaj. Wracając do Valkey, otwórz plik Program.cs w projekcie AppHost i zmodyfikuj go w następujący sposób:

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 to implementacja interfejsu IResourceBuilder, która zawiera informacje niezbędne do zbudowania usługi Valkey.WithDataVolume tworzy wolumin do przechowywania danych cache, a WithPersistence umożliwia trwałe przechowywanie danych cache. Z tego, co widać, wydaje się, że pełni on podobną rolę do volumes w docker-compose. Oczywiście, możecie to bez problemu stworzyć samodzielnie. Jednakże, wykracza to poza zakres tego artykułu, więc nie będę o tym teraz mówić.

Tworzenie serwera Echo w języku Go

Dodajmy teraz prosty serwer w języku Go. Najpierw utwórzmy obszar roboczy, używając go work init w folderze rozwiązania. Dla deweloperów .NET, obszar roboczy Go jest podobny do rozwiązania.

Następnie utwórz folder o nazwie EchoServer, przejdź do niego i wykonaj go mod init EchoServer. To polecenie tworzy moduł Go. Deweloperzy .NET mogą postrzegać moduł jako coś podobnego do projektu. Następnie utwórz plik main.go i napisz w nim następujący kod:

 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}

Serwer ten, po uruchomieniu Aspire AppHost, odbiera zmienną środowiskową PORT, na której ma nasłuchiwać, a następnie odczytuje ten port i uruchamia serwer. Jest to prosty serwer, który przyjmuje zapytanie name i zwraca Hello, {name}.

Teraz dodajmy ten serwer do dotnet aspire.

Dodawanie serwera Echo do Aspire

Ponownie przejdź do projektu Aspire AppHost, do którego dodaliśmy Valkey, i dodaj community hosting dla języka Go.

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

Następnie otwórz plik Program.cs i dodaj następujący kod:

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

W tym miejscu echoServer jest implementacją interfejsu IResourceBuilder, która zawiera informacje potrzebne do zbudowania serwera w języku Go. Właśnie dodana metoda AddGolangApp jest metodą rozszerzającą niestandardowego hosta, służącą do dodawania serwera w języku Go. Można zauważyć, że używa ona stałego portu 3000 i wstrzykuje zmienną środowiskową PORT. Ostatnia metoda, WithExternalHttpEndpoints, umożliwia dostęp z zewnątrz.

Aby przetestować, wejdź na http://localhost:3000/?name=world, a powinieneś zobaczyć komunikat Hello, world.

Jednakże, obecnie dotnet aspire nakłada poważną karę na projekty non-dotnet. Mianowicie...

Rozszerzanie projektu

Jak zatem skalować horyzontalnie?

Obecnie dotnet aspire udostępnia opcję WithReplica wyłącznie dla konstruktorów projektów .NET dodanych za pomocą metody AddProject. Jednakże, dla hostów Go lub projektów zewnętrznych, takich jak te dodane przez AddContainer, ta opcja nie jest dostępna.

Dlatego konieczne jest samodzielne zaimplementowanie tego za pomocą oddzielnego load balancera lub reverse proxy. Jednakże, w takim przypadku reverse proxy może stać się SPOF, dlatego zaleca się, aby reverse proxy zapewniało opcję WithReplica. W związku z tym, reverse proxy musi być projektem .NET.

Dotychczas do rozwiązywania tego problemu stosowano metody takie jak nginx, trafik czy własne implementacje, ale z ograniczeniem do projektów .NET, nie miałem pod ręką żadnych rozwiązań. Dlatego poszukałem reverse proxy zaimplementowanego w .NET i na szczęście znalazłem opcję YARP. YARP to reverse proxy zaimplementowane w .NET, które może pełnić rolę load balancera i oferuje różnorodne funkcje, dlatego uznałem je za dobry wybór.

Teraz dodajmy YARP.

Konfiguracja reverse proxy za pomocą YARP

Najpierw utwórz projekt, aby użyć YARP.

1dotnet new web -n ReverseProxy

Następnie przejdź do projektu i zainstaluj YARP.

1dotnet add package Yarp.ReverseProxy --version 2.2.0

Po zakończeniu instalacji, otwórz plik Program.cs i napisz następujący kod:

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

Ten kod to podstawowy kod do używania YARP.routes przechowuje informacje o trasach, które będzie używał reverse proxy, a clusters przechowuje informacje o klastrach, które będzie używał reverse proxy. Informacje te są ładowane do reverse proxy za pomocą metody LoadFromMemory. Na koniec, reverse proxy jest mapowane i uruchamiane za pomocą metody MapReverseProxy.

Następnie, w celu praktycznego użycia, dodaj odwołanie do projektu reverse proxy w projekcie aspire apphost i dodaj oraz zmodyfikuj następujący kod w pliku 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();

Teraz reverse proxy może odwoływać się do serwera echo. Żądania przychodzące z zewnątrz są odbierane przez reverse proxy i przekazywane do serwera echo.

Modyfikacja reverse proxy

Najpierw należy zmienić adres nasłuchiwania projektu przypisanego do reverse proxy. Usuń applicationUrl z pliku Properties/launchSettings.json. Następnie otwórz plik Program.cs i zmodyfikuj go w następujący sposób:

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

Najpierw modyfikujemy informacje dotyczące routes i clusters. Dodajemy odpowiednio echo-route i echo-cluster, aby żądania były wysyłane do serwera echo. Następnie modyfikujemy kod tak, aby adres serwera echo był odczytywany ze zmiennej środowiskowej.

Zasada nazewnictwa tego adresu to services__{service-name}__http__{index}. W przypadku serwera echo, nazwa usługi to echo-server, a ponieważ jest to pojedyncza instancja, używany jest indeks 0. Jeśli dodasz serwer ASP .NET Core, można utworzyć wiele instancji za pomocą WithReplica, więc możesz użyć rosnących indeksów. Wartość http://localhost:8080, która jest obsłużona jako wyjątek, jest wartością śmieciową, która nie ma żadnego znaczenia.

Teraz uruchom projekt i wejdź na http://localhost:3000/?name=world, a powinieneś nadal widzieć Hello, world.

Pomysły na rozszerzenia

Teraz, gdy dodaliśmy serwer Go do dotnet aspire i potwierdziliśmy, że żądania są przekazywane przez reverse proxy. Teraz możemy rozszerzyć ten proces, aby móc go programowo zaimplementować. Na przykład, możemy utworzyć wiele instancji serwera echo, dodając numerację po nazwie usługi, i automatycznie dodawać konfigurację dla reverse proxy.

Zmodyfikuj kod używający reverse proxy i serwera echo w pliku Program.cs projektu aspire apphost w następujący sposób:

 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}

Następnie zmodyfikuj plik Program.cs projektu reverse proxy w następujący sposób:

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

Dodajemy konfigurację docelową dla 8 zwiększonych instancji serwera echo. Teraz reverse proxy posiada informacje o docelowych serwerach echo i może przekazywać żądania. Jeśli wejdziesz na http://localhost:3000/?name=world, nadal powinieneś zobaczyć Hello, world.

Podsumowanie

W tym artykule opisano proces dodawania serwera Go do dotnet aspire i przekazywania żądań za pomocą reverse proxy. Jednakże, rozszerzenia nie zostały jeszcze w pełni opisane, a przykłady implementacji programistycznej za pomocą zmiennych środowiskowych zostały umieszczone w osobnym repozytorium. Szczegółową konfigurację projektu i kod można znaleźć pod adresem snowmerak/AspireStartPack.

Osobiście oczekuję, że dotnet aspire spełni swoją rolę jako alternatywa dla docker compose oraz jako narzędzie do wdrażania w chmurze. Już istnieją generatory tworzące manifesty docker compose czy k8s, co moim zdaniem zwiększyło dostępność narzędzi infrastrukturalnych dla przeciętnego dewelopera.