В прошлой статье я настроил локальный цикл разработки с Aspire + LocalStack. Никаких затрат на AWS, быстрые итерации, эмуляция сервисов AWS. Логичный следующий вопрос: как развернуть это же приложение в средах AWS (тестирование, стейджинг, продакшен), не поддерживая отдельный инфраструктурный код? И можно ли сделать это, оставаясь целиком в C#? Если коротко: д��, но с компромиссами.
В этой статье показан подход: один проект Aspire Host, который переключается между локальной эмуляцией и реальным развёртыванием в AWS в зависимости от контекста выполнения. При локальном запуске Aspire связывает LocalStack и контейнеры. При публикации тот же файл передаёт управление стеку AWS CDK, который поднимает VPC, Aurora Serverless, DynamoDB, Lambda и API Gateway. Та же логическая архитектура сервисов (API → база данных, API → DynamoDB), но немного разные реализации инфраструктуры.
Прежде чем перейти к коду, важно понимать ключевое ограничение: на конец 2025 года у Aspire нет встроенного паблишера (publisher) для AWS. Он не умеет упаковывать .NET-проект в ZIP, совместимый с Lambda, управлять версиями или моделировать специфичные для AWS ресурсы вроде RDS Proxy или VPC-эндпоинты. К счастью, AWS CDK уже умеет всё это. Поэтому схема такая: Aspire оркестрирует (решает «локально или публикация», связывает зависимости), CDK разворачивает инфраструктуру (синтезирует шаблон CloudFormation, собирает артефакты для Lambda, деплоит всё в AWS). Так мы держим всё в одном месте и не ждём, пока Aspire обрастёт полноценными примитивами деплоя в AWS. Можно думать об этом так: Aspire — оркестратор верхнего уровня, который знает о сервисах и их связях, а CDK — специализированный инструмент, который точно знает, как упаковать .NET-код для Lambda, настроить VPC и правильно связать группы безопасности. Каждый инструмент делает то, что у него получается лучше всего.
Что будем строить
Мы развернём примерное приложение на базе serverless-сервисов: функция Lambda, в которой работает наш API, Aurora Serverless v2 для PostgreSQL, DynamoDB для вспомогательного хранения данных и API Gateway, который маршрутизирует HTTP-трафик. Всё запускается в приватной VPC без прямого доступа в интернет — исключение составляет только эндпоинт API Gateway.
Во время локальной разработки Aspire поднимает LocalStack для эмуляции DynamoDB и других сервисов AWS, а также контейнер Postgres для базы данных. При публикации мы будем поднимать настоящие ресурсы AWS.
Локальный режим:
LocalStack эмулирует DynamoDB
Локальный контейнер Postgres для базы данных
Эмулятор AWS Lambda запускает API локально
Эмулятор API Gateway маршрутизирует HTTP-запросы
Режим AWS:
Стек CDK поднимает реальные ресурсы AWS VPC
Aurora Serverless v2 + RDS Proxy
Таблица DynamoDB
Функция Lambda
HTTP API Gateway
Aspire Host
Теперь, когда мы обозначили архитектурные различия, посмотрим, как Aspire оркестрирует это поведение в двух режимах. Ключевая идея в том, что Aspire может определить, запущен ли он в режиме локальной разработки или в режиме публикации (при развёртывании в AWS), и в зависимости от этого контекста условно подключать разных провайдеров инфраструктуры.
Вся логика оркестрации находится в одном файле хоста. Нет отдельной конфигурации деплоя, нет CI/CD-YAML, где инфраструктурные определения размазаны по нескольким файлам. Вместо этого мы используем ExecutionContext.IsPublishMode в Aspire, чтобы ветвиться между локальной эмуляцией и развёртыванием в AWS:
Вот полная логика ветвления:
// Выбираем между локальной эмуляцией и развёртыванием в AWS // IsPublishMode равен true при запуске 'dotnet run --project Host -- --publisher ...' if (builder.ExecutionContext.IsPublishMode) { // Конфигурация AWS SDK var awsConfig = builder.AddAWSSDKConfig() .WithProfile(builder.Configuration.GetValue("AWS:Profile")) .WithRegion(RegionEndpoint.GetBySystemName(builder.Configuration.GetValue("AWS:Region"))); // Стек CDK, который поднимает полный продакшен-набор инфраструктуры builder.AddAWSCDKStack("AwsSampleStack", s => new SampleStack(s)) .WithReference(awsConfig); } else { // Конфигурация AWS SDK для LocalStack var awsConfig = builder.AddAWSSDKConfig() .WithProfile(builder.Configuration.GetValue("AWS:Profile")) .WithRegion(RegionEndpoint.GetBySystemName(builder.Configuration.GetValue("AWS:Region"))); // Настройка LocalStack var awsLocal = builder.AddLocalStack("AwsLocal", awsConfig: awsConfig); // Стек CDK, который поднимает подмножество ресурсов для эмуляции в LocalStack var awsStack = builder.AddAWSCDKStack("AwsSampleBaseStack", s => new SampleBaseStack(s)) .WithReference(awsConfig); // Локальный контейнер Postgres var postgres = builder.AddPostgres("Postgres"); // Функция Lambda, которая запускает API локально var api = builder.AddAWSLambdaFunction<Projects.Api>("Api", "Api") .WithReference(awsStack) .WithReference(postgres); // Эмулятор API Gateway, проксирующий все запросы в Lambda builder.AddAWSAPIGatewayEmulator("ApiGatewayEmulator", APIGatewayType.HttpV2) .WithReference(api, Method.Any, "/{proxy+}"); // Подключаем LocalStack builder.UseLocalStack(awsLocal);
Ключевые различия:
Стек CDK: в режиме публикации поднимается SampleStack (полная продакшен-инфраструктура с VPC, Aurora, Lambda, API Gateway); локально поднимается
SampleBaseStack(только подмножество AWS-сервисов, которое LocalStack может эмулировать, — таблица DynamoDB)База данных: локально добавляется явный ресурс контейнера Postgres; в режиме публикации используется кластер Amazon Aurora (RDS), определённый в стеке CDK
Привязка Lambda: в локальном режиме используется
.AddAWSLambdaFunction<Projects.Api>, чтобы запускать API локально через эмулятор Lambda
Эта единственная проверка IsPublishMode и является швом между эмуляцией и деплоем. Когда мы запускаем dotnet run --project Host, получаем локальный режим. Когда запускаем dotnet run --project Host -- --publisher ..., попадаем в режим публикации, и дальше управление берёт на себя CDK.
Компромисс по паритету окружений
Вот слон в комнате: технически этот подход нарушает принцип паритета окружений. В локальном режиме база — это Postgres в контейнере; в продакшене — Aurora Serverless v2 с RDS Proxy. Локально используется эмуляция DynamoDB в LocalStack; в продакшене — настоящая DynamoDB. Локально API работает в эмуляторе Lambda; в продакшене — в реальной Lambda с VPC-сетью, группами безопасности и IAM-ролями.
Так почему принять это разделение?
Идеальный паритет между локальной средой и облаком редко оправдывает свою цену. Эмуляция всех нюансов управляемых сервисов (полноценная маршрутизация в VPC, поведение масштабирования Aurora Serverless, RDS Proxy, проверка IAM-политик, реалистичная задержка) на ноутбуке добавляет трение и замедляет внутренний цикл разработки. Альтернатива — разрабатывать напрямую на живом AWS — тоже замедляет обратную связь (деплой на каждое изменение), расходует бюджет, требует постоянного доступа к сети и ломает возможность офлайн-работы.
Поэтому мы целимся в логический паритет вместо физического: одинаковые пути выполнения кода, граф зависимостей, ключи конфигурации, контракты (HTTP/данные/события) и инструментация; но разные реализации, оптимизированные под своё окружение. Локальный режим максимизирует скорость итераций; продакшен — надёжность, масштабируемость и безопасность.
Что делает этот подход рабочим:
Тот же код приложения: код сервисов работает одинаково в обоих окружениях. Используется небольшой паттерн провайдера: флаг на старте определяет, какие зависимости регистрировать (эндпоинты LocalStack + статический пароль к Postgres или реальные клиенты AWS SDK + генератор токенов IAM для аутентификации). Эндпоинты зависят только от абстракций вроде
IDynamoDBContextиNpgsqlDataSource, поэтому при смене провайдеров не требуется никаких изменений в коде (детали конфигурации API мы разберём позже).Единый репозиторий: инфраструктура и код приложения живут вместе. Нет отдельного репозитория для IaC, нет необходимости синхронизироваться между командами, нет разрозненных скриптов деплоя, разбросанных по CI/CD-пайплайнам.
Снижение нагрузки ��а поддержку: когда мы добавляем новый сервис (например, очередь SQS), мы добавляем его один раз в стеке CDK. LocalStack автоматически эмулирует его локально, а CloudFormation разворачивает в продакшене. Никаких дублирующихся YAML-файлов и никакого дрейфа конфигурации.
Автономность разработчиков: разработчики могут итерироваться локально без AWS-учётных данных, сетевого доступа и облачных затрат. Когда всё готово, тот же самый код разворачивается одной командой — вручную или через CI/CD.
Цена этого подхода — осознанность: разработчикам нужно понимать, что Postgres и Aurora не идентичны побайтово (например, специфичные для Aurora возможности не будут работать локально) и что эмуляция LocalStack имеет свои ограничения. Однако это управляемый компромисс по сравнению с поддержкой отдельных инфраструктурных репозиториев или принуждением разработчиков работать напрямую с живым AWS.
Стек CDK: инфраструктура AWS
Теперь, когда мы понимаем стратегию оркестрации и компромиссы, давайте посмотрим, как CDK поднимает реальную инфраструктуру AWS. Напомню: в режиме публикации Aspire Host передаёт управление SampleStack, где описаны все ресурсы для продакшена. Вот что будет создано:
VPC с изолированными подсетями → без интернет-шлюза, без публичных IP
Кластер Aurora Serverless v2 → Postgres с автоскейлингом (0,5–1 ACU)
RDS Proxy → пул соединений + IAM-аутентификация для Lambda
Таблица DynamoDB → таблица с ключом партиции (partition key)
Функция Lambda → рантайм .NET 8, сборка через Docker (сейчас AWS поддерживает .NET 8 как встроенный рантайм)
HTTP API Gateway → один «универсальный» маршрут /{proxy+} в Lambda
Полный код CDK
// Базовый стек: общие ресурсы, которые используются и в LocalStack (локальная разработка), и в AWS (продакшен). // Этот стек создаёт только те ресурсы, которые LocalStack умеет эмулировать (например, DynamoDB). public class SampleBaseStack : Stack { public Table DynamoDbTable { get; } public SampleBaseStack(Construct scope) : this(scope, "SampleBaseStack") { } protected SampleBaseStack(Construct scope, string id) : base(scope, id) { DynamoDbTable = new Table(this, "Table", new TableProps { TableName = "sample-records", PartitionKey = new Attribute { Name = "id", Type = AttributeType.STRING }, RemovalPolicy = RemovalPolicy.DESTROY // Только для демо — в продакшене используйте RETAIN }); } } // Продакшен-стек: наследует общие ресурсы и добавляет специфичную для AWS инфраструктуру. // VPC, Aurora, RDS Proxy, Lambda и API Gateway существуют только в продакшене. public class SampleStack : SampleBaseStack { public CfnOutput PgConnectionString { get; } public CfnOutput ApiUrl { get; } public SampleStack(Construct scope) : base(scope, "SampleStack") { // --- VPC: Изолированная сеть --- // PRIVATE_ISOLATED = без интернет-шлюза, без NAT-шлюза, без публичных IP. // Lambda и Aurora могут общаться только внутри VPC или через VPC эндпоинты. // Доступ к DynamoDB — через VPC эндоинты (без выхода в интернет). var privateSubnets = new SubnetSelection { SubnetType = SubnetType.PRIVATE_ISOLATED }; var vpc = new Vpc(this, "ClusterVPC", new VpcProps { MaxAzs = 2, // Высокая доступность в двух зонах доступности VpcName = "sample-cluster-vpc", SubnetConfiguration = [ new SubnetConfiguration { Name = "private", SubnetType = SubnetType.PRIVATE_ISOLATED, CidrMask = 24 } ], // VPC Gateway Endpoint для DynamoDB: Lambda может обращаться к DynamoDB без доступа в интернет. // Трафик остаётся внутри сети AWS, повышает безопасность и снижает задержки. GatewayEndpoints = new Dictionary<string, IGatewayVpcEndpointOptions> { { "DynamoDbEndpoint", new GatewayVpcEndpointOptions { Service = GatewayVpcEndpointAwsService.DYNAMODB, Subnets = [privateSubnets] } } } }); // --- Группы безопасности: правила «файрвола» --- // Раздельные группы безопасности следуют принципу наименьших привилегий. // Правила настроим позже, чтобы разрешить связь Lambda → RDS Proxy. var dbSg = new SecurityGroup(this, "DatabaseSecurityGroup", new SecurityGroupProps { SecurityGroupName = "db-sg", Vpc = vpc }); var lambdaSg = new SecurityGroup(this, "LambdaSecurityGroup", new SecurityGroupProps { SecurityGroupName = "lambda-sg", Vpc = vpc }); // --- Кластер RDS Aurora PostgreSQL --- // Aurora Serverless v2: автоматически масштабирует ёмкость в зависимости от нагрузки (здесь 0,5–1 ACU). // Платите только за то, что используете — идеально для переменных нагрузок. const string pgUser = "lambda"; const string? pgDatabaseName = "sample"; var pg = new DatabaseCluster(this, "DatabaseCluster", new DatabaseClusterProps { Engine = DatabaseClusterEngine.AuroraPostgres(new AuroraPostgresClusterEngineProps { Version = AuroraPostgresEngineVersion.VER_17_5 }), Writer = ClusterInstance.ServerlessV2("SampleDatabaseClusterWriter", new ServerlessV2ClusterInstanceProps { PubliclyAccessible = false, // Для PRIVATE_ISOLATED подсетей должно быть false EnablePerformanceInsights = false }), ServerlessV2MinCapacity = 0.5, ServerlessV2MaxCapacity = 1, Vpc = vpc, VpcSubnets = privateSubnets, SecurityGroups = [dbSg], Credentials = Credentials.FromGeneratedSecret(pgUser), // Пароль хранится в Secrets Manager DefaultDatabaseName = pgDatabaseName, EnableDataApi = true, RemovalPolicy = RemovalPolicy.DESTROY, // Только для демо — в продакшене используйте RETAIN DeletionProtection = false }); // --- RDS Proxy: пул соединений + IAM-аутентификация --- // Зачем RDS Proxy? Lambda может создавать много параллельных соединений. Без пула // Aurora быстро упрётся в max_connections. Proxy мультиплексирует соединения Lambda // в меньший пул и предотвращает ошибки «too many connections». // Lambda использует свою IAM-роль для генерации токена IAM-аутентификации. var pgProxy = new DatabaseProxy(this, "DatabaseClusterProxy", new DatabaseProxyProps { DbProxyName = "sample-db-proxy", ProxyTarget = ProxyTarget.FromCluster(pg), Vpc = vpc, VpcSubnets = privateSubnets, SecurityGroups = [dbSg], RequireTLS = true, // Требовать шифрованные соединения IamAuth = true, // Включить IAM-аутентификацию к базе (без паролей) Secrets = [pg.Secret!] // Proxy использует этот секрет для подключения к Aurora }); // Строка подключения указывает на endpoint RDS Proxy, а не напрямую на Aurora. // Lambda будет генерировать IAM-токены аутентификации во время выполнения (см. Api/Program.cs). PgConnectionString = new CfnOutput(this, "DatabaseConnectionString", new CfnOutputProps { Value = $"Host={pgProxy.Endpoint};Port=5432;Username={pgUser};Database={pgDatabaseName};Ssl Mode=Require;Trust Server Certificate=true;" }); // Правило группы безопасности: Lambda может подключаться к RDS Proxy по порту 5432 pgProxy.Connections.AllowFrom(lambdaSg, Port.POSTGRES, "Lambda to Proxy"); // --- IAM-роль для Lambda --- // Принцип наименьших привилегий: Lambda нужны CloudWatch Logs, VPC-сетевое взаимодействие, // RDS IAM-аутентификация и доступ к DynamoDB. Ничего лишнего. var lambdaRole = new Role(this, "LambdaRole", new RoleProps { RoleName = "sample-lambda-execution-role", AssumedBy = new ServicePrincipal("lambda.amazonaws.com"), ManagedPolicies = [ ManagedPolicy.FromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), ManagedPolicy.FromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole") ] }); // Выдаём точечные права: Lambda может генерировать токен IAM-аутентификации к RDS (RDS IAM auth token) и обращаться к таблице DynamoDB. // Без «звёздочек» и избыточно широких политик. pgProxy.GrantConnect(lambdaRole, pgUser); DynamoDbTable.GrantReadWriteData(lambdaRole); // --- Lambda: стратегия упаковки --- // Разделение Build и Runtime: собираем через .NET 9 SDK (самые свежие инструменты и оптимизации), // но деплоим в рантайм .NET 8 (актуальная поддержка AWS Lambda с LTS; .NET 10 выйдет в январе 2026). // Скрипт bundle-lambda.sh запускается внутри контейнера .NET 9 и делает: // 1. Устанавливает CLI Amazon.Lambda.Tools // 2. Запускает `dotnet lambda package` с настройками под Lambda // 3. Выкладывает function.zip, готовый к деплою var buildOption = new BundlingOptions { Image = Runtime.DOTNET_9.BundlingImage, // Для сборки: .NET 9 SDK User = "root", OutputType = BundlingOutput.ARCHIVED, Command = ["/bin/bash", "bundle-lambda.sh"], BundlingFileAccess = BundlingFileAccess.VOLUME_COPY }; // Находим корень решения (bundle-lambda.sh ожидает запуск из директории решения) var solutionPath = Path.GetDirectoryName(Path.GetDirectoryName(new Projects.Api().ProjectPath)!)!; var lambda = new Function(this, "Lambda", new FunctionProps { FunctionName = "sample-lambda-function", Runtime = Runtime.DOTNET_8, // Рантайм: .NET 8 (поддержка AWS Lambda) Handler = "Api", // Имя сборки (точка входа) Code = Code.FromAsset(solutionPath, new Amazon.CDK.AWS.S3.Assets.AssetOptions { Bundling = buildOption }), Role = lambdaRole, Vpc = vpc, VpcSubnets = privateSubnets, SecurityGroups = [lambdaSg], MemorySize = 512, Timeout = Duration.Seconds(10), Environment = new Dictionary<string, string> { // Lambda читает строку подключения из переменных окружения. // Api/Program.cs использует её, чтобы настроить Npgsql + IAM-аутентификацию. ["ConnectionStrings__Postgres"] = PgConnectionString.Value.ToString()! } }); // --- API Gateway: публичная HTTP-точка входа --- // HTTP API (v2) проще и дешевле, чем REST API (v1). // Универсальный маршрут /{proxy+} прокидывает ВСЕ запросы в Lambda. // Маршрутизацией внутри занимается приложение (ASP.NET Core) в Lambda. var httpApi = new HttpApi(this, "Api", new HttpApiProps { ApiName = "api", Description = "HTTP API" }); // Интеграция «catch-all»: API Gateway не нужно знать про маршруты. // /{proxy+} подходит под /users, /products/123 и т. п. // ASP.NET Core-приложение в Lambda маршрутизирует запросы через контроллеры/эндпоинты. httpApi.AddRoutes(new AddRoutesOptions { Path = "/{proxy+}", Methods = [Amazon.CDK.AWS.Apigatewayv2.HttpMethod.ANY], Integration = new HttpLambdaIntegration("LambdaIntegration", lambda) }); ApiUrl = new CfnOutput(this, "ApiUrl", new CfnOutputProps { Value = httpApi.Url!, }); } }
Подробности по скрипту сборки: скрипт bundle-lambda.sh запускается во время выполнения cdk deploy. Он устанавливает Amazon.Lambda.Tools, выполняет dotnet lambda package с оптимизациями под Lambda и кладёт function.zip в каталог /asset-output/. CDK загружает zip в S3, после чего CloudFormation создаёт/обновляет функцию Lambda, используя этот артефакт.
Наследование в стеках: SampleStack наследуется от SampleBaseStack. Это позволяет переиспользовать общие ресурсы (например, таблицу DynamoDB) между локальным и продакшен-стеками, а в производном классе добавлять инфраструктуру, специфичную для продакшена (VPC, Aurora, Lambda, API Gateway). Так уменьшается дублирование, а общие определения остаются в одном месте. Альтернатива — использовать вложенные стеки (nested stacks). Вложенные стеки позволяют композировать стеки из других стеков, улучшая повторное использование и модульность. Однако для простоты здесь достаточно наследования. В более сложных сценариях можно рассмотреть nested stacks.
Проект API: конфигурация с учётом окружения
Когда инфраструктура определена, нужно сделать так, чтобы код приложения понимал, в каком окружении он работает. CDK-стек создаёт инфраструктуру, но самому API нужно подключаться к правильным сервисам: к LocalStack при локальном запуске и к реальному AWS при деплое.
Program.cs подстраивается под окружение с помощью простого конфигурационного флага. UseLocalStack определяет, использовать ли эмулированные сервисы или реальные сервисы AWS (хотя можно ограничиться конфигурацией, при которой SDK использует LocalStack-эндпоинты там, где они заданы, а иначе обращается к реальным сервисам AWS). Кроме того, подключение к Postgres использует либо статический пароль (локально), либо токены IAM-аутентификации к БД (RDS/Aurora) через периодический провайдер пароля.
if (builder.Configuration.GetValue("LocalStack:UseLocalStack", false)) { // Локально: используем эндпоинты LocalStack и статический пароль для БД builder.Services.AddLocalStack(builder.Configuration); builder.Services.AddAWSServiceLocalStack<IAmazonDynamoDB>(); builder.Services.AddNpgsqlDataSource(builder.Configuration.GetConnectionString("Postgres")!); } else { // AWS: используем реальный AWS и IAM-токены для БД builder.Services.AddAWSService<IAmazonDynamoDB>(); builder.Services.AddNpgsqlDataSource(builder.Configuration.GetConnectionString("Postgres")!, b => { b.UsePeriodicPasswordProvider((cs, _) => ValueTask.FromResult(RDSAuthTokenGenerator.GenerateAuthToken(cs.Host, cs.Port, cs.Username)), TimeSpan.FromMinutes(10), // Обновление каждые 10 минут TimeSpan.FromSeconds(5)); // Повторная попытка через 5 секунд при ошибке }); }
Почему периодический провайдер пароля?
RDS Proxy с IAM-аутентификацией не использует статические пароли. Вместо этого Lambda генерирует временный токен аутентификации (действительный 15 минут), подписанный её IAM-учётными данными. Метод RDSAuthTokenGenerator.GenerateAuthToken создаёт этот токен по запросу, используя IAM-роль Lambda.
Периодический провайдер автоматически управляет обновлением токена:
Генерирует новый токен каждые 10 минут (до истечения 15 минут)
Прозрачно обновляет токен — код приложения видит обычное подключение и «не знает», что токены ротируются
Полностью убирает управление секретами (пароли не хранятся в переменных окружения, конфиг-файлах или секрет-хранилищах)
Локально Postgres-контейнер использует статический пароль из строки подключения (так проще для разработки). Запросы приложения при этом те же, меняется только стратегия аутентификации.
Развёртывание в AWS
Перед первым развёртыванием нужно выполнить bootstrap CDK в аккаунте и регионе AWS. Bootstrap — это одноразовая настройка, которая создаёт:
S3-бакет для хранения шаблонов
CloudFormationи пакетов деплоя LambdaIAM-роли, которые позволяют
CloudFormationсоздавать ресурсы от нашего имениECR-репозиторий для Docker-образов (если потребуется)
Если окружение AWS настроено и cdk установлен, достаточно выполнить команду bootstrap:
cdk bootstrap
Это нужно сделать один раз для каждой пары «аккаунт/регион». Если вы деплоите в нескольких регионах (например, us-east-1, eu-west-1), bootstrap нужно выполнить в каждом из них отдельно.
Что происходит во время bootstrap:
CDK создаёт стек
CloudFormationс именем CDKToolkitСоздаётся S3-бакет (с именем вроде
cdk-hnb659fds-assets-ACCOUNT-REGION) для хранения артефактов деплояСоздаются IAM-роли с правами на развёртывание инфраструктуры
Bootstrap-стек версионируется, благодаря чему CDK может со временем обновлять собственную инфраструктуру
Важно
CDK требует, чтобы в корне проекта был файл cdk.json. Если его нет, его можно создать командой
cdk init app --language csharp, а затем скопировать в корень проекта Aspire Host.В cdk.json должна быть указана корректная команда app, с помощью которой CDK запускает проект Aspire Host, чтобы сформировать CDK-стек. Пример содержимого:
"app": "dotnet run -- --publisher manifest --output-path ./manifest.json"
После выполнения bootstrap стек можно разворачивать. Одна команда переключает в режим публикации:
cdk deploy --outputs-file ./cdk-outputs.json
Когда мы запускаем команду развёртывания, процесс выглядит так, шаг за шагом:
Aspire оценивает/строит граф ресурсов в режиме публикации →
IsPublishMode = true, поэтому выполняется ветка для публикацииCDK синтезирует шаблон
CloudFormation→ CDK формирует шаблонCloudFormationиз инфраструктурного кода на C# и сохраняет его в каталогеcdk.out/вместе со всеми артефактами (например, ZIP-пакетами для Lambda)Код Lambda упаковывается → Docker-контейнер с .NET SDK запускает bundle-lambda.sh, который собирает проект Api с настройками под Lambda и упаковывает его в ZIP
CDK разворачивает стек →
CloudFormationсоздаёт все ресурсы: VPC (с изолированными подсетями, таблицами маршрутизации и группами безопасности), кластер Aurora Serverless v2 (с автоскейлингом), RDS Proxy (с настройкой IAM-аутентификации), таблицу DynamoDB, функцию Lambda (загружается из собранного ZIP), и HTTP API Gateway (с настройкой маршрутизации)Выводы стека отображаются → после завершения развёртывания CDK показывает выводы стека, которые мы определили:
ApiUrl(публичная точка входа для проверки) иDatabaseConnectionString(для диагностики)Выводы стека сохраняются в файл → опция
--outputs-fileсохраняет outputs вcdk-outputs.json
Первое развёртывание занимает 5–10 минут — в основном из-за ожидания, пока будет создан кластер Aurora. Последующие обновления гораздо быстрее (30–60 секунд), потому что CloudFormation обновляет только изменившиеся ресурсы. Если поменять только код Lambda, CloudFormation обновит лишь функцию, не трогая базу и VPC.
После развёртывания можно проверить API, отправляя HTTP-запросы на адрес ApiUrl через Postman, curl или прямо из браузера.
Почему этот подход работает
Один язык, весь стек. И логика приложения, и инфраструктура остаются в C#. Мы получаем IntelliSense, проверки на этапе компиляции и инструменты рефакторинга для инфраструктурного кода — то, чего нет в YAML или HCL.
Паритет dev/prod на уровне сервисов. Один и тот же Program.cs сервиса описывает оба окружения. Меняются только провайдеры (LocalStack против реальных сервисов AWS), а не топология. Это снижает вероятность проблем в духе «у меня локально работает».
Инфраструктура вокруг приложения. Инфраструктурный код живёт рядом с кодом приложения. Добавить новый сервис означает: добавить ссылку на проект + добавить фрагмент инфраструктуры в том же файле. Нет отдельного репозитория IaC, который нужно держать синхронизированным.
Упрощённый CI/CD. Одна команда разворачивает всё. PR может атомарно принести и изменения кода, и изменения инфраструктуры. Нет риска ситуации «инфраструктуру уже смёржили, а приложение ещё не выкатили» — или наоборот.
Текущие ограничения и дальнейшее направление
Почему сегодня нельзя «просто деплоить через Aspire»?
На сегодня (конец 2025 года) у Aspire нет встроенных паблишеров (publishers) для AWS, которые умеют:
собирать и упаковывать .NET-приложения (артефакты) в ZIP для рантайма Lambda
моделировать сервисы AWS через абстракции первого класса
описывать специфичные для AWS конструкции: RDS Proxy, VPC, VPC эндоинты, IAM-роли и т. д.
реализовывать продвинутые стратегии развёртывания (blue/green, canary)
AWS CDK уже решает эти задачи. Поэтому разделение ответственности такое:
Aspire оркестрирует: выбирает режим, связывает зависимости, управляет ссылками между сервисами
CDK разворачивает инфраструктуру: синтезирует шаблон CloudFormation, упаковывает артефакты и деплоит в AWS.
Что может улучшиться
Если Aspire эволюционирует и получит первоклассные абстракции ресурсов для типовых сервисов AWS (Aurora, DynamoDB, API Gateway, Lambda и т. д.), то этот паттерн можно будет упростить: меньше явных конструкций CDK и больше декларативных определений ресурсов на стороне Aspire. Но чтобы продуктивно работать уже сегодня, ждать этого будущего не обязательно.
Хорошая новость: в этом направлении уже идёт активная работа. AWS и команда .NET совместно улучшают интеграцию AWS с Aspire. Цель инициативы — первоклассная (нативная) поддержка ресурсов AWS, нативные примитивы развёртывания и более «гладкие» рабочие процессы — ровно те улучшения, о которых сказано выше.
Если после Aspire+CDK хочется систематизировать весь контур, обратите внимание на курс «DevOps практики и инструменты» — на нем разбираются IaC, CI/CD и управление конфигурацией на уровне приёмов и инструментов. Отдельно — артефакт-репозитории, работа с секретами и Observability: метрики, логи, трейсы. Пройдите бесплатное тестирование по курсу, чтобы оценить свои знания и навыки.
Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:
14 января 20:00. «IaC: Тестирование инфраструктуры — как внедрить инженерные практики и перестать бояться изменений». Записаться
22 января 19:00. «eBPF: рентгеновское зрение для production. Видим сеть, безопасность и узкие места на уровне ядра Linux». Записаться
29 января 20:00. «CI/CD: 90 минут от платформы до конвейера». Записаться
