---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Ausprobieren, einen Go-Server skalierbar mit .NET Aspire auszuführen
description: 'Wie man Go-Server mit .NET Aspire skalierbar betreibt: Beispielkonfiguration für horizontale Skalierung und Automatisierung mit dem YARP-Reverse-Proxy.'
language: de
date: 2025-01-05T05:04:22.744778105Z
path: /blog/posts/exploring-the-scalable-execution-of-go-servers-in-net-aspire-z42f3b6ee
category: network
---

## dotnet aspire?

dotnet aspire ist ein Werkzeug, das mit der zunehmenden Verbreitung cloud-nativer Umgebungen entwickelt wurde, um Entwicklern bei der Entwicklung und Konfiguration cloud-nativer Anwendungen zu helfen. Dieses Werkzeug ermöglicht .NET-Entwicklern, .NET-Projekte sowie verschiedene cloud-native Infrastrukturen und darüber hinaus Dienste in anderen Sprachen oder Container auf einfache Weise bereitzustellen.

Selbstverständlich reicht dies von Docker bis Kubernetes und wird produktiv betrieben. Viele Bereiche, Branchen und Entwickler migrieren von klassischen On-Premises-Umgebungen in cloud-native Umgebungen, und diese Migration ist bereits vollzogen. Mittlerweile handelt es sich um einen ausgereiften Bereich. Daher dürfte es nicht notwendig sein, die bisherigen Unannehmlichkeiten bei Hostnamen, Portkonfiguration, Firewalls, Metrikverwaltung und Ähnlichem ausführlich zu erläutern.

Aufgrund der obigen Erläuterungen dürfte es Ihnen daher kaum möglich sein, eine Vorstellung davon zu entwickeln, was dotnet aspire eigentlich ist. Der Grund dafür ist, dass Microsoft selbst keine präzise Definition dafür liefert. Deshalb werde auch ich keine eigene Definition vornehmen. Stattdessen werde ich in diesem Beitrag die grundlegenden Funktionen von dotnet aspire verwenden, wie ich sie verstanden habe. Sie können sich daran orientieren und sich Ihre eigene Position bilden.

## Projektaufbau

### Erstellen eines dotnet aspire-Projekts

Falls die dotnet aspire-Vorlage nicht vorhanden ist, muss sie zunächst installiert werden. Installieren Sie die Vorlage mit dem folgenden Befehl. Falls .NET nicht installiert ist, installieren Sie es bitte selbst.

```bash
dotnet new install Aspire.ProjectTemplates
```

Erstellen Sie anschließend in einem geeigneten Ordner eine neue Solution.

```bash
dotnet new sln
```

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

```bash
dotnet new aspire-apphost -o AppHost
```

Daraufhin wird ein aspire-apphost-Projekt erzeugt, das lediglich einen einfachen Code für die Konfiguration enthält.

### Hinzufügen von Valkey

Nun fügen wir Valkey auf einfache Weise hinzu.

Bevor wir es hinzufügen, sei erwähnt, dass dotnet aspire über Community Hosting eine Vielzahl von Drittanbieterlösungen bereitstellt.
Selbstverständlich kann auch Valkey von diesem Community Hosting profitieren und lässt sich über das folgende NuGet-Paket einfach verwenden.

```bash
dotnet add package Aspire.Hosting.Valkey
```

Darüber hinaus werden zahlreiche weitere integrierte Hostings angeboten, die Sie [hier](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview) einsehen können.
Kehren wir nun zu Valkey zurück: Öffnen Sie im AppHost-Projekt die Datei Program.cs und passen Sie sie wie folgt an.

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddValkey("cache")
    .WithDataVolume(isReadOnly: false)
    .WithPersistence(interval: TimeSpan.FromMinutes(5),
        keysChangedThreshold: 100);

builder.Build().Run();
```

`cache` ist eine Implementierung der Schnittstelle `IResourceBuilder`, die die Informationen enthält, mit denen ein Valkey-Dienst erstellt werden kann.
`WithDataVolume` erstellt ein Volume zur Speicherung der Cache-Daten, und `WithPersistence` sorgt dafür, dass die Cache-Daten kontinuierlich gespeichert werden.
Betrachtet man dies bis hierhin, scheint es eine ähnliche Funktion wie `volumes` in `docker-compose` zu erfüllen.
Selbstverständlich können auch Sie dies ohne Schwierigkeiten umsetzen.
Da dies jedoch über den Rahmen dieses Beitrags hinausgeht, werde ich nicht weiter darauf eingehen.

### Erstellen eines Go-Echo-Servers

Nun fügen wir einen einfachen Go-Server hinzu.
Erstellen Sie zunächst im Solution-Ordner mit `go work init` einen Workspace.
Für .NET-Entwickler kann man den Go-Workspace etwa als Entsprechung zu einer Solution betrachten.

Erstellen Sie anschließend einen Ordner namens EchoServer, wechseln Sie hinein und führen Sie `go mod init EchoServer` aus.
Mit diesem Befehl wird ein Go-Modul erstellt. Für .NET-Entwickler kann man ein Modul etwa als Entsprechung zu einem Projekt betrachten.
Erstellen Sie danach die Datei `main.go` und schreiben Sie Folgendes.

```go
package main

