В прошлой статье я настроил локальный цикл разработки с 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 минут от платформы до конвейера». Записаться