---
id: 7ecf5f1e3335910ea27e6beb68f04ae7
author: snowmerak
title: Запуск масштабируемого Go-сервера в .NET Aspire
description: 'Как масштабируемо запускать Go-серверы с помощью .NET Aspire: пример горизонтального масштабирования и автоматизированной конфигурации с обратным прокси-сервером YARP.'
language: ru
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 — это инструмент, созданный для того, чтобы помогать разработчикам в создании и конфигурировании облачно-нативных приложений по мере роста популярности облачно-нативных сред. Он позволяет разработчикам на .NET легко развертывать .NET-проекты, различную облачно-нативную инфраструктуру, а также сервисы на других языках или контейнеры.

Разумеется, технологии от docker до k8s уже выведены в производство и используются, и многие сферы, отрасли и разработчики, работавшие с локальной on-premise средой, переходят в облачно-нативную среду и уже перешли. Сегодня это зрелая область. Поэтому, полагаю, нет необходимости объяснять существующие неудобства, связанные с именами хостов, конфигурацией портов, брандмауэрами, управлением метриками и т. п.

Именно поэтому, судя по приведенному выше описанию, вы, вероятно, совершенно не представляете, что такое dotnet aspire. Дело в том, что Microsoft также не даёт его точного определения. Поэтому я тоже не буду давать собственного определения. Однако в этой статье я воспользуюсь основными функциями dotnet aspire, как я их понимаю, так что можно ориентироваться на них и определить собственное представление.

## Конфигурация проекта

### Создание проекта dotnet aspire

Если шаблон dotnet aspire отсутствует, сначала необходимо его установить. Установите шаблон следующей командой. Если .NET не установлен, пожалуйста, установите его самостоятельно.

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

Затем в подходящей папке создайте новое решение.

```bash
dotnet new sln
```

После этого в папке решения выполните следующую команду, чтобы создать проект по шаблону aspire-apphost.

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

В результате будет создан проект aspire-apphost, содержащий лишь простой код для настройки.

### Добавление Valkey

Теперь добавим Valkey.

Прежде чем добавлять его напрямую, отметим, что dotnet aspire предоставляет различные сторонние решения через community hosting.  
Разумеется, Valkey также может получить поддержку такого community hosting и легко используется через следующий пакет NuGet.

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

