---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Forsøg på at køre en skalerbar Go-server med dotnet Aspire
description: 'Sådan kører du en Go-server skalerbart med .NET Aspire: eksempel på vandret skalering og automatiseret konfiguration med YARP reverse proxy.'
language: da
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 er et værktøj, der er blevet udviklet for at hjælpe udviklere med cloud-native udvikling og konfiguration, i takt med at cloud-native miljøer har vokset sig større. Værktøjet gør det muligt for .NET-udviklere nemt at udrulle .NET-projekter, forskellige cloud-native infrastrukturer, samt tjenester skrevet i andre sprog eller containere og lignende.

Naturligvis er det lanceret og drevet fra docker til k8s, og inden for mange områder, brancher og blandt mange udviklere har man i høj grad flyttet, og er allerede flyttet, fra traditionelle on-premises miljøer til cloud-native miljøer. Det er nu et modent område. Derfor forventer jeg, at der ikke er behov for at forklare de tidligere ulemper ved værtsnavne, portkonfiguration, firewalls, metrikstyring og lignende.

Ud fra ovenstående forklaringer vil du sikkert ikke have nogen idé om, hvad dotnet aspire egentlig er. Det skyldes, at Microsoft heller ikke har givet en præcis definition. Derfor vil jeg heller ikke give nogen særlig definition. I stedet vil jeg i denne artikel anvende de grundlæggende funktioner i dotnet aspire, som jeg har forstået dem, så du kan bruge dem som reference og selv finde din egen position.

## Projektopsætning

### Oprettelse af et dotnet aspire-projekt

Hvis dotnet aspire-skabelonen ikke findes, skal du først installere skabelonen. Installer skabelonen med følgende kommando. Hvis du ikke har .NET, skal du selv installere det.

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

Opret derefter en ny solution i en passende mappe.

```bash
dotnet new sln
```

Kør derefter følgende kommando i solution-mappen for at oprette et projekt baseret på skabelonen aspire-apphost.

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

Herefter oprettes et aspire-apphost-projekt, der kun indeholder en simpel kode til opsætningen.

### Tilføjelse af Valkey

Lad os nu tilføje Valkey på en enkel måde.

Før vi tilføjer den, skal det nævnes, at dotnet aspire via community hosting tilbyder forskellige tredjepartsløsninger.  
Valkey kan naturligvis også drage fordel af denne community hosting, og den kan nemt tages i brug via følgende NuGet-pakke.

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

Der findes desuden flere integrerede hostingmuligheder, som du kan se [her](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview).  
Lad os vende tilbage til Valkey. Åbn filen Program.cs i AppHost-projektet og rediger den som følger.

```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` er en implementering af grænsefladen `IResourceBuilder`, som indeholder oplysningerne til at bygge en Valkey-tjeneste.  
`WithDataVolume` opretter et volumen til lagring af cachedata, mens `WithPersistence` sørger for, at cachedata lagres løbende.  
Ud fra dette ser det ud til, at den har en lignende rolle som `volumes` i `docker-compose`.  
Naturligvis kan du også let oprette dette selv.  
Det ligger dog uden for denne artikels rammer, så vi vil ikke gennemgå det her.

### Oprettelse af en Go-ekkoserver

Lad os nu tilføje en simpel Go-server.  
Først opretter du et workspace i solution-mappen med `go work init`.  
For .NET-udviklere kan man betragte et Go-workspace som noget, der minder om en solution.

Opret derefter en mappe ved navn EchoServer, gå ind i den og kør `go mod init EchoServer`.  
Med denne kommando oprettes et Go-modul. For .NET-udviklere kan et modul betragtes som noget, der minder om et projekt.  
Opret derefter filen `main.go` og skriv følgende.

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

Denne server læser portnummeret fra miljøvariablen `PORT`, som Aspire AppHost injicerer, når den kører, hvorefter den starter serveren på den pågældende port.  
Det er en simpel server, der modtager forespørgselsparameteren `name` og returnerer `Hello, {name}`.

Lad os nu tilføje denne server til dotnet aspire.

### Tilføjelse af ekkoserveren til aspire

Gå tilbage til Aspire AppHost-projektet, hvor vi tilføjede Valkey, og tilføj community hosting for Go.

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

Åbn derefter Program.cs og tilføj følgende udtryk.

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

Her er `echoServer` en implementering af grænsefladen `IResourceBuilder`, som indeholder oplysningerne til at bygge Go-serveren.  
Den nyligt tilføjede metode `AddGolangApp` er en extension method på den brugerdefinerede host, der tilføjer en Go-server.  
Den bruger fast port 3000, og det kan ses, at miljøvariablen `PORT` injiceres.  
Endelig gør `WithExternalHttpEndpoints` det muligt at tilgå serveren udefra.

Når du tester ved at åbne `http://localhost:3000/?name=world`, vil du kunne se, at `Hello, world` udskrives.

Men i den nuværende version af dotnet aspire er der en tung straf for ikke-dotnet-projekter.  
Den er netop...

## Udvidelse af projektet

### Hvordan gør man så med horisontal skalering?

