.NET Aspire: правильное создание облачных приложений

· 10 мин чтения

Если вы когда-либо создавали распределенное приложение на .NET, вы знаете суть. Вы запускаете веб-API, добавляете фонового работника, добавляете Redis для кэширования, PostgreSQL для сохранения, возможно, RabbitMQ для обмена сообщениями — и внезапно вы тратите больше времени на подключение инфраструктуры, чем на написание бизнес-логики. Строки подключения, разбросанные по файлам appsettings.json, проверки работоспособности, которые вы забыли настроить, наблюдаемость, которая всегда является «проблемой следующего спринта».

Я был там. Больше раз, чем мне хотелось бы признать.

Именно для решения этой проблемы был создан .NET Aspire. После нескольких месяцев эксплуатации его в производстве я хочу поделиться тем, что я узнал — хорошим, замечательным и ошибками.

Что такое .NET Aspire?

.NET Aspire — это продуманный стек для создания наблюдаемых, готовых к использованию распределенных приложений с помощью .NET. Это не платформа в традиционном смысле — она не заменяет ASP.NET Core и не заставляет вас использовать новую модель программирования. Вместо этого он опирается на то, что вы уже знаете, и заполняет пробелы, которые всегда существовали при создании облачных приложений.

По своей сути Aspire дает вам четыре вещи:

  1. AppHost — проект, определяющий всю топологию распределенного приложения. Какие сервисы существуют, от чего они зависят и как подключаются.
  2. Настройки службы по умолчанию – общий проект, который настраивает сквозные задачи, такие как проверки работоспособности, политики устойчивости и OpenTelemetry, – один раз для всех ваших служб.
  3. Компоненты — пакеты NuGet, обеспечивающие стандартизированную интеграцию с вспомогательными службами, такими как Redis, PostgreSQL, RabbitMQ, Azure Storage и другими.
  4. Панель разработчика — пользовательский интерфейс, работающий в режиме реального времени, который отображает журналы, трассировки и метрики для всего распределенного приложения во время локальной разработки.

Философия проста: если каждое облачное приложение .NET нуждается в этих вещах, почему мы все каждый раз реализуем их с нуля?

Настройка вашего первого проекта Aspire

Начать работу очень просто. Вам потребуется .NET 8 или более поздняя версия и установленная рабочая нагрузка Aspire:

[[[ТОК_1]]]

Теперь создайте новый стартовый проект Aspire:

[[[ТОК_2]]]

Это генерирует решение с четырьмя проектами:

  • MyCloudApp.AppHost — Оркестратор
  • MyCloudApp.ServiceDefaults — Общая конфигурация
  • MyCloudApp.ApiService — пример веб-API.
  • MyCloudApp.Web — интерфейс Blazor

Запустите AppHost, и вы сразу же увидите в браузере открытую панель управления Aspire, показывающую все ваши службы, их журналы и состояние их работоспособности. Нет файла Docker Compose. Никакого ручного управления портами. Это просто работает.

[[[ТОК_7]]]

Именно этот первый опыт меня и зацепил. Менее чем за минуту вы получите полностью оркестрованное распределенное приложение со встроенной возможностью наблюдения.

Шаблон AppHost

AppHost — это место, где живет волшебство. Это небольшое консольное приложение, которое использует шаблон компоновщика для определения топологии вашего распределенного приложения — какие ресурсы существуют и как к ним подключаются службы.

Вот как выглядит реалистичный AppHost:

[[[ТОК_8]]]Прочтите этот код вслух. Он практически документирует себя. «API каталога ссылается на PostgreSQL, Redis и RabbitMQ. Интерфейс ссылается на API каталога и API заказа». Это ваша архитектура, выраженная в коде.

Несколько вещей, которые стоит отметить:

  • WithReference выполняет тяжелую работу. Он автоматически вводит строки подключения и URL-адреса служб в потребляющий проект через переменные среды и конфигурацию. Вашим службам не нужно знать, где работает Redis — Aspire справится с этим.
  • WithPgAdmin() и WithManagementPlugin() расширяют пользовательские интерфейсы администратора для PostgreSQL и RabbitMQ наряду с реальными службами. Во время разработки они неоценимы.
  • WithExternalHttpEndpoints() отмечает службу как доступную извне, что имеет значение во время развертывания.

