---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Uruchamianie skalowalnego serwera Go w .NET Aspire
description: 'Jak uruchamiać serwery Go w sposób skalowalny za pomocą .NET Aspire: przykład konfiguracji skalowania poziomego i automatyzacji z wykorzystaniem odwrotnego proxy YARP.'
language: pl
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 to narzędzie stworzone, aby pomagać deweloperom w rozwoju i konfiguracji aplikacji chmurowych w miarę jak środowiska cloud native się rozrastają. To narzędzie umożliwia deweloperom .NET łatwe wdrażanie projektów .NET, różnorodnej infrastruktury cloud native, a także usług napisanych w innych językach lub kontenerów.

Oczywiście, od Dockera aż po K8s, technologie te są wdrażane i eksploatowane, a wiele dziedzin, branż i deweloperów przenosi się z tradycyjnych środowisk on-premise do środowisk cloud native, i większość z nich już to zrobiła. Ten obszar jest już dojrzały. Dlatego uważam, że nie ma potrzeby wyjaśniać dotychczasowych niedogodności związanych z nazwami hostów, konfiguracją portów, zaporami sieciowymi czy zarządzaniem metrykami.

Dlatego już na podstawie powyższych wyjaśnień może być Pan/Pani zupełnie nie wiedzieć, czym właściwie jest dotnet aspire. Dzieje się tak, ponieważ Microsoft nie podaje dokładnej definicji tego narzędzia. Dlatego ja również nie będę podawać własnej definicji. Zamiast tego, w tym artykule wykorzystam podstawowe funkcje dotnet aspire, tak jak je rozumiem, więc może Pan/Pani, korzystając z tego, wyrobić sobie własne zdanie.

## Konfiguracja projektu

### Tworzenie projektu dotnet aspire

Jeśli szablon dotnet aspire nie jest zainstalowany, należy najpierw zainstalować szablon. Szablon instalujemy następującym poleceniem. Jeśli nie ma Pan/Pani zainstalowanego .NET, prosimy o samodzielną instalację.

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

Następnie w odpowiednim folderze tworzymy nowe rozwiązanie.

```bash
dotnet new sln
```

Potem, w folderze rozwiązania, uruchamiamy poniższe polecenie, aby utworzyć projekt na podstawie szablonu aspire-apphost.

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

W ten sposób zostanie utworzony projekt aspire-apphost zawierający jedynie prosty kod konfiguracyjny.

### Dodawanie Valkey

Teraz w prosty sposób dodamy Valkey.

Zanim jednak to zrobimy, warto wiedzieć, że dotnet aspire udostępnia różnorodne rozwiązania firm trzecich poprzez community hosting. Oczywiście Valkey również może korzystać z tego wsparcia społeczności, a łatwo go używać dzięki poniższemu pakietowi NuGet.

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

Poza tym dostępne są różne zintegrowane hostingi, które można sprawdzić [tutaj](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview).  
Wracając do Valkey, w projekcie AppHost otwieramy plik Program.cs i modyfikujemy go w następujący sposób.

```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` jest implementacją interfejsu `IResourceBuilder`, która zawiera informacje potrzebne do zbudowania usługi Valkey.  
`WithDataVolume` tworzy wolumen do przechowywania danych pamięci podręcznej, a `WithPersistence` pozwala na ciągły zapis danych pamięci podręcznej.  
Patrząc na to, wydaje się, że pełni on podobną rolę jak `volumes` w `docker-compose`.  
Oczywiście Państwo również mogą to zrobić bez trudu.  
Jednak wykracza to poza zakres tego artykułu, dlatego nie będziemy o tym teraz mówić.

### Tworzenie prostego serwera echo w Go

Teraz dodamy prosty serwer w języku Go.  
Najpierw w folderze rozwiązania tworzymy workspace poleceniem `go work init`.  
Dla deweloperów .NET workspace Go można traktować jako odpowiednik rozwiązania.

Następnie tworzymy folder EchoServer, przechodzimy do niego i uruchamiamy `go mod init EchoServer`.  
To polecenie tworzy moduł Go. Dla deweloperów .NET moduł można traktować jako odpowiednik projektu.  
Następnie tworzymy plik `main.go` i piszemy w nim następujący kod.

```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)
}
```

Ten serwer, gdy AppHost Aspire jest uruchamiany i wstrzykuje zmienną środowiskową `PORT`, na której serwer ma nasłuchiwać, odczytuje ten port i uruchamia serwer.  
Jest to prosty serwer, który odbiera parametr zapytania `name` i zwraca `Hello, {name}`.

Teraz dodamy ten serwer do dotnet aspire.

### Dodawanie serwera echo do aspire

Wracamy do projektu Aspire AppHost, w którym dodaliśmy Valkey, i instalujemy community hosting dla języka Go.

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

Następnie otwieramy plik Program.cs i dodajemy poniższą instrukcję.

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

Tutaj `echoServer` jest implementacją interfejsu `IResourceBuilder`, która zawiera informacje potrzebne do zbudowania serwera w Go.  
Dodana przed chwilą metoda `AddGolangApp` jest metodą rozszerzającą niestandardowego hosta, służącą do dodawania serwera w Go.  
Serwer używa stałego portu 3000 i, jak widać, wstrzykuje zmienną środowiskową `PORT`.  
Na koniec `WithExternalHttpEndpoints` umożliwia dostęp z zewnątrz.

Aby przetestować, proszę otworzyć `http://localhost:3000/?name=world`, a zobaczy Pan/Pani, że wyświetla się `Hello, world`.