import (
	"log"
	"net/http"
	"os"
)

func main() {
	addr := os.Getenv("PORT")
	log.Printf("Server started on %s", addr)

	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		name := request.URL.Query().Get("name")
		writer.Write([]byte("Hello, " + name))
	})

	http.ListenAndServe(":"+addr, nil)
}
```

Dieser Server liest, sofern das Aspire AppHost beim Start die Umgebungsvariable `PORT` injiziert, den darin angegebenen Port und startet den Server auf diesem.
Es handelt sich um einen einfachen Server, der den Query-Parameter `name` entgegennimmt und `Hello, {name}` zurückgibt.

Nun fügen wir diesen Server zu dotnet aspire hinzu.

### Hinzufügen des Echo-Servers zu Aspire

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

```bash
dotnet add package CommunityToolkit.Aspire.Hosting.Golang
```

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

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddValkey("cache")
    .WithDataVolume(isReadOnly: false)
    .WithPersistence(interval: TimeSpan.FromMinutes(5),
        keysChangedThreshold: 100);

var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
    .WithHttpEndpoint(port: 3000, env: "PORT")
    .WithExternalHttpEndpoints();

builder.Build().Run();
```

Hier ist `echoServer` eine Implementierung der Schnittstelle `IResourceBuilder`, die die Informationen enthält, mit denen ein Go-Server erstellt werden kann.
Die soeben hinzugefügte Methode `AddGolangApp` ist eine Erweiterungsmethode eines benutzerdefinierten Hosts, um einen Go-Server hinzuzufügen.
Sie verwendet fest den Port 3000 und injiziert die Umgebungsvariable `PORT`, wie Sie sehen können.
Schließlich ermöglicht `WithExternalHttpEndpoints` den Zugriff von außen.

Zum Testen rufen Sie `http://localhost:3000/?name=world` auf, und Sie sollten die Ausgabe `Hello, world` sehen.

Allerdings gibt es in der aktuellen Version von dotnet aspire einen erheblichen Nachteil für Nicht-.NET-Projekte.
Dieser besteht darin, dass...

## Erweiterung des Projekts

### Wie funktioniert dann die horizontale Skalierung?

Derzeit stellt dotnet aspire die Option `WithReplica` nur für Builder von .NET-Projekten bereit, die über die Methode `AddProject` hinzugefügt wurden.
Für Go-Hosts oder externe Projekte wie `AddContainer` wird diese Option jedoch nicht angeboten.

Daher muss man einen separaten Load Balancer oder Reverse Proxy einsetzen und die Lösung selbst implementieren.
Da dieser Reverse Proxy jedoch zu einem SPOF werden kann, empfiehlt es sich, dass der Reverse Proxy die Option `WithReplica` unterstützt.
Daraus folgt zwangsläufig, dass der Reverse Proxy ein .NET-Projekt sein muss.

