GoSuda

Ausführungen eines Go-Servers in dotnet aspire auf erweiterbare Weise

By snowmerak
views ...

dotnet aspire?

dotnet aspire ist ein Tool, das entwickelt wurde, um Entwickler bei der Cloud-nativen Entwicklung und Konfiguration zu unterstützen, da Cloud-native Umgebungen immer häufiger werden. Dieses Tool ermöglicht es .NET-Entwicklern, .NET-Projekte und verschiedene Cloud-native Infrastrukturen sowie Dienste oder Container in anderen Sprachen einfach bereitzustellen.

Es ist selbstverständlich, dass von Docker bis k8s eingeführt und betrieben wird, und dass viele Bereiche, Industrien und Entwickler von der bestehenden On-Premise-Umgebung zu Cloud-nativen Umgebungen übergegangen sind oder sich bereits dort befinden. Dies ist mittlerweile ein ausgereifter Bereich. Daher halte ich es für unnötig, die bisherigen Unannehmlichkeiten in Bezug auf Hostnamen, Portkonfiguration, Firewalls und Metrikverwaltung zu erläutern.

Daher werden Sie selbst anhand der obigen Erklärungen kaum ein Gefühl dafür bekommen, was dotnet aspire ist. Denn selbst Microsoft gibt keine präzise Definition. Daher werde ich auch keine gesonderte Definition geben. Allerdings werde ich in diesem Artikel die grundlegenden Funktionen von dotnet aspire verwenden, wie ich sie verstanden habe, und Sie können sich daran orientieren, um Ihre eigene Position zu bestimmen.

Projektkonfiguration

Erstellung eines dotnet aspire-Projekts

Falls keine dotnet aspire-Vorlage vorhanden ist, muss diese zuerst installiert werden. Installieren Sie die Vorlage mit dem folgenden Befehl. Falls .NET nicht installiert ist, installieren Sie es bitte selbst.

1dotnet new install Aspire.ProjectTemplates

Erstellen Sie anschließend in einem geeigneten Ordner eine neue Lösung.

1dotnet new sln

Führen Sie danach im Lösungsordner den folgenden Befehl aus, um ein Projekt der aspire-apphost-Vorlage zu erstellen.

1dotnet new aspire-apphost -o AppHost

Dadurch wird ein aspire-apphost-Projekt generiert, das lediglich einfachen Code zur Konfiguration enthält.

Hinzufügen von Valkey

Lassen Sie uns nun Valkey einfach hinzufügen.

Bevor wir es hinzufügen, bietet dotnet aspire über Community Hosting verschiedene Lösungen von Drittanbietern an. Valkey kann selbstverständlich auch durch dieses Community Hosting unterstützt werden und ist einfach über das folgende NuGet-Paket verfügbar.

1dotnet add package Aspire.Hosting.Valkey

Es werden auch verschiedene integrierte Hostings angeboten, die Sie hier einsehen können. Kehren wir zu Valkey zurück und öffnen die Datei Program.cs im AppHost-Projekt, um sie wie folgt zu ändern:

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 ist eine Implementierung des IResourceBuilder-Interfaces, das Informationen zum Erstellen des Valkey-Dienstes enthält. WithDataVolume erstellt ein Volume zum Speichern der Cache-Daten, und WithPersistence ermöglicht die persistente Speicherung der Cache-Daten. Bis hierher scheint es eine ähnliche Rolle wie die volumes in docker-compose zu spielen. Selbstverständlich können Sie dies auch selbst problemlos erstellen. Dies würde jedoch den Rahmen dieses Artikels sprengen, daher werde ich es jetzt nicht behandeln.

Erstellung eines Go-Sprach-Echo-Servers

Fügen wir nun einen einfachen Go-Sprachserver hinzu. Zunächst initialisieren wir den Workspace im Lösungsordner mit go work init. Für .NET-Entwickler kann ein Go-Workspace als Äquivalent zu einer Lösung betrachtet werden.