Конфигурация ресурса

Вы можете настроить ресурсы с детальным контролем:

var postgres = builder.AddPostgres("postgres")
    .WithEnvironment("POSTGRES_MAX_CONNECTIONS", "200")
    .WithDataVolume("postgres-data")
    .AddDatabase("catalogdb");

var redis = builder.AddRedis("cache")
    .WithDataVolume("redis-data");

Объемы данных гарантируют, что ваши локальные данные разработки выдержат перезапуск контейнера. Маленькие детали, большое улучшение качества жизни.

Сервисные настройки по умолчанию: Невоспетый герой

Проект ServiceDefaults — самая недооцененная часть Aspire. Это общая библиотека, на которую ссылается каждая служба в вашем решении, и она настраивает все сквозные проблемы, которые вы в противном случае забыли бы или реализовали непоследовательно.

Вот как выглядит типичный Extensions.cs в ServiceDefaults:

public static class Extensions
{
    public static IHostApplicationBuilder AddServiceDefaults(
        this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();
        builder.AddDefaultHealthChecks();
        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            http.AddStandardResilienceHandler();
            http.AddServiceDiscovery();
        });

        return builder;
    }

    public static IHostApplicationBuilder ConfigureOpenTelemetry(
        this IHostApplicationBuilder builder)
    {
        builder.Logging.AddOpenTelemetry(logging =>
        {
            logging.IncludeFormattedMessage = true;
            logging.IncludeScopes = true;
        });

        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation();
            })
            .WithTracing(tracing =>
            {
                tracing.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddGrpcClientInstrumentation();
            });

        builder.AddOpenTelemetryExporters();
        return builder;
    }

    public static IHostApplicationBuilder AddDefaultHealthChecks(
        this IHostApplicationBuilder builder)
    {
        builder.Services.AddHealthChecks()
            .AddCheck("self", () => HealthCheckResult.Healthy(),
                ["live"]);

        return builder;
    }
}

Затем в Program.cs каждой службы одна строка делает все это:

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();

// Your service-specific configuration...

var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();

Этот единственный вызов AddServiceDefaults() дает вам:

  • OpenTelemetry со структурированным журналированием, метриками и распределенной трассировкой.
  • Проверки работоспособности с указанием конечных точек работоспособности и готовности.
  • Обнаружение служб, чтобы службы могли находить друг друга по имени.
  • Политики устойчивости для всех исходящих HTTP-вызовов (повторные попытки, автоматические выключатели, тайм-ауты).

Это то, что отличает демоверсию «работает на моей машине» от готовой к использованию системы. И Aspire делает это значением по умолчанию, а не второстепенным вопросом.

Компоненты Aspire

Компоненты Aspire — это пакеты NuGet, которые стандартизируют способ подключения ваших сервисов к поддерживающей инфраструктуре. Это больше, чем просто клиентские библиотеки — они включают в себя проверку работоспособности, ведение журналов, трассировку и настраиваемую устойчивость.

Редис

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddRedisDistributedCache("cache");

Вот и все. Строка подключения поступает от AppHost через WithReference. Компонент регистрирует IDistributedCache, поддерживаемый Redis, с уже подключенными проверками работоспособности и инструментами OpenTelemetry.

Вы также можете использовать Redis для кэширования вывода:

builder.AddRedisOutputCache("cache");

PostgreSQL с Entity Framework Core

builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb", settings =>
{
    settings.DisableRetry = false;
});

При этом ваш DbContext будет подключен к базе данных catalogdb, определенной в AppHost. Он включает в себя пул соединений, проверки работоспособности и политики повторных попыток.

КроликMQ

builder.AddRabbitMQClient("messaging");

Регистрирует IConnection из клиентской библиотеки RabbitMQ, полностью настроенный и проверенный на работоспособность.

Интеграции Azure

Aspire также имеет первоклассные компоненты для сервисов Azure:

// Azure Blob Storage
builder.AddAzureBlobClient("blobs");

// Azure Service Bus
builder.AddAzureServiceBusClient("servicebus");