Bisher habe ich für solche Probleme Ansätze wie nginx, Traefik oder eine eigene Implementierung verwendet. Sobald jedoch die Einschränkung auf .NET-Projekte gilt, hatte ich dafür unmittelbar keine Lösung.
Daher habe ich nach einem in .NET implementierten Reverse Proxy gesucht und glücklicherweise die Option [YARP](https://microsoft.github.io/reverse-proxy/) gefunden.
YARP ist ein in .NET implementierter Reverse Proxy, der auch die Rolle eines Load Balancers übernehmen kann und zahlreiche Funktionen bietet. Daher habe ich es als gute Wahl beurteilt.

Nun fügen wir YARP hinzu.

### Aufbau eines Reverse Proxys mit YARP

Erstellen Sie zunächst ein Projekt zur Verwendung von YARP.

```bash
dotnet new web -n ReverseProxy
```

Wechseln Sie anschließend in das Projekt und installieren Sie YARP.

```bash
dotnet add package Yarp.ReverseProxy --version 2.2.0
```

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

```csharp
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

var routes = new List<RouteConfig>();
var clusters = new List<ClusterConfig>();

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

var app = builder.Build();

app.MapReverseProxy();
app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");
```

Dieser Code ist der grundlegende Code zur Verwendung von YARP.
`routes` enthält die Routeninformationen, die der Reverse Proxy verwendet, und `clusters` enthält die Clusterinformationen, die der Reverse Proxy verwendet.
Diese Informationen werden mit der Methode `LoadFromMemory` in den Reverse Proxy geladen.
Abschließend wird mit der Methode `MapReverseProxy` der Reverse Proxy zugeordnet und ausgeführt.

Für den praktischen Einsatz fügen Sie im Aspire-AppHost-Projekt das Reverse-Proxy-Projekt als Referenz hinzu und ergänzen bzw. ändern Sie die Datei Program.cs wie folgt.

```bash
dotnet add reference ../ReverseProxy
```

```csharp
var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
    .WithHttpEndpoint(env: "PORT");

var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
    .WithReference(echoServer)
    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
    .WithExternalHttpEndpoints();
```

Nun kann der Reverse Proxy auf den Echo-Server verweisen.
Die Struktur wurde so geändert, dass eingehende externe Anfragen vom Reverse Proxy entgegengenommen und an den Echo-Server weitergeleitet werden.

### Anpassung des Reverse Proxys

Zunächst muss die Listening-Adresse des dem Reverse Proxy zugewiesenen Projekts geändert werden.
Entfernen Sie die `applicationUrl` in der Datei `Properties/launchSettings.json`.
Öffnen Sie anschließend die Datei Program.cs und passen Sie sie wie folgt umfassend an.

```csharp
using Yarp.ReverseProxy.Configuration;

var builder = WebApplication.CreateBuilder(args);

var routes = new List<RouteConfig>
{
    new RouteConfig
    {
        ClusterId = "cluster-echo",
        RouteId = "route-echo",
        Match = new RouteMatch
        {
            Path = "/"
        }
    }
};

var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";

var clusters = new List<ClusterConfig>
{
    new ClusterConfig
    {
        ClusterId = "cluster-echo",
        Destinations = new Dictionary<string, DestinationConfig>
        {
            { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
        }
    }
};

builder.Services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);

var app = builder.Build();

app.MapReverseProxy();
app.Run(url: $"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "5000"}");
```

Zunächst werden die Angaben zu `routes` und `clusters` angepasst.
Jeweils werden `echo-route` und `echo-cluster` hinzugefügt, sodass Anfragen an den Echo-Server weitergeleitet werden.
Anschließend wird die Adresse des Echo-Servers aus der Umgebungsvariable gelesen und verwendet.

Die Regel für diese Adresse lautet `services__{service-name}__http__{index}`.
Beim Echo-Server lautet der Dienstname `echo-server`, und da es sich um eine einzelne Instanz handelt, wird der Index `0` verwendet.
Falls ein ASP.NET-Core-Server hinzugefügt wird, können über `WithReplica` mehrere Instanzen erzeugt werden; in diesem Fall kann der Index entsprechend erhöht werden.
Der als Ausnahmebehandlung hinterlegte Wert `http://localhost:8080` ist ein bedeutungsloser Platzhalter.

Starten Sie nun das Projekt und rufen Sie `http://localhost:3000/?name=world` auf. Sie sollten weiterhin die Ausgabe `Hello, world` sehen.

### Ideen zur Erweiterung

Nun haben wir gesehen, wie ein Go-Server zu dotnet aspire hinzugefügt wird und Anfragen über einen Reverse Proxy weitergeleitet werden.
Anschließend lässt sich dieser Prozess programmatisch erweitern.
Beispielsweise kann man für den Echo-Server Instanzen mit fortlaufender Nummerierung im Dienstnamen erzeugen und die Konfiguration des Reverse Proxys automatisch hinzufügen.

Passen Sie den Code in der Datei Program.cs des Aspire-AppHost-Projekts, der den Reverse Proxy und den Echo-Server verwendet, wie folgt an.

```csharp
var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
    .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
    .WithExternalHttpEndpoints();

for (var i = 0; i < 8; i++)
{
    var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
        .WithHttpEndpoint(env: "PORT");
    reverseProxy.WithReference(echoServer);
}
```

Passen Sie anschließend die Datei Program.cs des Reverse-Proxy-Projekts wie folgt an.

```csharp
var echoServerDestinations = new Dictionary<string, DestinationConfig>();
for (var i = 0; i < 8; i++)
{
    echoServerDestinations[$"destination-{i}"] = new DestinationConfig
    {
        Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
    };
}

var clusters = new List<ClusterConfig>
{
    new ClusterConfig
    {
        ClusterId = "cluster-echo",
        Destinations = echoServerDestinations
    }
};
```

Für die auf 8 Stück erweiterten Echo-Server-Instanzen werden Zielkonfigurationen hinzugefügt.
Der Reverse Proxy verfügt nun über die Zielinformationen aller erweiterten Echo-Server und kann Anfragen entsprechend weiterleiten.
Rufen Sie wie zuvor `http://localhost:3000/?name=world` auf, und Sie sollten weiterhin die Ausgabe `Hello, world` sehen.

## Schlussbemerkung

In diesem Beitrag wurde beschrieben, wie ein Go-Server zu dotnet aspire hinzugefügt und Anfragen über einen Reverse Proxy weitergeleitet werden.
Die Erweiterung haben wir allerdings noch nicht vollständig ausgearbeitet. Ein Beispiel, das die Umsetzung über Umgebungsvariablen noch stärker programmatisch gestaltet, habe ich in einem separaten Repository abgelegt.
Details zur Projektstruktur und zum Code finden Sie unter [snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack).

Persönlich erwarte ich, dass dotnet aspire als Alternative zu Docker Compose sowie als Werkzeug für Cloud-Deployments eine eigene Rolle übernehmen kann.
Da bereits Generatoren existieren, die Docker-Compose- oder Kubernetes-Manifeste erzeugen, meine ich, dass der Zugang normaler Entwickler zu Infrastrukturwerkzeugen dadurch bereits leichter geworden ist.
