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

  1. VPC с изолированными подсетями → без интернет-шлюза, без публичных IP

  2. Кластер Aurora Serverless v2 → Postgres с автоскейлингом (0,5–1 ACU)

  3. RDS Proxy → пул соединений + IAM-аутентификация для Lambda

  4. Таблица DynamoDB → таблица с ключом партиции (partition key)

  5. Функция Lambda → рантайм .NET 8, сборка через Docker (сейчас AWS поддерживает .NET 8 как встроенный рантайм)

  6. 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 и пакетов деплоя Lambda

  • IAM-роли, которые позволяют CloudFormation создавать ресурсы от нашего имени

  • ECR-репозиторий для Docker-образов (если потребуется)

Если окружение AWS настроено и cdk установлен, достаточно выполнить команду bootstrap:

cdk bootstrap

Это нужно сделать один раз для каждой пары «аккаунт/регион». Если вы деплоите в нескольких регионах (например, us-east-1, eu-west-1), bootstrap нужно выполнить в каждом из них отдельно.

Что происходит во время bootstrap:

  1. CDK создаёт стек CloudFormation с именем CDKToolkit

  2. Создаётся S3-бакет (с именем вроде cdk-hnb659fds-assets-ACCOUNT-REGION) для хранения артефактов деплоя

  3. Создаются IAM-роли с правами на развёртывание инфраструктуры

  4. 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

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

  1. Aspire оценивает/строит граф ресурсов в режиме публикацииIsPublishMode = true, поэтому выполняется ветка для публикации

  2. CDK синтезирует шаблон CloudFormation → CDK формирует шаблон CloudFormation из инфраструктурного кода на C# и сохраняет его в каталоге cdk.out/ вместе со всеми артефактами (например, ZIP-пакетами для Lambda)

  3. Код Lambda упаковывается → Docker-контейнер с .NET SDK запускает bundle-lambda.sh, который собирает проект Api с настройками под Lambda и упаковывает его в ZIP

  4. CDK разворачивает стекCloudFormation создаёт все ресурсы: VPC (с изолированными подсетями, таблицами маршрутизации и группами безопасности), кластер Aurora Serverless v2 (с автоскейлингом), RDS Proxy (с настройкой IAM-аутентификации), таблицу DynamoDB, функцию Lambda (загружается из собранного ZIP), и HTTP API Gateway (с настройкой маршрутизации)

  5. Выводы стека отображаются → после завершения развёртывания CDK показывает выводы стека, которые мы определили: ApiUrl (публичная точка входа для проверки) и DatabaseConnectionString (для диагностики)

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

Комментарии (0)