Erstellen Sie dann einen Ordner namens EchoServer, navigieren Sie hinein und führen Sie go mod init EchoServer aus. Dieser Befehl erstellt ein Go-Modul. Für .NET-Entwickler kann ein Modul als Äquivalent zu einem Projekt betrachtet werden. Erstellen Sie anschließend die Datei main.go und schreiben Sie den folgenden Code hinein:

 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}

Dieser Server liest den PORT-Umgebungsvariablen, die von Aspire AppHost injiziert wird, wenn es ausgeführt wird und auf einen Port lauschen soll, und startet den Server auf diesem Port. Es ist ein einfacher Server, der eine name-Abfrage entgegennimmt und Hello, {name} zurückgibt.

Lassen Sie uns diesen Server nun zu dotnet aspire hinzufügen.

Hinzufügen des Echo-Servers zu Aspire

Wechseln Sie zurück zum Aspire AppHost-Projekt, wo Valkey hinzugefügt wurde, und fügen Sie das Community Hosting für die Go-Sprache hinzu.

1dotnet add package CommunityToolkit.Aspire.Hosting.Golang

Öffnen Sie anschließend die Datei Program.cs und fügen Sie die folgende Anweisung hinzu:

 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 ist echoServer eine Implementierung der IResourceBuilder-Schnittstelle, die Informationen zum Erstellen des Go-Sprachservers enthält. Die gerade hinzugefügte Methode AddGolangApp ist eine Erweiterungsmethode des benutzerdefinierten Hosts zum Hinzufügen eines Go-Sprachservers. Es ist zu erkennen, dass der Port 3000 fest verwendet und die Umgebungsvariable PORT injiziert wird. Schließlich ermöglicht WithExternalHttpEndpoints den Zugriff von außen.

Zum Testen können Sie http://localhost:3000/?name=world aufrufen, und Sie sollten "Hello, world" als Ausgabe sehen.

Aktuell gibt es jedoch eine erhebliche Benachteiligung für Nicht-dotnet-Projekte in dotnet aspire. Es ist die Tatsache, dass...

Projekterweiterung

Wie funktioniert dann die horizontale Skalierung?

Derzeit bietet dotnet aspire die WithReplica-Option nur für Builder von .NET-Projekten an, die mit der Methode AddProject hinzugefügt wurden. Für Go-Sprachhosts oder externe Projekte wie AddContainer wird diese Option jedoch nicht bereitgestellt.

Daher muss man dies direkt mit einem separaten Load Balancer oder Reverse Proxy implementieren. Dies könnte jedoch dazu führen, dass der Reverse Proxy ein SPOF (Single Point of Failure) wird, weshalb es ratsam ist, dass der Reverse Proxy die WithReplica-Option bietet. Das bedeutet zwangsläufig, dass der Reverse Proxy ein .NET-Projekt sein muss.

Bisher wurden für solche Probleme Methoden wie Nginx, Traefik oder Eigenimplementierungen verwendet, aber mit der Einschränkung auf .NET-Projekte hatte ich persönlich keine sofortige Lösung zur Hand. Daher suchte ich nach einem in .NET implementierten Reverse Proxy und fand glücklicherweise mit YARP eine Option. YARP ist ein in .NET implementierter Reverse Proxy, der auch als Load Balancer fungieren und verschiedene Funktionen bereitstellen kann, was ihn zu einer guten Wahl machte.

Fügen wir nun YARP hinzu.

Konfiguration des Reverse Proxy mit YARP

Erstellen Sie zunächst ein Projekt für die Verwendung von YARP.

1dotnet new web -n ReverseProxy

Navigieren Sie dann in das Projekt und installieren Sie YARP.

1dotnet add package Yarp.ReverseProxy --version 2.2.0

Nach der Installation öffnen Sie die Datei Program.cs und schreiben Sie Folgendes:

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

