TL;DR
Статья показывает, как организовать локальную разработку .NET-сервиса с интеграцией AWS без моков и без постоянных походов в реальный AWS — за счёт связки .NET Aspire + LocalStack (переключение управляется конфигурацией).
Что делается:
В AppHost подключается
LocalStack.Aspire.Hosting, задаётся общий AWS-конфиг (профиль/регион) и включается флагLocalStack:UseLocalStack, чтобы режим «локально/прод» переключался конфигурацией.Через
AddLocalStack(...)хост Aspire поднимает LocalStack в контейнере, управляет временем жизни (Session/Persistent) и логированием для отладки.Инфраструктура описывается как код через AWS CDK на C#: создаётся S3-бакет со статическим хостингом и политикой доступа.
Выходные значения стека (например,
BucketNameиBucketWebsiteUrl) регистрируются типобезопасно и прокидываются в сервис через переменные окружения (Storage__*).Одна строка
builder.UseLocalStack(...)включает автоматическую конфигурацию AWS-ресурсов под LocalStack, обнаружение CDK/CloudFormation (включая bootstrap при необходимости) и правильный порядок зависимостей.В API-сервисе подключаются
LocalStack.Clientи AWS SDK, регистрируетсяIAmazonS3, после чего тот же код ходит либо в LocalStack, либо в AWS — в зависимости от конфигурации.В конце — минимальный пример API с эндпоинтами
/upload(загрузка в S3) и/download/{key}, плюс проверка черезcurlи AWS CLI с--endpoint-url.
Разработка .NET-приложения, использующего сервисы AWS, сопровождается множеством сложностей. Либо мы постоянно ходим в настоящий AWS и наблюдаем, как растёт счёт, либо всё замокаем и не уверены, что код вообще работает. Команды часто делят один dev-стенд, мешая друг другу данными и отладкой. Разработчики регулярно оказываются в ситуации, когда приходится ждать, пока «освободится дев-окружение», потому что кто-то другой как раз тестирует свою интеграцию. .NET Aspire помогает с оркестрацией микросервисов, но не решает проблему разработки именно с AWS.
Здесь в игру вступает LocalStack. По сути, это AWS, который запускается у нас на машине: S3, Lambda, DynamoDB, SQS, SNS и т.д. Community-версия бесплатна и покрывает почти всё, что нужно для типовой разработки. Больше не нужно поднимать отдельные AWS-окружения для каждого разработчика или разбираться с очисткой ресурсов в разных аккаунтах. К тому же, мы получаем более быстрые циклы обратной связи, возможность работать офлайн и возможность полностью «обнулить» окружение простым перезапуском контейнера.
В этой статье мы рассмотрим практический пример, с которым постоянно сталкиваются разработчики: API-сервис, который загружает файлы в Amazon S3 и раздаёт их через статический хостинг сайта. Такой паттерн встречается повсюду: загрузка аватарок, хранение документов, медиагалереи и т.п. Мы настроим всё через .NET Aspire и LocalStack так, чтобы один и тот же код без изменений работал и локально, и в продакшене.
Прежде чем переходить к настройке, зафиксируем исходную точку. У нас есть приложение .NET Aspire с одним API-сервисом. Сервис предоставляет HTTP-эндпоинты. Структура решения выглядит так (стандартная структура Aspire):
AppHost — проект хоста Aspire
Program.cs — точка входа хоста Aspire
appsettings.json — файл конфигурации хоста Aspire
другие файлы хоста Aspire...
Api
Program.cs — точка входа API-сервиса
appsettings.json — файл конфигурации API-сервиса
другие файлы API-сервиса...
ServiceDefaults — общая конфигурация сервисов
Extensions.cs — общие расширения для сервисовAppHost/Program.cs:
var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.Api>("api")
.WithHttpHealthCheck("/health");
await builder.Build().RunAsync();Api/Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
var app = builder.Build();
app.MapDefaultEndpoints();
// Endpoint для проверки состояния
app.MapGet("/health", () => "Healthy");
app.Run();С такой структурой мы можем расширить AppHost и интегрировать в него LocalStack и сервисы AWS. Для начала нужно добавить в проект AppHost пакет Aspire для работы с LocalStack — LocalStack.Aspire.Hosting — через NuGet. В файле AppHost.csproj это выглядит так:
<PackageReference Include="LocalStack.Aspire.Hosting" />Этот пакет включает всё необходимое: управление контейнером LocalStack, интеграцию с AWS CDK и оркестрацию через CloudFormation.
Дальше нам нужно настроить AWS SDK, добавив общий контекст конфигурации AWS:
var awsConfig = builder.AddAWSSDKConfig()
.WithProfile("default")
.WithRegion(RegionEndpoint.EUCentral1);Профиль использует стандартную цепочку учётных данных AWS: для локальной разработки можно применять профили AWS CLI, а в продакшене — роли IAM. Регион гарантирует, что будут использованы корректные эндпоинты. Большей гибкости можно достичь за счёт конфигурации:
var awsConfig = builder.AddAWSSDKConfig()
.WithProfile(builder.Configuration["AWS:Profile"] ?? "default")
.WithRegion(RegionEndpoint.GetBySystemName(builder.Configuration["AWS:Region"] ?? "eu-central-1"));Далее нужно настроить сам LocalStack. Мы скажем Aspire, чтобы он управлял контейнером LocalStack за нас, настроим время жизни контейнера и уровни логирования для отладки:
var awsLocal = builder.AddLocalStack("aws-local", // Имя инстанса LocalStack
awsConfig: awsConfig, // Ссылка на конфигурацию AWS, созданную выше
configureContainer: c => // Настройка параметров контейнера LocalStack
{
c.Lifetime = ContainerLifetime.Session; // Сбрасывать контейнер при каждом рестарте приложения
c.DebugLevel = 1; // Включить детализированное логирование
c.LogLevel = LocalStackLogLevel.Debug; // Показывать отладочную информацию
});Ключевые настройки здесь:
Lifetime(время жизни):
-Session: контейнер пересоздаётся при каждом рестарте приложения (подходит по умолчанию для тестов)
-Persistent: контейнер переживает перезапуски приложения, сохраняя данныеLogLevel: позволяет видеть, что именно происходит при вызовах AWSDebugLevel: значение1— детализированные логи запросов/ответов для отладки
Режим Persistent (постоянное время жизни контейнера) важен: мы не хотим заново создавать S3-бакеты каждый раз, когда перезапускаем отладку. Пока Session-режим удобен для тестовых стендов, Persistent лучше подходит для повседневной разработки.
Важная часть здесь — файл конфигурации AppHost. Метод AddLocalStack использует настройку LocalStack:UseLocalStack, чтобы понять, нужно ли работать в локальном режиме. Он ищет это значение в конфигурации (appsettings.json, переменная окружения LocalStack__UseLocalStack и т.д.). Поэтому в AppHost/appsettings.json можно добавить:
{
"LocalStack": {
"UseLocalStack": true
}
}Без этой настройки интеграция с LocalStack будет отключена, и наши сервисы попытаются подключаться к реальному AWS. Такой подход упрощает переключение между локальной разработкой и продакшеном без изменений в коде.
После этого мы можем начать описывать инфраструктуру AWS. Мы можем определить инфраструктуру AWS как код, и она будет работать как с LocalStack, так и с реальным AWS. Для этого мы будем использовать AWS CDK (Cloud Development Kit). Проще всего думать о нём как о способе описывать инфраструктуру на привычных языках программирования, а не в YAML или JSON. CDK позволяет объявлять ресурсы AWS — такие как S3-бакеты, очереди SQS или функции Lambda — с помощью C#-классов, с поддержкой IntelliSense, типобезопасностью и всеми плюсами полноценного языка. При сборке CDK-кода генерируются шаблоны CloudFormation, которые затем может разворачивать AWS (или LocalStack).
var awsStack = builder.AddAWSCDKStack("aws-stack", s => new AwsStack(s))
.WithReference(awsConfig); // Связать со сконфигурированным AWS
var awsStackOutputs = AwsStackOutputs.Create(awsStack); // Получить типобезопасный доступ к выходным значениям стекаAwsStack — это кастомный класс, в котором мы описываем ресурсы AWS. Чтобы задать реальную инфраструктуру, вот как можно создать бакет Amazon S3 с включённым статическим хостингом сайта:
public sealed class AwsStack : Amazon.CDK.Stack
{
// Пробрасываем S3-бакет как свойство, чтобы использовать его в выходных значениях
public IBucket Bucket { get; }
public AwsStack(Constructs.Construct scope)
: base(scope, "bucket")
{
// Создаём S3-бакет с включённым статическим хостингом
Bucket = new Bucket(this,
"bucket",
new BucketProps
{
BucketName = "test-data-bucket",
WebsiteIndexDocument = "index.html" // Включаем статический хостинг сайта
});
// Разрешаем публичное чтение объектов для статического хостинга
Bucket.AddToResourcePolicy(new PolicyStatement(new PolicyStatementProps
{
Actions = ["s3:GetObject"], // Разрешить публичное чтение объектов
Effect = Effect.ALLOW, // Разрешить доступ
Principals = [new AnyPrincipal()], // Любой клиент может обратиться
Resources = [Bucket.ArnForObjects("*")] // Все объекты в бакете
}));
}
}
// Класс AwsStackOutputs даёт типобезопасный доступ к ресурсам стека.
// Вместо того чтобы работать с «сырыми» строками в Program.cs, мы получаем проверку на этапе компиляции
// и защищаемся от опечаток, которые могли бы привести к ошибкам уже во время выполнения.
public sealed class AwsStackOutputs
{
private readonly IResourceBuilder<IStackResource<AwsStack>> _stack;
private AwsStackOutputs(IResourceBuilder<IStackResource<AwsStack>> stack)
{
_stack = stack;
}
// Выходное значение с именем бакета
public StackOutputReference BucketName => _stack.GetOutput(nameof(BucketName));
// Выходное значение с URL сайта на базе бакета
public StackOutputReference BucketWebsiteUrl => _stack.GetOutput(nameof(BucketWebsiteUrl));
// Фабричный метод для создания AwsStackOutputs и регистрации выходных значений
public static AwsStackOutputs Create(IResourceBuilder<IStackResource<AwsStack>> stack)
{
stack.AddOutput(nameof(BucketName), s => s.Bucket.BucketName);
stack.AddOutput(nameof(BucketWebsiteUrl), s => s.Bucket.BucketWebsiteUrl);
return new AwsStackOutputs(stack);
}
}Конфигурация бакета настраивает статический хостинг сайта с публичным доступом к контенту. Политика доступа к ресурсу позволяет публичный доступ к объектам, при этом сам бакет остаётся приватным. Аналогично мы можем определять и другие ресурсы AWS по мере необходимости. Например, для очередей Amazon SQS мы можем вызвать new Queue(this, "queue", new QueueProps { ... }) и отдать наружу такие значения, как QueueUrl и/или QueueArn. Для таблиц Amazon DynamoDB мы можем определить new Table(this, "table", new TableProps { ... }) и экспортировать, например, TableName.
Когда инфраструктура определена, мы можем сослаться на эти ресурсы в наших сервисах. Вот как настроить API-сервис для использования созданного нами бакета Amazon S3:
var apiService = builder.AddProject<Projects.ApiService>("api")
.WithHttpHealthCheck("/health")
.WithReference(awsStack) // Ссылаемся на AWS-стек для автоматической конфигурации
.WithEnvironment("Storage__BucketName", awsStackOutputs.BucketName) // Прокидываем имя бакета в конфиг
.WithEnvironment("Storage__PublicBaseUrl", awsStackOutputs.BucketWebsiteUrl); // Прокидываем URL бакета в конфигВызовы WithEnvironment(...) пробрасывают эти значения как переменные окружения. Storage__BucketName в конфигурации сервиса превращается в Storage:BucketName.
Важно отметить, что WithReference(awsStack) автоматически добавит выходные значения ресурсов AWS в секцию конфигурации AWS__Resources__*, так что мы тоже можем обращаться к ним напрямую. Однако явная конфигурация вроде Storage:BucketName в самом сервисе часто предпочтительнее, чем опора на «общее» хранилище выходных значений. Такой подход дополнительно развязывает конфигурацию сервиса и конкретную реализацию инфраструктуры, позволяя менять инфраструктуру, не затрагивая код сервиса.
Непосредственно перед сборкой хоста нам нужно включить интеграцию с LocalStack:
// Enable LocalStack integration for all services which reference AWS resources
builder.UseLocalStack(awsLocal);Эта одна строка проделывает очень большую работу. Она настраивает все ресурсы AWS в приложении так, чтобы они использовали указанный инстанс LocalStack, автоматически обнаруживает шаблоны CloudFormation и CDK-стеки и при необходимости выполняет CDK bootstrap. Этот метод сканирует все ресурсы в приложении и автоматически конфигурирует ресурсы AWS и проекты, которые на них ссылаются, для работы с LocalStack в локальной среде. Он:
Обнаруживает все шаблоны CloudFormation и ресурсы CDK-стеков
Автоматически создаёт ресурс CDK bootstrap, если присутствуют CDK-стеки
Настраивает все ресурсы AWS на использование эндпоинтов LocalStack
Выстраивает правильный порядок зависимостей для CDK bootstrap
Автоматически конфигурирует проекты, которые ссылаются на ресурсы AWS
Добавляет аннотации, чтобы избежать повторной конфигурации ресурсов
Вот здесь начинается самое интересное. Когда мы вызываем WithReference(awsStack) и включаем LocalStack, Aspire автоматически подставляет конфигурацию в наши сервисы. Нам не приходится управлять этим вручную.
Сервис автоматически получает все эти переменные окружения:
Конфигурация LocalStack (пример):
LocalStack__UseLocalStack = True
LocalStack__Config__EdgePort = 33895
LocalStack__Config__LocalStackHost = localhost
LocalStack__Config__UseLegacyPorts = False
LocalStack__Config__UseSsl = False
LocalStack__Session__AwsAccessKey = secretKey
LocalStack__Session__AwsAccessKeyId = accessKey
LocalStack__Session__AwsSessionToken = token
LocalStack__Session__RegionName = eu-central-1Ссылки на ресурсы AWS (пример):
AWS__Resources__ProfileBucketName = test-data-bucket
AWS__Resources__ProfileBucketWebsiteUrl = http://test-data-bucket.s3-website.localhost:33895Конфигурация приложения (пример):
Storage__BucketName = test-data-bucket
Storage__PublicBaseUrl = http://test-data-bucket.s3-website.localhost:33895Всё это отображается в эквивалентную JSON-конфигурацию:
{
"LocalStack": {
"UseLocalStack": true,
"Config": {
"EdgePort": 33895,
"LocalStackHost": "localhost",
"UseLegacyPorts": false,
"UseSsl": false
},
"Session": {
"AwsAccessKey": "secretKey",
"AwsAccessKeyId": "accessKey",
"AwsSessionToken": "token",
"RegionName": "eu-central-1"
}
},
"AWS": {
"Resources": {
"ProfileBucketName": "test-data-bucket",
"ProfileBucketWebsiteUrl": "http://test-data-bucket.s3-website.localhost:33895"
}
},
"Storage": {
"BucketName": "test-data-bucket",
"PublicBaseUrl": "http://test-data-bucket.s3-website.localhost:33895"
}
}Интересно и то, что LocalStack автоматически обрабатывает переписывание URL для website-эндпоинтов Amazon S3. Когда мы обращаемся, например, по адресу http://test-data-bucket.s3-website.localhost:33895, LocalStack понимает, к какому S3-бакету в контейнере нужно направить запрос. Поэтому если приложение загружает объект в S3, а затем формирует URL, используя website-URL бакета, всё будет прозрачно работать с LocalStack без дополнительной конфигурации. Аналогично мы можем использовать Amazon CloudFront поверх S3, создав объект Distribution — это также будет работать с LocalStack.
Итак, после всех этих настроек финальная версия Program.cs в нашем AppHost может выглядеть так:
using Amazon;
using Amazon.CDK.AWS.IAM;
using Amazon.CDK.AWS.S3;
using Aspire.Hosting.AWS.CDK;
using Aspire.Hosting.AWS.CloudFormation;
using Aspire.Hosting.LocalStack.Container;
var builder = DistributedApplication.CreateBuilder(args);
// Ресурсы AWS
var awsConfig = builder.AddAWSSDKConfig()
.WithProfile("default")
.WithRegion(RegionEndpoint.EUCentral1);
var awsLocal = builder.AddLocalStack("aws-local",
awsConfig: awsConfig,
configureContainer: c =>
{
c.Lifetime = ContainerLifetime.Session;
c.DebugLevel = 1;
c.LogLevel = LocalStackLogLevel.Debug;
});
var awsStack = builder.AddAWSCDKStack("aws-stack", s => new AwsStack(s))
.WithReference(awsConfig);
var awsStackOutputs = AwsStackOutputs.Create(awsStack);
// Сервисы
builder.AddProject<Projects.Api>("api")
.WithHttpHealthCheck("/health")
.WithReference(awsStack)
.WithEnvironment("Storage__BucketName", awsStackOutputs.BucketName)
.WithEnvironment("Storage__PublicBaseUrl", awsStackOutputs.BucketWebsiteUrl);
// Запуск
builder.UseLocalStack(awsLocal);
await builder.Build().RunAsync();
public sealed class AwsStack : Amazon.CDK.Stack
{
public IBucket Bucket { get; }
public AwsStack(Constructs.Construct scope)
: base(scope, "bucket")
{
Bucket = new Bucket(this,
"bucket",
new BucketProps
{
BucketName = "test-data-bucket",
WebsiteIndexDocument = "index.html" // Включаем статический хостинг сайта
});
Bucket.AddToResourcePolicy(new PolicyStatement(new PolicyStatementProps
{
Actions = ["s3:GetObject"],
Effect = Effect.ALLOW,
Principals = [new AnyPrincipal()],
Resources = [Bucket.ArnForObjects("*")]
}));
}
}
public sealed class AwsStackOutputs
{
private readonly IResourceBuilder<IStackResource<AwsStack>> _stack;
private AwsStackOutputs(IResourceBuilder<IStackResource<AwsStack>> stack)
{
_stack = stack;
}
public StackOutputReference BucketName => _stack.GetOutput(nameof(BucketName));
public StackOutputReference BucketWebsiteUrl => _stack.GetOutput(nameof(BucketWebsiteUrl));
public static AwsStackOutputs Create(IResourceBuilder<IStackResource<AwsStack>> stack)
{
stack.AddOutput(nameof(BucketName), s => s.Bucket.BucketName);
stack.AddOutput(nameof(BucketWebsiteUrl), s => s.Bucket.BucketWebsiteUrl);
return new AwsStackOutputs(stack);
}
}А теперь, когда мы запускаем AppHost, Aspire поднимает LocalStack, разворачивает в нём CDK-стек и прокидывает всю необходимую конфигурацию в API-сервис. На скриншоте дашборда Aspire можно увидеть запущенный LocalStack и задеплоенный CDK-стек.