// Azure Key Vault
builder.AddAzureKeyVaultClient("secrets");
```Шаблон всегда один и тот же: одна строка в AppHost для определения ресурса, одна строка в службе-потребителе для его использования. Детали подключения передаются автоматически.

## Панель разработчика

Панель управления Aspire  одна из тех функций, которая кажется приятной, пока вы ее не используете  тогда вы не сможете представить себе работу без нее.

Когда вы запускаете AppHost локально, запускается панель мониторинга, которая предоставляет вам:

- **Обзор ресурсов**  Краткий обзор всех ваших услуг и инфраструктуры с индикаторами состояния.
- **Структурированные журналы**  потоковая передача журналов в реальном времени из каждой службы с возможностью фильтрации и поиска.
- **Распределенные трассировки**  сквозные трассировки запросов, охватывающие несколько служб, визуализированные в виде флейм-диаграмм.
- **Метрики**  частота HTTP-запросов, частота ошибок, задержки и пользовательские метрики в режиме реального времени.
- **Журналы консоли**  необработанные данные stdout/stderr из каждого контейнера и проекта.

Распределенная трассировка особенно ценна. Когда запрос попадает в ваш интерфейс, проходит через API каталога, касается Redis и PostgreSQL  вы видите всю цепочку с указанием времени для каждого перехода. Больше не нужно гадать, где находится узкое место.

Я считаю, что панель мониторинга наиболее полезна во время отладки. Вместо отслеживания нескольких окон терминала или перехода между файлами журналов все находится в одном месте с идентификаторами корреляции, связывающими связанные события между службами.

Панель мониторинга также доступна в виде автономного контейнера, что означает, что вы можете использовать ее в промежуточных средах или конвейерах CI:

```bash
docker run --rm -p 18888:18888 \
  mcr.microsoft.com/dotnet/aspire-dashboard:latest

Развертывание

Aspire помогает во время разработки, но как насчет производства? Здесь все становится практичным.

Приложения-контейнеры Azure

Самый простой путь развертывания — это приложения-контейнеры Azure (ACA), которые имеют первоклассную поддержку Aspire. Вы можете выполнить развертывание напрямую с помощью интерфейса командной строки разработчика Azure:

azd init
azd up

Команда azd init обнаруживает ваш Aspire AppHost и генерирует необходимую инфраструктуру в виде кода. azd up подготавливает все — реестр контейнеров, приложения-контейнеры, базы данных, экземпляры Redis — на основе вашей топологии AppHost.

Ваш AppHost по сути становится манифестом вашего развертывания. Тот же код, который определяет, что «catalog-api зависит от PostgreSQL и Redis», управляет подготовкой инфраструктуры.

Кубернетес

Для развертываний Kubernetes Aspire не создает манифесты напрямую, но ваша топология AppHost четко сопоставляется с ресурсами Kubernetes. Инструмент сообщества aspirate (Aspir8) может создавать диаграммы Helm или манифесты Kubernetes из вашего AppHost:

dotnet tool install -g aspirate
aspirate generate
aspirate apply

Рекомендации по развертыванию

Несколько вещей, которые следует иметь в виду:- Строки подключения меняются в зависимости от среды. Локально Aspire запускает контейнеры и автоматически вводит строки подключения. В рабочей среде вы укажете на управляемые службы. Aspire использует стандартную конфигурацию .NET, поэтому переменные среды и Azure Key Vault работают должным образом.

  • AppHost не работает в рабочей среде. Это инструмент оркестрации разработки и развертывания. В рабочей среде ваши службы работают независимо и настраиваются с помощью переменных среды и оркестраторов. — Ресурсы инфраструктуры становятся управляемыми службами. Ваш локальный контейнер PostgreSQL становится базой данных Azure для PostgreSQL. Ваш локальный контейнер Redis становится кэшем Azure для Redis. Потребляющий код не меняется.

Реальные советы

После некоторого запуска Aspire в производство, вот уроки, которые сэкономили нам время:

1. Используйте специальные проверки жизненного цикла ресурсов

Не полагайтесь только на запуск контейнера, чтобы определить, готов ли ресурс. PostgreSQL может принимать TCP-соединения еще до того, как будет готов обслуживать запросы.