Den nuværende version af dotnet aspire tilbyder kun indstillingen `WithReplica` for builderen af .NET-projekter, der er tilføjet med metoden `AddProject`.  
For Go-hosts eller eksterne projekter som `AddContainer` tilbydes denne indstilling dog ikke.

Derfor må man selv implementere det med en separat load balancer eller reverse proxy.  
Men da denne reverse proxy så kan blive et SPOF, er det en god idé, at reverse proxy understøtter indstillingen `WithReplica`.  
Så må reverse proxy nødvendigvis være et .NET-projekt.

Indtil nu har jeg forsøgt at løse dette med nginx, Traefik, egen implementering og lignende, men når begrænsningen om .NET-projekt indføres, havde jeg på stedet ingen løsning.  
Derfor ledte jeg efter en reverse proxy implementeret i .NET, og heldigvis fandtes der muligheden [YARP](https://microsoft.github.io/reverse-proxy/).  
YARP er en reverse proxy implementeret i .NET, der også kan fungere som load balancer og tilbyder mange funktioner, så jeg vurderede, at det var et godt valg.

Lad os nu tilføje YARP.

### Opsætning af reverse proxy med YARP

Først opretter du et projekt til brug med YARP.

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

Gå derefter til projektet og installer YARP.

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

Når installationen er færdig, åbner du Program.cs og skriver følgende.

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

Denne kode er den grundlæggende kode til brug af YARP.  
`routes` indeholder routinginformation, som reverse proxy skal bruge, og `clusters` indeholder klyngeinformation, som reverse proxy skal bruge.  
Disse oplysninger indlæses i reverse proxy med metoden `LoadFromMemory`.  
Til sidst mappes og køres reverse proxy med metoden `MapReverseProxy`.

For at bruge det i praksis skal du tilføje reverse proxy-projektet som reference i aspire apphost-projektet og tilføje eller ændre følgende udtryk i Program.cs.

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

Nu kan reverse proxy referere til echo-serveren.  
Den struktur, hvor eksterne anmodninger modtages af reverse proxy og videregives til echo-serveren, er nu under etablering.

### Ændring af reverse proxy

Først skal du ændre den lytteadresse, der er tildelt reverse proxy-projektet.  
Fjern `applicationUrl` i filen `Properties/launchSettings.json`.  
Åbn derefter Program.cs og ret den grundlæggende kode væsentligt som vist nedenfor.

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

Først ændrer vi oplysningerne om `routes` og `clusters`.  
Vi tilføjer henholdsvis `echo-route` og `echo-cluster` og konfigurerer dem, så anmodninger sendes til echo-serveren.  
Derudover ændres koden, så adressen til echo-serveren læses fra miljøvariablerne og anvendes.

Reglen for denne adresse er `services__{service-name}__http__{index}`.  
For echo-serveren er tjenestenavnet `echo-server`, og da der kun er én instans, bruges indekset `0`.  
Hvis du tilføjer en asp.net core-server, kan flere instanser oprettes via `WithReplica`, og du kan så bruge indekset ved at øge det.  
Den fallback-værdi `http://localhost:8080`, der er angivet som undtagelseshåndtering, er en meningsløs værdi.

Når du nu kører projektet og åbner `http://localhost:3000/?name=world`, vil du stadig kunne se, at `Hello, world` udskrives.

### Ideer til udvidelse

Nu har vi bekræftet, at en Go-server kan tilføjes til dotnet aspire, og at anmodninger kan videresendes via reverse proxy.  
Derefter kan vi udvide processen, så den implementeres programmatisk.  
For eksempel kan vi tilføje nummerering efter tjenestenavnet for echo-serveren for at oprette flere instanser og automatisk tilføje konfigurationen til reverse proxy.

Ret koden i Program.cs i aspire apphost-projektet, så den bruger reverse proxy og echo-serveren, som vist nedenfor.

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

Derefter ændrer du Program.cs i reverse proxy-projektet som følger.

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

Vi tilføjer destinationskonfigurationen for de 8 echo-serverinstanser.  
Nu har reverse proxy destinationsoplysninger for de udvidede echo-servere og kan videresende anmodninger til dem.  
Når du åbner det tidligere `http://localhost:3000/?name=world`, vil du stadig kunne se, at `Hello, world` udskrives.

## Afslutning

I denne artikel har jeg beskrevet processen med at tilføje en Go-server til dotnet aspire og videresende anmodninger via reverse proxy.  
Dog har jeg endnu ikke skrevet alt om udvidelsen, men jeg har lagt et eksempel i et separat repository, der viser, hvordan det kan implementeres mere programmatisk ved hjælp af miljøvariabler.  
Se [snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack) for detaljer om projektopsætningen og koden.

Personligt forventer jeg, at dotnet aspire kan spille sin egen rolle både som alternativ til docker compose og som værktøj til cloud-udrulning.  
Der findes allerede [generatorer](https://prom3theu5.github.io/aspirational-manifests/generate-command.html), der opretter docker compose- eller k8s manifests, så jeg synes, at det er blevet lettere for almindelige udviklere at få adgang til infrastrukturværktøjer.
