---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Kjøre en skalerbar Go-server med .NET Aspire
description: 'Slik kjører du en Go-server skalerbart med .NET Aspire: eksempel på horisontal skalering og automatisert konfigurasjon med YARP-reverseproxy.'
language: "no"
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 verktøy som ble utviklet for å hjelpe utviklere med utvikling og konfigurasjon av skybaserte (cloud native) løsninger, i takt med at antallet skybaserte miljøer øker. Verktøyet gjør det enklere for .NET-utviklere å distribuere .NET-prosjekter og ulik cloud native-infrastruktur, samt tjenester skrevet på andre språk eller containere.

Det er selvsagt at løsninger lanseres og kjøres fra Docker til Kubernetes (k8s). Mange sektorer, bransjer og utviklere flytter fra tradisjonelle lokale (on-premises) miljøer til skybaserte miljøer, og mange har allerede gjort det. Dette er nå et modent område. Derfor antar jeg at det ikke er nødvendig å forklare de eksisterende utfordringene knyttet til vertsnavn, portkonfigurasjon, brannmurer, metrikkhåndtering og lignende.

Med bare forklaringene ovenfor er det sannsynlig at du fortsatt ikke har en god forståelse av hva dotnet aspire er. Dette skyldes at Microsoft heller ikke gir en presis definisjon av det. Derfor vil heller ikke jeg gi noen egen definisjon. I stedet vil jeg bruke de grunnleggende funksjonene i dotnet aspire slik jeg har forstått dem i denne artikkelen, og du kan bruke dette som utgangspunkt for å danne din egen oppfatning.

## Prosjektoppsett

### Opprette et dotnet aspire-prosjekt

Dersom dotnet aspire-malen ikke er tilgjengelig, må du først installere malen. Installer malen med følgende kommando. Dersom .NET ikke er installert, må du installere det selv.

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

Deretter oppretter du en ny løsning (solution) i en passende mappe.

```bash
dotnet new sln
```

Kjør så følgende kommando i løsningsmappen for å opprette et prosjekt basert på malen aspire-apphost.

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

Da opprettes et aspire-apphost-prosjekt som kun inneholder enkel kode for oppsett.

### Legge til Valkey

La oss nå legge til Valkey på en enkel måte.

Før vi legger den til, bør du vite at dotnet aspire gjennom community hosting tilbyr ulike tredjepartsløsninger.
Valkey støttes også av community hosting, og den kan enkelt tas i bruk med følgende NuGet-pakke.

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

I tillegg finnes det flere integrerte hostingløsninger, som du kan se [her](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview).
Tilbake til Valkey: Åpne filen Program.cs i AppHost-prosjektet og endre den slik:

```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 implementasjon av grensesnittet `IResourceBuilder`, som inneholder informasjon for å bygge en Valkey-tjeneste.
`WithDataVolume` oppretter et volum for lagring av cachedata, og `WithPersistence` sørger for at cachedata lagres løpende.
Sett fra dette perspektivet ser det ut til å ha en lignende rolle som `volumes` i `docker-compose`.
Dette er selvsagt noe dere også kan lage uten store vanskeligheter.
Men siden det ligger utenfor rammene for denne artikkelen, vil jeg ikke gå nærmere inn på det nå.

### Opprette en Go-ekkoserver

La oss deretter legge til en enkel Go-server.
Først oppretter du et Go-workspace i løsningsmappen med `go work init`.
For .NET-utviklere kan man se på et Go-workspace som noe tilsvarende en løsning.

Deretter oppretter du en mappe kalt EchoServer, går inn i den og kjører `go mod init EchoServer`.
Denne kommandoen oppretter en Go-modul. For .NET-utviklere kan en modul forstås som noe tilsvarende et prosjekt.
Deretter oppretter du filen `main.go` og skriver 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 serveren leser porten fra miljøvariabelen `PORT`, som Aspire AppHost injiserer når den kjøres, og starter serveren på denne porten.
Den er en enkel server som tar imot spørringsparameteren `name` og returnerer `Hello, {name}`.

Nå skal vi legge denne serveren til i dotnet aspire.

### Legge til ekkoserveren i aspire

Gå tilbake til AppHost-prosjektet der Valkey ble lagt til, og legg til community hosting for Go:

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

Åpne deretter Program.cs og legg til følgende kode:

```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 implementasjon av grensesnittet `IResourceBuilder`, som inneholder informasjon for å bygge Go-serveren.
Metoden `AddGolangApp` som vi nettopp la til, er en extension-metode for å legge til en Go-server via en egendefinert host.
Den bruker fast port 3000, og vi ser at miljøvariabelen `PORT` injiseres.
Til slutt sørger `WithExternalHttpEndpoints` for at serveren kan nås utenfra.

For å teste kan du åpne `http://localhost:3000/?name=world`, og du vil se at `Hello, world` skrives ut.

Men i dotnet aspire finnes det for øyeblikket en tung ulempe for prosjekter som ikke er .NET-prosjekter.
Den er nemlig...