var postgres = builder.AddPostgres("postgres")
    .AddDatabase("catalogdb")
    .WithHealthCheck();

Aspire может проверять работоспособность ресурсов и удерживать зависимые службы до тех пор, пока они не будут фактически готовы.

2. Извлечение общих шаблонов в расширения

Если несколько служб имеют схожие конфигурации, создайте методы расширения:

public static class AppHostExtensions
{
    public static IResourceBuilder<ProjectResource> AddWorkerService(
        this IDistributedApplicationBuilder builder,
        string name,
        IResourceBuilder<IResourceWithConnectionString> db,
        IResourceBuilder<IResourceWithConnectionString> messaging)
    {
        return builder.AddProject<Projects.WorkerService>(name)
            .WithReference(db)
            .WithReference(messaging)
            .WithReplicas(3);
    }
}

3. Использование WithReplicas для нагрузочного тестирования

var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReplicas(5);

WithReplicas локально запускает несколько экземпляров службы. Это отлично подходит для тестирования балансировки нагрузки, ошибок параллелизма и поведения распределенного кэширования без развертывания в кластере.

4. Используйте параметры для чувствительных значений

Не указывайте учетные данные жестко, даже для локальной разработки:

var dbPassword = builder.AddParameter("db-password", secret: true);

var postgres = builder.AddPostgres("postgres", password: dbPassword)
    .AddDatabase("catalogdb");

При локальном запуске Aspire запросит значение или прочтет его из секретов пользователя. В CI/CD это происходит из переменных среды.

5. Интеграционные тесты становятся тривиальными

Aspire включает пакет тестирования, который значительно упрощает интеграционное тестирование распределенных приложений:

[Fact]
public async Task CatalogApiReturnsProducts()
{
    var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.MyCloudApp_AppHost>();

    await using var app = await appHost.BuildAsync();
    await app.StartAsync();

    var httpClient = app.CreateHttpClient("catalog-api");
    var response = await httpClient.GetAsync("/api/products");

    response.EnsureSuccessStatusCode();

    var products = await response.Content
        .ReadFromJsonAsync<List<Product>>();
    Assert.NotEmpty(products);
}

Это раскручивает ваше все распределенное приложение — включая базы данных и брокеры сообщений — запускает ваш тест и разрушает его. Реальные интеграционные тесты с реальной инфраструктурой в вашем конвейере CI. Никаких издевательств.

6. Мониторинг использования ресурсов локально

При локальном запуске нескольких контейнеров следите за потреблением ресурсов. Экземпляр PostgreSQL, Redis и RabbitMQ с пользовательским интерфейсом управления может легко занимать 2–3 ГБ ОЗУ. Если вы используете машину с ограниченными возможностями, рассмотрите возможность использования более легких конфигураций ресурсов:

var redis = builder.AddRedis("cache")
    .WithContainerRuntimeArgs("--memory=256m");

Заключение

.NET Aspire фундаментально изменил мой подход к созданию распределенных приложений. Не потому, что он вводит революционные концепции — проверки работоспособности, OpenTelemetry и оркестровка контейнеров не новы. А потому, что это делает их по умолчанию. Он принимает сотни небольших решений, которые вам обычно приходится принимать, реализует их с разумными значениями по умолчанию и позволяет вам переопределить их, когда это необходимо.На мой взгляд, шаблон AppHost — это самая большая победа. Если топология распределенного приложения выражена в виде кода, а не разбросана по файлам Docker Compose, манифестам Kubernetes и документам README, это делает систему понятной. Новые члены команды могут открыть Program.cs в AppHost и понять всю архитектуру за считанные минуты.

Если вы создаете распределенные приложения с помощью .NET, присмотритесь к Aspire серьезно. Начните с шаблона aspire-starter, изучите панель мониторинга и постепенно внедряйте компоненты по мере необходимости. Вам не обязательно идти ва-банк в первый же день — Aspire по своей сути аддитивен.

Дни, когда вы тратили свой первый спринт на подключение шаблона инфраструктуры, прошли. Позвольте Aspire позаботиться о сантехнике, чтобы вы могли сосредоточиться на том, что действительно важно: на вашем приложении.