Dieser Code ist der grundlegende Code für die Verwendung von YARP. routes enthält die Routeninformationen, die der Reverse Proxy verwenden wird, und clusters enthält die Clusterinformationen, die der Reverse Proxy verwenden wird. Diese Informationen werden mit der LoadFromMemory-Methode in den Reverse Proxy geladen. Schließlich wird der Reverse Proxy mit der MapReverseProxy-Methode gemappt und ausgeführt.

Für die praktische Anwendung fügen Sie das Reverse Proxy Projekt als Referenz in das Aspire AppHost Projekt ein und fügen Sie die folgende Anweisung in die Datei Program.cs ein und ändern Sie sie:

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

Nun kann der Reverse Proxy auf den Echo-Server verweisen. Externe Anfragen werden nun vom Reverse Proxy empfangen und an den Echo-Server weitergeleitet.

Reverse Proxy Modifikation

Zunächst muss die Listening-Adresse des dem Reverse Proxy zugewiesenen Projekts geändert werden. Entfernen Sie applicationUrl aus der Datei Properties/launchSettings.json. Öffnen Sie dann die Datei Program.cs und nehmen Sie die folgenden umfassenden Änderungen vor:

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

Zunächst werden die Informationen für routes und clusters angepasst. Es werden echo-route und echo-cluster hinzugefügt, um Anfragen an den Echo-Server weiterzuleiten. Die Adresse des Echo-Servers wird so geändert, dass sie aus einer Umgebungsvariable gelesen wird.

Die Regel für diese Adresse lautet services__{service-name}__http__{index}. Im Falle des Echo-Servers ist der Dienstname echo-server, und da es sich um eine einzelne Instanz handelt, wird 0 als Index verwendet. Wenn ein ASP.NET Core-Server hinzugefügt wird, können mehrere Instanzen über WithReplica erstellt werden, sodass der Index entsprechend erhöht werden kann. Der als Ausnahme behandelte Wert http://localhost:8080 ist ein bedeutungsloser Junk-Wert.

Führen Sie nun das Projekt aus und greifen Sie auf http://localhost:3000/?name=world zu. Sie sollten immer noch "Hello, world" sehen.

Erweiterungsideen

Wir haben nun gesehen, wie ein Go-Server zu dotnet aspire hinzugefügt und Anfragen über einen Reverse Proxy weitergeleitet werden. Nun können wir diesen Prozess programmatisch erweitern. Zum Beispiel können wir für den Echo-Server mehrere Instanzen erstellen, indem wir eine Nummerierung zum Dienstnamen hinzufügen und die Einstellungen für den Reverse Proxy automatisch ergänzen.

Ändern Sie den Code, der den Reverse Proxy und den Echo Server verwendet, in der Datei Program.cs des aspire apphost-Projekts wie folgt:

 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}

Und ändern Sie die Datei Program.cs des Reverse-Proxy-Projekts wie folgt:

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

Für die 8 erweiterten Echo-Server-Instanzen werden Zielkonfigurationen hinzugefügt. Der Reverse Proxy verfügt nun über die Zielinformationen für die erweiterten Echo-Server und kann Anfragen weiterleiten. Wenn Sie die ursprüngliche URL http://localhost:3000/?name=world aufrufen, sollten Sie immer noch "Hello, world" sehen.

Fazit

In diesem Artikel wurde der Prozess beschrieben, wie ein Go-Server zu dotnet aspire hinzugefügt und Anfragen über einen Reverse Proxy weitergeleitet werden. Die Erweiterung wurde jedoch noch nicht vollständig beschrieben, und ein Beispiel, das eine programmatischere Implementierung über Umgebungsvariablen ermöglicht, wurde in einem separaten Repository erstellt. Ausführliche Projektkonfigurationen und Code finden Sie unter snowmerak/AspireStartPack.

Ich persönlich erwarte, dass dotnet aspire als Alternative zu Docker Compose und als Cloud-Bereitstellungstool seine eigene Rolle spielen kann. Es gibt bereits einen Generator, der Docker Compose- oder k8s-Manifeste erstellt, was die Zugänglichkeit zu Infrastruktur-Tools für normale Entwickler verbessert hat, wie ich meine.