Помимо этого, доступны и другие интегрированные хостинги, с которыми можно ознакомиться [здесь](https://learn.microsoft.com/ko-kr/dotnet/aspire/fundamentals/integrations-overview).  
Вернемся к Valkey: откройте файл Program.cs в проекте AppHost и измените его следующим образом.

```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` является реализацией интерфейса `IResourceBuilder`, которая содержит информацию для сборки сервиса valkey.  
`WithDataVolume` создает том для хранения данных кэша, а `WithPersistence` обеспечивает постоянное сохранение данных кэша.  
Судя по этому, их роль похожа на `volumes` в `docker-compose`.  
Разумеется, вы без труда сможете сделать это самостоятельно.  
Однако это выходит за рамки данной статьи, поэтому сейчас я не буду об этом рассказывать.

### Создание эхо-сервера на Go

Теперь добавим простой сервер на языке Go.  
Сначала в папке решения создадим workspace с помощью `go work init`.  
Для разработчиков на .NET workspace Go можно считать чем-то похожим на решение.

Затем создайте папку EchoServer, перейдите в неё и выполните `go mod init EchoServer`.  
Эта команда создает модуль Go. Модуль можно воспринимать как аналог проекта для разработчиков .NET.  
Затем создайте файл `main.go` и напишите в нем следующее.

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

Этот сервер при запуске Aspire AppHost получает переменную окружения `PORT`, в которой указан порт для прослушивания, считывает этот порт и запускает сервер.  
Это простой сервер, который принимает параметр запроса `name` и возвращает `Hello, {name}`.

Теперь добавим этот сервер в dotnet aspire.

### Добавление эхо-сервера в aspire

Вернитесь в проект Aspire AppHost, в котором мы добавляли Valkey, и добавьте community hosting для языка Go.

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

Затем откройте файл Program.cs и добавьте следующий код.

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

Здесь `echoServer` является реализацией интерфейса `IResourceBuilder`, которая содержит информацию для сборки сервера на Go.  
Метод `AddGolangApp`, который мы только что добавили, является методом расширения пользовательского хоста для добавления сервера на Go.  
Используется фиксированный порт 3000, и можно видеть, что переменная окружения `PORT` передается сервису.  
Наконец, `WithExternalHttpEndpoints` делает сервис доступным извне.

Для проверки откройте `http://localhost:3000/?name=world`, и вы увидите вывод `Hello, world`.

Однако в текущей версии dotnet aspire для проектов, не относящихся к .NET, существует серьезный недостаток.  
Он заключается в следующем...

## Масштабирование проекта

### Как же тогда выполнить горизонтальное масштабирование?

Сейчас dotnet aspire предоставляет опцию `WithReplica` только для построителя проектов .NET, добавленных методом `AddProject`.  
Однако для хоста на Go или внешних проектов, например, добавленных через `AddContainer`, эта опция не предоставляется.

Поэтому приходится использовать отдельный load balancer или reverse proxy и реализовывать масштабирование самостоятельно.  
Но в таком случае reverse proxy может стать SPOF, поэтому для него желательно предоставлять опцию `WithReplica`.  
А значит, reverse proxy неизбежно должен быть проектом .NET.

До сих пор для решения этой задачи я использовал nginx, traefik, а также собственную реализацию, но при ограничении в виде проекта .NET у меня не было готового решения.  
Поэтому я поискал reverse proxy, реализованный на .NET, и, к счастью, нашел вариант [YARP](https://microsoft.github.io/reverse-proxy/).  
YARP — это reverse proxy, реализованный на .NET; он может выполнять роль load balancer и предоставляет различные возможности, поэтому я счел его хорошим выбором.

Теперь добавим YARP.

### Настройка reverse proxy на YARP

Сначала создайте проект для использования YARP.

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

Затем перейдите в проект и установите YARP.

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

После завершения установки откройте файл Program.cs и напишите следующее.

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

Этот код является базовым кодом для использования YARP.  
`routes` содержит информацию о маршрутах, которые будет использовать reverse proxy, а `clusters` содержит информацию о кластерах, которые будет использовать reverse proxy.  
Эта информация загружается в reverse proxy методом `LoadFromMemory`.  
Наконец, метод `MapReverseProxy` сопоставляет reverse proxy и запускает его.

Затем, для практического использования, добавьте ссылку на проект reverse proxy в проекте aspire apphost и внесите следующие изменения и дополнения в файл 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();
```

Теперь reverse proxy может обращаться к echo server по ссылке.  
Структура изменена так, что внешние запросы принимаются reverse proxy и передаются на echo server.

### Изменение reverse proxy

Прежде всего необходимо изменить адрес прослушивания для проекта reverse proxy.  
Удалите `applicationUrl` из файла `Properties/launchSettings.json`.  
Затем откройте файл Program.cs и существенно измените его следующим образом.

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

Сначала изменяются данные для `routes` и `clusters`.  
Добавляются `echo-route` и `echo-cluster`, чтобы запросы направлялись на echo server.  
Кроме того, адрес echo server теперь считывается из переменной окружения.

Правило для этого адреса: `services__{service-name}__http__{index}`.  
Для echo server имя сервиса равно `echo-server`, а поскольку экземпляр один, в качестве индекса используется `0`.  
Если добавить сервер ASP.NET Core, то с помощью `WithReplica` может быть создано несколько экземпляров, и тогда достаточно увеличивать индекс.  
Значение `http://localhost:8080`, указанное в качестве обработки исключения, является бессмысленным мусорным значением.

Теперь запустите проект и откройте `http://localhost:3000/?name=world`: вы по-прежнему увидите вывод `Hello, world`.

### Идеи для расширения

Итак, мы добавили сервер на Go в dotnet aspire и убедились, что запросы передаются через reverse proxy.  
Теперь можно расширить этот процесс так, чтобы реализовать его программно.  
Например, можно добавить к имени echo server порядковый номер, создавая несколько экземпляров, и автоматически добавлять настройки в reverse proxy.

Измените код в файле Program.cs проекта aspire apphost, использующий reverse proxy и echo server, следующим образом.

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

Затем измените файл Program.cs проекта reverse proxy следующим образом.

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

Добавляем настройки назначения для 8 экземпляров echo server.  
Теперь reverse proxy располагает информацией о назначениях для всех экземпляров echo server и может передавать им запросы.  
Откройте прежний адрес `http://localhost:3000/?name=world`: вы по-прежнему увидите вывод `Hello, world`.

## Заключение

В этой статье мы рассмотрели, как добавить сервер на Go в dotnet aspire и передавать запросы через reverse proxy.  
Однако расширение пока описано не полностью: пример более программного подхода с использованием переменных окружения я разместил в отдельном репозитории.  
Подробнее о структуре проекта и коде см. в [snowmerak/AspireStartPack](https://github.com/snowmerak/AspireStarterPack).

Лично я полагаю, что dotnet aspire может выполнять собственную роль как альтернатива docker compose и как инструмент развертывания в облаке.  
Уже существуют [генераторы](https://prom3theu5.github.io/aspirational-manifests/generate-command.html), создающие манифесты docker compose и k8s, и, по-моему, доступ обычных разработчиков к инфраструктурным инструментам стал проще.