Jednak obecnie dotnet aspire nakłada na projekty niebędące projektami .NET znaczną karę.  
Jest nią właśnie...

## Skalowanie projektu

### A jak zatem wykonać skalowanie poziome?

Obecnie dotnet aspire udostępnia opcję `WithReplica` wyłącznie dla projektów .NET dodanych metodą `AddProject`.  
Nie udostępnia jej jednak dla hostów Go ani zewnętrznych projektów, takich jak te dodane metodą `AddContainer`.

Dlatego trzeba to zaimplementować samodzielnie, używając osobnego load balancera lub reverse proxy.  
Jednak wtedy reverse proxy może stać się SPOF (Single Point of Failure), dlatego dobrze byłoby, aby reverse proxy również udostępniał opcję `WithReplica`.  
W takim przypadku reverse proxy musi być nieuchronnie projektem .NET.

Dotychczas do rozwiązania tego problemu używałem nginx, Traefika lub własnej implementacji, jednak gdy pojawia się ograniczenie do projektów .NET, nie miałem wtedy żadnego rozwiązania pod ręką.  
Dlatego poszukałem reverse proxy zaimplementowanego w .NET i, na szczęście, znalazłem [YARP](https://microsoft.github.io/reverse-proxy/).  
YARP to reverse proxy zaimplementowane w .NET, które może pełnić rolę load balancera i oferuje wiele funkcji, dlatego uznałem go za dobry wybór.

Teraz dodamy YARP.

### Konfiguracja reverse proxy z YARP

Najpierw tworzymy projekt, który będzie używał YARP.

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

Następnie przechodzimy do projektu i instalujemy YARP.

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

Po zakończeniu instalacji otwieramy plik Program.cs i piszemy następujący kod.

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

Ten kod to podstawowa konfiguracja potrzebna do korzystania z YARP.  
`routes` zawiera informacje o trasach, z których będzie korzystać reverse proxy, a `clusters` zawiera informacje o klastrach, z których będzie korzystać reverse proxy.  
Informacje te są ładowane do reverse proxy metodą `LoadFromMemory`.  
Na koniec metoda `MapReverseProxy` mapuje reverse proxy i je uruchamia.

Aby korzystać z tego w praktyce, w projekcie aspire apphost dodajemy referencję do projektu reverse proxy, a w pliku Program.cs dodajemy lub modyfikujemy następującą instrukcję.

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

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

### Modyfikacja reverse proxy

Na początek należy zmienić adres nasłuchiwania projektu przypisanego do reverse proxy.  
Usuwamy `applicationUrl` z pliku `Properties/launchSettings.json`.  
Następnie otwieramy plik Program.cs i wprowadzamy poniższe, znaczące zmiany.

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

Najpierw modyfikujemy informacje o `routes` i `clusters`.  
Dodajemy odpowiednio `echo-route` i `echo-cluster`, aby żądania były kierowane do serwera echo.  
Następnie zmieniamy kod tak, aby adres serwera echo był odczytywany z zmiennych środowiskowych.

Reguła dla tego adresu to `services__{service-name}__http__{index}`.  
W przypadku serwera echo nazwa usługi to `echo-server`, a ponieważ jest to pojedyncza instancja, jako indeks używamy `0`.  
Jeśli dodamy serwer ASP.NET Core, za pomocą `WithReplica` można utworzyć wiele instancji, więc wystarczy zwiększać indeks.  
Wartość awaryjna `http://localhost:8080` jest bezsensownym, pozbawionym znaczenia ciągiem.

Teraz uruchamiamy projekt i otwieramy `http://localhost:3000/?name=world`, a zobaczy Pan/Pani, że nadal wyświetla się `Hello, world`.

### Pomysły na rozszerzenie

Dodaliśmy już serwer Go do dotnet aspire i potwierdziliśmy, że żądania są przekazywane przez reverse proxy.  
Teraz można rozszerzyć ten proces tak, aby był realizowany programowo.  
Na przykład, dla serwera echo można dodać numerację po nazwie usługi, tworząc wiele instancji, i automatycznie dodawać konfigurację do reverse proxy.

W pliku Program.cs projektu aspire apphost modyfikujemy kod korzystający z reverse proxy i serwera echo w następujący sposób.

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

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

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

Dodajemy konfigurację miejsc docelowych dla 8 instancji serwera echo.  
Teraz reverse proxy posiada informacje o miejscach docelowych rozszerzonych serwerów echo i może przekazywać do nich żądania.  
Po otwarciu `http://localhost:3000/?name=world` nadal zobaczy Pan/Pani wyświetlone `Hello, world`.

## Zakończenie

W tym artykule opisałem proces dodawania serwera Go do dotnet aspire oraz przekazywania żądań przez reverse proxy.  
Nie napisałem jeszcze wszystkiego dotyczącego rozszerzeń, a przykład, który pozwala na bardziej programowe podejście z użyciem zmiennych środowiskowych, umieściłem w osobnym repozytorium.  
Szczegółową strukturę projektu i kod proszę znaleźć w [snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack).

Osobiście oczekuję, że dotnet aspire może odegrać własną rolę jako alternatywa dla docker compose oraz jako narzędzie do wdrażania w chmurze.  
Istnieje już [generator](https://prom3theu5.github.io/aspirational-manifests/generate-command.html), który tworzy manifesty docker compose i k8s, dlatego uważam, że dostęp zwykłych deweloperów do narzędzi infrastrukturalnych stał się łatwiejszy.