## Utvidelse av prosjektet

### Hvordan gjør man horisontal skalering?

For øyeblikket tilbyr dotnet aspire alternativet `WithReplica` kun for builderen til .NET-prosjekter som er lagt til med metoden `AddProject`.
For Go-verter og eksterne prosjekter, slik som de som legges til med `AddContainer`, tilbys ikke dette alternativet.

Derfor må man selv implementere dette med en egen load balancer eller reverse proxy.
Men da kan reverse proxyen bli et SPOF (single point of failure), og det er derfor en god idé at reverse proxyen støtter `WithReplica`.
Da må reverse proxyen nødvendigvis være et .NET-prosjekt.

Jeg har tidligere forsøkt å løse dette med nginx, Traefik og egne implementasjoner, men når kravet om .NET-prosjekt settes, fantes det ingen umiddelbar løsning for meg.
Derfor så jeg etter reverse proxyer implementert i .NET, og heldigvis fant jeg [YARP](https://microsoft.github.io/reverse-proxy/).
YARP er en reverse proxy implementert i .NET. Den kan også fungere som load balancer og tilbyr mange funksjoner, så jeg vurderte den som et godt valg.

La oss nå legge til YARP.

### Oppsett av reverse proxy med YARP

Først oppretter du et prosjekt som skal bruke YARP:

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

Deretter går du inn i prosjektet og installerer YARP:

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

Når installasjonen er ferdig, åpner 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 koden er et grunnleggende oppsett for bruk av YARP.
`routes` inneholder rutinginformasjonen som reverse proxyen skal bruke, og `clusters` inneholder klyngeinformasjonen som reverse proxyen skal bruke.
Denne informasjonen lastes inn i reverse proxyen med metoden `LoadFromMemory`.
Til slutt registreres og kjøres reverse proxyen med `MapReverseProxy`.

For faktisk bruk legger du til reverse proxy-prosjektet som referanse i AppHost-prosjektet i aspire, og legger til eller endrer følgende 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();
```

Nå kan reverse proxyen referere til ekkoserveren.
Innkommende forespørsler fra eksterne kilder mottas av reverse proxyen og videresendes til ekkoserveren.

### Endring av reverse proxy

Først må du endre listening-adressen for prosjektet som er tildelt reverse proxyen.
Fjern `applicationUrl` i filen `Properties/launchSettings.json`.
Deretter åpner du Program.cs og endrer den omfattende slik:

```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 endrer du informasjonen om `routes` og `clusters`.
Du legger til henholdsvis `echo-route` og `echo-cluster`, slik at forespørsler sendes til ekkoserveren.
Deretter endrer du koden slik at adressen til ekkoserveren leses fra miljøvariabler.

Regelen for denne adressen er `services__{service-name}__http__{index}`.
For ekkoserveren er tjenestenavnet `echo-server`, og siden det kun finnes én instans, brukes `0` som indeks.
Dersom du legger til en ASP.NET Core-server, kan flere instanser opprettes med `WithReplica`, og da økes indeksen for å bruke dem.
Verdien `http://localhost:8080` i unntakshåndteringen er en meningsløs standardverdi.

Nå kan du kjøre prosjektet og åpne `http://localhost:3000/?name=world`, og du vil fortsatt se at `Hello, world` skrives ut.

### Utvidelsesidéer

Nå har vi lagt til en Go-server i dotnet aspire og bekreftet at forespørsler videresendes via reverse proxyen.
Da kan vi utvide prosessen slik at den kan implementeres programmatisk.
For eksempel kan vi legge til løpenummer etter tjenestenavnet for ekkoserveren for å opprette flere instanser, og automatisk legge til konfigurasjon for reverse proxyen.

Endre koden i Program.cs i aspire AppHost-prosjektet slik at den bruker reverse proxyen og ekkoserveren:

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

Deretter endrer du Program.cs i reverse proxy-prosjektet slik:

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

Du legger til målkonfigurasjon for de åtte ekkoserverinstansene.
Reverse proxyen har nå målinformasjon for alle ekkoserverne og kan videresende forespørsler til dem.
Når du åpner `http://localhost:3000/?name=world`, vil du fortsatt se at `Hello, world` skrives ut.

## Avslutning

I denne artikkelen har vi beskrevet hvordan man legger til en Go-server i dotnet aspire og videresender forespørsler via en reverse proxy.
Utvidelsen er imidlertid ikke fullstendig dokumentert ennå. Jeg har laget et eksempel i et eget repository som viser hvordan dette kan implementeres mer programmatisk ved hjelp av miljøvariabler.
For detaljert prosjektoppsett og kode, se [snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack).

Personlig tror jeg at dotnet aspire kan spille sin egen rolle både som et alternativ til docker compose og som et verktøy for skyutrulling.
Det finnes allerede [generatorer](https://prom3theu5.github.io/aspirational-manifests/generate-command.html) som lager docker compose- eller k8s-manifester, og jeg mener dette har gjort infrastrukturverktøy mer tilgjengelige for vanlige utviklere.