Теперь AppHost настроен. Пришло время настроить сам сервис для работы с AWS SDK через LocalStack. Сначала добавим в сервис необходимые зависимости: LocalStack.Client, LocalStack.Client.Extensions, AWSSDK.S3. В файле Api.csproj это выглядит так:
<PackageReference Include="AWSSDK.S3" />
<PackageReference Include="LocalStack.Client" />
<PackageReference Include="LocalStack.Client.Extensions" />Затем в Program.cs API-сервиса добавляем интеграцию с LocalStack и регистрируем клиент S3:
// Настраиваем интеграцию LocalStack и AWS SDK
builder.Services.AddLocalStack(builder.Configuration); // Читаем конфигурацию LocalStack, которую подставил Aspire
builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions()); // Настраиваем AWS SDK
builder.Services.AddAwsService<IAmazonS3>(); // Регистрируем клиент S3 в контейнере внедрения зависимостейВот и всё. Метод AddLocalStack(...) читает всю конфигурацию, которую Aspire уже подставил, и настраивает AWS SDK так, чтобы он ходил в LocalStack. Клиент S3 теперь автоматически указывает на LocalStack. Дополнительно регистрация клиента AWS SDK через AddAwsService<IAmazonS3>() использует сконфигурированные AWS-опции, которые уже включают эндпоинты LocalStack.
Вот как выглядит код простого API-сервиса (у нас есть два эндпоинта: один для загрузки файлов в Amazon S3 и второй — для скачивания файлов из S3; это упрощённый тестовый функционал, демонстрирующий интеграцию с S3):
// УПРОЩЁННЫЙ ТЕСТОВЫЙ КОД — НЕ ИСПОЛЬЗОВАТЬ В ПРОДАКШЕНЕ
// ПРЕДНАЗНАЧЕН ТОЛЬКО ДЛЯ ДЕМОНСТРАЦИИ ИНТЕГРАЦИИ
using Amazon.S3;
using Amazon.S3.Model;
using LocalStack.Client.Extensions;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Настраиваем интеграцию LocalStack и AWS SDK
builder.Services.AddLocalStack(builder.Configuration);
builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions());
builder.Services.AddAwsService<IAmazonS3>();
// Регистрируем конфигурацию хранилища
builder.Services.Configure<StorageOptions>(builder.Configuration.GetSection("Storage"));
var app = builder.Build();
app.MapDefaultEndpoints();
// Эндпоинт проверки состояния
app.MapGet("/health", () => "Healthy");
// Эндпоинт загрузки файла
app.MapPost("/upload", async (
IFormFile file,
IAmazonS3 s3Client,
IOptions<StorageOptions> storageOptions) =>
{
var bucketName = storageOptions.Value.BucketName;
// Для простоты используем исходное имя файла как ключ в S3 (НЕБЕЗОПАСНО для продакшена)
var key = file.FileName;
// Открываем поток исходного файла
await using var stream = file.OpenReadStream();
// Загружаем файл в S3
await s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = bucketName,
Key = key,
InputStream = stream,
ContentType = file.ContentType
});
return Results.Ok(new
{
Url = new Uri(storageOptions.Value.PublicBaseUrl, key)
});
})
// В этом примере отключаем защиту от подделки запросов (antiforgery) для упрощения
.DisableAntiforgery();
// Эндпоинт скачивания файла
app.MapGet("/download/{key}", async (
string key,
IAmazonS3 s3Client,
IOptions<StorageOptions> storageOptions) =>
{
var bucketName = storageOptions.Value.BucketName;
try
{
var response = await s3Client.GetObjectAsync(bucketName, key);
return Results.File(response.ResponseStream, response.Headers["Content-Type"], key);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return Results.NotFound();
}
});
app.Run();
// Класс конфигурации хранилища
// Соответствует секции Storage в конфигурации:
// Storage:BucketName и Storage:PublicBaseUrl
public sealed class StorageOptions
{
public string BucketName { get; init; } = null!;
public Uri PublicBaseUrl { get; init; } = null!;
}Вся прелесть здесь в том, что клиент LocalStack автоматически переключается на реальный AWS, когда LocalStack не включён. Нам не нужны никакие условные регистрации — просто везде используем AddAwsService<...>(), а уже конфигурация решает, куда пойдут вызовы. LocalStack Client использует подставленные настройки (параметры с префиксом LocalStack__ из примера выше, особенно LocalStack__UseLocalStack), чтобы направлять запросы либо в LocalStack, если он включён, либо в настоящий AWS — если нет.
Теперь мы можем снова запустить наше приложение на Aspire и протестировать API-сервис. Для загрузки и скачивания файлов подойдут инструменты вроде Postman или curl. Поскольку LocalStack запущен, все операции с Amazon S3 выполняются локально, не затрагивая реальный AWS. Мы даже можем воспользоваться AWS CLI, чтобы посмотреть содержимое S3-бакета в LocalStack. В моём случае это выглядит так:
# Загрузить файл с помощью curl
curl -v "http://localhost:22456/upload" -F "file=@index.html"
# * Trying 127.0.0.1:22456...
# * Connected to localhost (127.0.0.1) port 22456 (#0)
# > POST /upload HTTP/1.1
# > Host: localhost:22456
# > User-Agent: curl/7.87.0
# > Accept: */*
# > Content-Length: 326
# > Content-Type: multipart/form-data; boundary=------------------------90474ff5b4693aae
# >
# * Файл полностью загружен, всё в порядке
# * Помечаем этот набор как не поддерживающий повторное использование соединения
# < HTTP/1.1 200 OK
# < Content-Type: application/json; charset=utf-8
# < Date: Wed, 02 Nov 2025 16:44:05 GMT
# < Server: Kestrel
# < Transfer-Encoding: chunked
# <
# * Соединение #0 с хостом localhost оставлено открытым
# {"url":"http://test-data-bucket.s3-website.localhost:33895/index.html"}%
# Посмотреть список объектов в S3-бакете с помощью AWS CLI
aws --endpoint-url="http://localhost:33895" s3 ls s3://test-data-bucket
# 2025-11-02 17:44:05 139 index.html
# Скачать файл с помощью curl
curl -v "http://localhost:22456/download/index.html" -o downloaded_index.html
Полезные ссылки

Если Aspire и LocalStack закрывают «как запустить», то дальше обычно упираешься в границы сервисов, контракты, отказоустойчивость и наблюдаемость. На курсе Microservice Architecture разбираем паттерны и инструменты микросервисов на реальных распределённых системах. Чтобы узнать, подойдет ли вам программа, пройдите вступительный тест.
Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:
14 января, 20:00. «IaC: Тестирование инфраструктуры — как внедрить инженерные практики и перестать бояться изменений». Записаться
21 января, 20:00. «Мониторинг: как понять, что твой сервис болен». Записаться
22 января, 19:00. «eBPF: рентгеновское зрение для production. Видим сеть, безопасность и узкие места на уровне ядра Linux». Записаться
