---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Running a Scalable Go Server with .NET Aspire
description: 'How to Run Go Servers Scalably with .NET Aspire: A Configuration Example for Horizontal Scaling and Automation Using the YARP Reverse Proxy.'
language: en
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 is a tool created to assist developers in cloud-native development and configuration as the cloud-native environment continues to grow. This tool enables .NET developers to easily deploy .NET projects, various cloud-native infrastructures, services written in other languages, and containers.

Naturally, as releases and operations range from Docker to Kubernetes (K8s), a significant number of fields, industries, and developers have migrated or are currently migrating from traditional on-premise environments to cloud-native environments. It is now a matured domain. Therefore, I believe there is no need to explain the traditional inconveniences associated with host names, port configurations, firewalls, and metric management.

Consequently, based on the descriptions above, you probably still cannot quite grasp what dotnet aspire actually is. This is because even Microsoft has not provided a precise definition for it. Therefore, I will not provide a definitive explanation either. However, since this article will utilize the fundamental functionalities of dotnet aspire as I understand them, you may refer to this and determine your own perspective.

## Project Configuration

### Creating a dotnet aspire Project

If the dotnet aspire templates are not installed, you must install them first. Install the templates using the following command. If you do not have .NET installed, please install it yourself.

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

Next, create a new solution in an appropriate folder.

```bash
dotnet new sln
```

Afterward, execute the following command within the solution folder to generate a project using the aspire-apphost template.

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

This generates an aspire-apphost project containing only simple code for initial setup.

### Adding Valkey

Let us now add Valkey simply.

Before adding it directly, dotnet aspire provides various third-party solutions through community hosting.  
Naturally, Valkey is also supported by this community hosting and can be easily utilized via the following NuGet package:

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

Various other integrated hosting options are provided, which can be checked [here](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview).  
Returning to Valkey, open the Program.cs file in the AppHost project and modify it as follows:

```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` is an implementation of the `IResourceBuilder` interface containing the information required to build the Valkey service.  
`WithDataVolume` creates a volume to store cache data, and `WithPersistence` enables persistent storage of the cache data.  
Looking at this so far, it appears to serve a role similar to `volumes` in `docker-compose`.  
Naturally, you can also construct this without difficulty.  
However, as this falls outside the scope of this article, it will not be discussed further at this time.

### Creating a Go Language Echo Server

Let us now add a simple Go language server.  
First, initialize a workspace in the solution folder by running `go work init`.  
For a .NET developer, a Go workspace can be understood as being similar to a solution.

Next, create a folder named EchoServer, navigate into it, and execute `go mod init EchoServer`.  
This command generates a Go module. You can understand a module as being similar to a project for a .NET developer.  
Then, create a `main.go` file and write the following code:

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

When the Aspire AppHost runs, this server reads the `PORT` environment variable injected into it and starts the server on that specified port.  
It is a simple server that receives a `name` query and returns `Hello, {name}`.

Let us now add this server to dotnet aspire.

### Adding the Echo Server to Aspire

Navigate back to the Aspire AppHost project where Valkey was added, and add the community hosting package for the Go language.

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

Then, open the Program.cs file and add the following statements:

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

Here, `echoServer` is an implementation of the `IResourceBuilder` interface containing the information required to build the Go language server.  
The `AddGolangApp` method just added is a custom host extension method for adding a Go language server.  
It uses a fixed port of 3000 and injects the `PORT` environment variable.  
Finally, `WithExternalHttpEndpoints` makes the service accessible from the outside.

For testing, if you access `http://localhost:3000/?name=world`, you will be able to confirm that `Hello, world` is printed.

However, there is currently a heavy penalty imposed on non-.NET projects in dotnet aspire.  
That is...

## Project Expansion

### How Then Do We Handle Horizontal Scaling?

Currently, dotnet aspire only provides the `WithReplica` option for builders of .NET projects added via the `AddProject` method.  
However, this option is not provided for external projects such as Go language hosts or `AddContainer`.

