Запуск масштабируемого Go-сервера в .NET Aspire

dotnet aspire?
dotnet aspire — это инструмент, созданный для того, чтобы помогать разработчикам в создании и конфигурировании облачно-нативных приложений по мере роста популярности облачно-нативных сред. Он позволяет разработчикам на .NET легко развертывать .NET-проекты, различную облачно-нативную инфраструктуру, а также сервисы на других языках или контейнеры.
Разумеется, технологии от docker до k8s уже выведены в производство и используются, и многие сферы, отрасли и разработчики, работавшие с локальной on-premise средой, переходят в облачно-нативную среду и уже перешли. Сегодня это зрелая область. Поэтому, полагаю, нет необходимости объяснять существующие неудобства, связанные с именами хостов, конфигурацией портов, брандмауэрами, управлением метриками и т. п.
Именно поэтому, судя по приведенному выше описанию, вы, вероятно, совершенно не представляете, что такое dotnet aspire. Дело в том, что Microsoft также не даёт его точного определения. Поэтому я тоже не буду давать собственного определения. Однако в этой статье я воспользуюсь основными функциями dotnet aspire, как я их понимаю, так что можно ориентироваться на них и определить собственное представление.
Конфигурация проекта
Создание проекта dotnet aspire
Если шаблон dotnet aspire отсутствует, сначала необходимо его установить. Установите шаблон следующей командой. Если .NET не установлен, пожалуйста, установите его самостоятельно.
1dotnet new install Aspire.ProjectTemplates
Затем в подходящей папке создайте новое решение.
1dotnet new sln
После этого в папке решения выполните следующую команду, чтобы создать проект по шаблону aspire-apphost.
1dotnet new aspire-apphost -o AppHost
В результате будет создан проект aspire-apphost, содержащий лишь простой код для настройки.
Добавление Valkey
Теперь добавим Valkey.
Прежде чем добавлять его напрямую, отметим, что dotnet aspire предоставляет различные сторонние решения через community hosting.
Разумеется, Valkey также может получить поддержку такого community hosting и легко используется через следующий пакет NuGet.
1dotnet add package Aspire.Hosting.Valkey
Помимо этого, доступны и другие интегрированные хостинги, с которыми можно ознакомиться здесь.
Вернемся к Valkey: откройте файл Program.cs в проекте AppHost и измените его следующим образом.
1var builder = DistributedApplication.CreateBuilder(args);
2
3var cache = builder.AddValkey("cache")
4 .WithDataVolume(isReadOnly: false)
5 .WithPersistence(interval: TimeSpan.FromMinutes(5),
6 keysChangedThreshold: 100);
7
8builder.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 и напишите в нем следующее.
1package main
2
3import (
4 "log"
5 "net/http"
6 "os"
7)
8
9func main() {
10 addr := os.Getenv("PORT")
11 log.Printf("Server started on %s", addr)
12
13 http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
14 name := request.URL.Query().Get("name")
15 writer.Write([]byte("Hello, " + name))
16 })
17
18 http.ListenAndServe(":"+addr, nil)
19}
Этот сервер при запуске Aspire AppHost получает переменную окружения PORT, в которой указан порт для прослушивания, считывает этот порт и запускает сервер.
Это простой сервер, который принимает параметр запроса name и возвращает Hello, {name}.
Теперь добавим этот сервер в dotnet aspire.
Добавление эхо-сервера в aspire
Вернитесь в проект Aspire AppHost, в котором мы добавляли Valkey, и добавьте community hosting для языка Go.
1dotnet add package CommunityToolkit.Aspire.Hosting.Golang
Затем откройте файл Program.cs и добавьте следующий код.
1var builder = DistributedApplication.CreateBuilder(args);
2
3var cache = builder.AddValkey("cache")
4 .WithDataVolume(isReadOnly: false)
5 .WithPersistence(interval: TimeSpan.FromMinutes(5),
6 keysChangedThreshold: 100);
7
8var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
9 .WithHttpEndpoint(port: 3000, env: "PORT")
10 .WithExternalHttpEndpoints();
11
12builder.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.
YARP — это reverse proxy, реализованный на .NET; он может выполнять роль load balancer и предоставляет различные возможности, поэтому я счел его хорошим выбором.
Теперь добавим YARP.
Настройка reverse proxy на YARP
Сначала создайте проект для использования YARP.
1dotnet new web -n ReverseProxy
Затем перейдите в проект и установите YARP.
1dotnet add package Yarp.ReverseProxy --version 2.2.0
После завершения установки откройте файл Program.cs и напишите следующее.
1using Yarp.ReverseProxy.Configuration;
2
3var builder = WebApplication.CreateBuilder(args);
4
5var routes = new List<RouteConfig>();
6var clusters = new List<ClusterConfig>();
7
8builder.Services.AddReverseProxy()
9 .LoadFromMemory(routes, clusters);
10
11var app = builder.Build();
12
13app.MapReverseProxy();
14app.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.
1dotnet add reference ../ReverseProxy
1var echoServer = builder.AddGolangApp("echo-server", "../EchoServer")
2 .WithHttpEndpoint(env: "PORT");
3
4var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
5 .WithReference(echoServer)
6 .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
7 .WithExternalHttpEndpoints();
Теперь reverse proxy может обращаться к echo server по ссылке.
Структура изменена так, что внешние запросы принимаются reverse proxy и передаются на echo server.
Изменение reverse proxy
Прежде всего необходимо изменить адрес прослушивания для проекта reverse proxy.
Удалите applicationUrl из файла Properties/launchSettings.json.
Затем откройте файл Program.cs и существенно измените его следующим образом.
1using Yarp.ReverseProxy.Configuration;
2
3var builder = WebApplication.CreateBuilder(args);
4
5var routes = new List<RouteConfig>
6{
7 new RouteConfig
8 {
9 ClusterId = "cluster-echo",
10 RouteId = "route-echo",
11 Match = new RouteMatch
12 {
13 Path = "/"
14 }
15 }
16};
17
18var echoServerAddr = Environment.GetEnvironmentVariable("services__echo-server__http__0") ?? "http://localhost:8080";
19
20var clusters = new List<ClusterConfig>
21{
22 new ClusterConfig
23 {
24 ClusterId = "cluster-echo",
25 Destinations = new Dictionary<string, DestinationConfig>
26 {
27 { "destination-echo", new DestinationConfig { Address = echoServerAddr } }
28 }
29 }
30};
31
32builder.Services.AddReverseProxy()
33 .LoadFromMemory(routes, clusters);
34
35var app = builder.Build();
36
37app.MapReverseProxy();
38app.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, следующим образом.
1var reverseProxy = builder.AddProject<Projects.ReverseProxy>("gateway")
2 .WithHttpEndpoint(port: 3000, env: "PORT", isProxied: true)
3 .WithExternalHttpEndpoints();
4
5for (var i = 0; i < 8; i++)
6{
7 var echoServer = builder.AddGolangApp($"echo-server-{i}", "../EchoServer")
8 .WithHttpEndpoint(env: "PORT");
9 reverseProxy.WithReference(echoServer);
10}
Затем измените файл Program.cs проекта reverse proxy следующим образом.
1var echoServerDestinations = new Dictionary<string, DestinationConfig>();
2for (var i = 0; i < 8; i++)
3{
4 echoServerDestinations[$"destination-{i}"] = new DestinationConfig
5 {
6 Address = Environment.GetEnvironmentVariable($"services__echo-server-{i}__http__0") ?? "http://localhost:8080"
7 };
8}
9
10var clusters = new List<ClusterConfig>
11{
12 new ClusterConfig
13 {
14 ClusterId = "cluster-echo",
15 Destinations = echoServerDestinations
16 }
17};
Добавляем настройки назначения для 8 экземпляров echo server.
Теперь reverse proxy располагает информацией о назначениях для всех экземпляров echo server и может передавать им запросы.
Откройте прежний адрес http://localhost:3000/?name=world: вы по-прежнему увидите вывод Hello, world.
Заключение
В этой статье мы рассмотрели, как добавить сервер на Go в dotnet aspire и передавать запросы через reverse proxy.
Однако расширение пока описано не полностью: пример более программного подхода с использованием переменных окружения я разместил в отдельном репозитории.
Подробнее о структуре проекта и коде см. в snowmerak/AspireStartPack.
Лично я полагаю, что dotnet aspire может выполнять собственную роль как альтернатива docker compose и как инструмент развертывания в облаке.
Уже существуют генераторы, создающие манифесты docker compose и k8s, и, по-моему, доступ обычных разработчиков к инфраструктурным инструментам стал проще.