Consequently, one must implement a separate load balancer or reverse proxy manually.  
However, since such a reverse proxy could become a single point of failure (SPOF), it is advisable for the reverse proxy to support the `WithReplica` option.  
Thus, the reverse proxy inevitably needs to be a .NET project.

In the past, methods such as Nginx, Traefik, or custom implementations were used to address this issue, but given the restriction that it must be a .NET project, I had no immediate solution at hand.  
Therefore, I searched for a reverse proxy implemented in .NET, and fortunately, [YARP](https://microsoft.github.io/reverse-proxy/) was available as an option.  
YARP is a reverse proxy implemented in .NET that can also function as a load balancer and offers various features, leading me to conclude that it was an appropriate choice.

Let us now add YARP.

### Configuring a Reverse Proxy with YARP

First, create a project to utilize YARP.

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

Then, navigate to the project and install YARP.

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

Once the installation is complete, open the Program.cs file and write the following:

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

This code is the foundational setup for utilizing YARP.  
`routes` contains the route information that the reverse proxy will use, and `clusters` contains the cluster information for the reverse proxy.  
This information is loaded into the reverse proxy via the `LoadFromMemory` method.  
Finally, the `MapReverseProxy` method is used to map and execute the reverse proxy.

For practical use, add a reference to the reverse proxy project within the Aspire AppHost project, and add and modify the following statements in the Program.cs file:

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

Now the reverse proxy can reference the echo server.  
The architecture is being modified so that incoming external requests are received by the reverse proxy and forwarded to the echo server.

### Modifying the Reverse Proxy

First, we must change the listening address of the project assigned to the reverse proxy.  
Remove `applicationUrl` inside the `Properties/launchSettings.json` file.  
Then, open the Program.cs file and substantially modify it as shown below:

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

First, modify the information for `routes` and `clusters`.  
Configure them by adding `echo-route` and `echo-cluster` respectively, so that requests are routed to the echo server.  
Also, modify the code to read and use the address of the echo server from the environment variables.

The naming convention for this address is `services__{service-name}__http__{index}`.  
In the case of the echo server, the service name is `echo-server`, and since it is a single instance, the index `0` is used.  
If you were to add an ASP.NET Core server, multiple instances could be generated via `WithReplica`, in which case you would increment the index accordingly.  
The fallback value `http://localhost:8080` handled via exception logic is a meaningless placeholder value.

Now, if you run the project and access `http://localhost:3000/?name=world`, you will be able to verify that `Hello, world` is still outputted.

### Scaling Ideas

We have now verified adding a Go server to dotnet aspire and forwarding requests through a reverse proxy.  
We can now extend this process to be implemented programmatically.  
For instance, we can generate multiple instances by appending numbers to the service name for the echo server, and automatically add the configurations for the reverse proxy.

Modify the code in the Program.cs file of the Aspire AppHost project to utilize the reverse proxy and echo server as follows:

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

Next, modify the Program.cs file of the reverse proxy project as follows:

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

Destination settings are added for the eight increased echo server instances.  
Now the reverse proxy possesses destination information for the expanded echo servers and can route requests to them.  
If you access the existing `http://localhost:3000/?name=world`, you will confirm that `Hello, world` is still successfully printed.

## Conclusion

In this article, the process of adding a Go server to dotnet aspire and forwarding requests through a reverse proxy was explained.  
However, scaling has not been fully covered in its entirety here, and an example implementing a more programmatic approach using environment variables has been prepared in a separate repository.  
For detailed project configuration and code, please refer to [snowmerak/AspireStarterPack](https://github.com/snowmerak/AspireStarterPack).

Personally, I expect dotnet aspire to fulfill its own role as an alternative to Docker Compose and as a cloud deployment tool.  
Since [generators](https://prom3theu5.github.io/aspirational-manifests/generate-command.html) that create Docker Compose or Kubernetes manifests already exist, I believe accessibility to infrastructure tools has become much greater for ordinary developers.
