.NET Aspire: 올바른 방법으로 클라우드 네이티브 애플리케이션 구축
.NET에서 분산 애플리케이션을 구축해 본 적이 있다면 훈련 방법을 알고 계실 것입니다. 웹 API를 가동하고, 백그라운드 작업자를 추가하고, 캐싱을 위해 Redis를, 지속성을 위해 PostgreSQL을, 메시징을 위해 RabbitMQ를 추가하면 갑자기 비즈니스 로직을 작성하는 것보다 인프라 연결에 더 많은 시간을 소비하게 됩니다. appsettings.json 파일에 흩어져 있는 연결 문자열, 구성하는 것을 잊은 상태 확인, 항상 “다음 스프린트의 문제"인 관찰 가능성.
나는 거기에 있었다. 내가 인정하고 싶은 것보다 더 많이.
이것이 바로 .NET Aspire가 해결하도록 설계된 문제입니다. 몇 달 동안 프로덕션 환경에서 실행한 후 제가 배운 좋은 점, 좋은 점, 문제점 등을 공유하고 싶습니다.
.NET Aspire란 무엇입니까?
.NET Aspire는 .NET을 사용하여 관찰 가능하고 생산 준비가 완료된 분산 애플리케이션을 구축하기 위한 독보적인 스택입니다. 이는 전통적인 의미의 프레임워크가 아닙니다. ASP.NET Core를 대체하거나 새로운 프로그래밍 모델을 강제로 적용하지 않습니다. 대신, 이미 알고 있는 내용을 바탕으로 클라우드 네이티브 앱을 구축할 때 항상 존재했던 격차를 메웁니다.
Aspire는 기본적으로 네 가지 기능을 제공합니다.
- AppHost — 전체 분산 애플리케이션 토폴로지를 정의하는 프로젝트입니다. 어떤 서비스가 존재하는지, 무엇에 의존하는지, 어떻게 연결되는지 알아보세요.
- 서비스 기본값 — 모든 서비스에 대해 상태 확인, 탄력성 정책, OpenTelemetry와 같은 교차 관심사를 한 번에 구성하는 공유 프로젝트입니다.
- 구성 요소 — Redis, PostgreSQL, RabbitMQ, Azure Storage 등과 같은 지원 서비스와의 표준화된 통합을 제공하는 NuGet 패키지입니다.
- 개발자 대시보드 — 로컬 개발 중에 전체 배포 앱에 대한 로그, 추적 및 측정항목을 표시하는 실시간 UI입니다.
철학은 간단합니다. 모든 .NET 클라우드 앱에 이러한 기능이 필요하다면 왜 매번 처음부터 구현해야 할까요?
첫 번째 Aspire 프로젝트 설정
시작하는 방법은 간단합니다. .NET 8 이상이 필요하고 Aspire 워크로드가 설치되어 있어야 합니다.
dotnet workload update
dotnet workload install aspire
이제 새로운 Aspire 시작 프로젝트를 만듭니다.
dotnet new aspire-starter -n MyCloudApp
그러면 다음 네 가지 프로젝트가 포함된 솔루션이 생성됩니다.
MyCloudApp.AppHost— 오케스트레이터MyCloudApp.ServiceDefaults— 공유 구성MyCloudApp.ApiService— 샘플 웹 APIMyCloudApp.Web— Blazor 프런트엔드
AppHost를 실행하면 브라우저에 Aspire 대시보드가 열려 모든 서비스, 로그 및 상태를 즉시 볼 수 있습니다. Docker Compose 파일이 없습니다. 수동 포트 관리가 없습니다. 그냥 작동합니다.
dotnet run --project MyCloudApp.AppHost
그 첫 경험이 나를 매료시켰습니다. 1분 안에 관찰 기능이 내장된 완벽하게 조정된 분산 앱이 완성됩니다.
AppHost 패턴
AppHost는 마법이 살아있는 곳입니다. 이는 빌더 패턴을 사용하여 분산 애플리케이션의 토폴로지, 즉 존재하는 리소스와 서비스가 리소스에 연결되는 방식을 정의하는 작은 콘솔 애플리케이션입니다.
현실적인 AppHost의 모습은 다음과 같습니다.
var builder = DistributedApplication.CreateBuilder(args);
// Infrastructure resources
var postgres = builder.AddPostgres("postgres")
.WithPgAdmin()
.AddDatabase("catalogdb");
var redis = builder.AddRedis("cache");
var rabbitmq = builder.AddRabbitMQ("messaging")
.WithManagementPlugin();
// Application services
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
.WithReference(postgres)
.WithReference(redis)
.WithReference(rabbitmq);
var orderApi = builder.AddProject<Projects.OrderApi>("order-api")
.WithReference(postgres)
.WithReference(rabbitmq);
var frontend = builder.AddProject<Projects.WebFrontend>("frontend")
.WithExternalHttpEndpoints()
.WithReference(catalogApi)
.WithReference(orderApi);
builder.Build().Run();
```해당 코드를 소리내어 읽어보세요. 실제로는 그 자체를 문서화합니다. "카탈로그 API는 PostgreSQL, Redis 및 RabbitMQ를 참조합니다. 프런트엔드는 카탈로그 API 및 주문 API를 참조합니다." 이것이 코드로 표현된 아키텍처입니다.
주목할 만한 몇 가지 사항:
- **`WithReference`** 무거운 작업을 수행합니다. 환경 변수 및 구성을 통해 연결 문자열과 서비스 URL을 소비 프로젝트에 자동으로 삽입합니다. 귀하의 서비스는 Redis가 *어디* 실행되고 있는지 알 필요가 없습니다. Aspire가 이를 처리합니다.
- **`WithPgAdmin()`** 및 **`WithManagementPlugin()`** 실제 서비스와 함께 PostgreSQL 및 RabbitMQ용 관리 UI를 실행합니다. 개발하는 동안 이는 매우 중요합니다.
- **`WithExternalHttpEndpoints()`** 서비스를 외부에서 액세스할 수 있는 것으로 표시하며 이는 배포 시 중요합니다.
### 리소스 구성
세분화된 제어를 통해 리소스를 구성할 수 있습니다.
```csharp
var postgres = builder.AddPostgres("postgres")
.WithEnvironment("POSTGRES_MAX_CONNECTIONS", "200")
.WithDataVolume("postgres-data")
.AddDatabase("catalogdb");
var redis = builder.AddRedis("cache")
.WithDataVolume("redis-data");
데이터 볼륨은 로컬 개발 데이터가 컨테이너를 다시 시작해도 유지되도록 보장합니다. 작은 디테일로 삶의 질이 크게 향상됩니다.
서비스 기본값: 알려지지 않은 영웅
ServiceDefaults 프로젝트는 Aspire에서 가장 과소평가된 부분입니다. 이는 솔루션의 모든 서비스가 참조하는 공유 라이브러리이며, 잊어버리거나 일관되지 않게 구현할 수 있는 모든 교차 문제를 구성합니다.
ServiceDefaults의 일반적인 Extensions.cs는 다음과 같습니다.
public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}
public static IHostApplicationBuilder ConfigureOpenTelemetry(
this IHostApplicationBuilder builder)
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddGrpcClientInstrumentation();
});
builder.AddOpenTelemetryExporters();
return builder;
}
public static IHostApplicationBuilder AddDefaultHealthChecks(
this IHostApplicationBuilder builder)
{
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(),
["live"]);
return builder;
}
}
그런 다음 각 서비스의 Program.cs에서 한 줄로 모든 작업을 수행합니다.
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Your service-specific configuration...
var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();
단일 AddServiceDefaults() 호출은 다음을 제공합니다.
- 구조화된 로깅, 측정항목 및 분산 추적 기능을 갖춘 OpenTelemetry
- 활성 및 준비 엔드포인트를 통한 상태 확인
- 서비스 검색 - 서비스가 이름으로 서로를 찾을 수 있음
- 나가는 모든 HTTP 호출에 대한 복원력 정책(재시도, 회로 차단기, 시간 초과)
이것은 “내 컴퓨터에서 작동하는” 데모와 프로덕션 준비 시스템을 구분하는 요소입니다. 그리고 Aspire는 이를 나중에 고려하지 않고 기본값으로 설정합니다.
Aspire 구성요소
Aspire 구성 요소는 서비스가 지원 인프라에 연결되는 방식을 표준화하는 NuGet 패키지입니다. 이는 단순한 클라이언트 라이브러리 그 이상입니다. 상태 확인, 로깅, 추적 및 즉시 구성 가능한 복원력을 포함합니다.
레디스
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddRedisDistributedCache("cache");
그게 다야. 연결 문자열은 WithReference를 통해 AppHost에서 가져옵니다. 구성 요소는 상태 확인 및 OpenTelemetry 계측이 이미 연결되어 있는 Redis에서 지원하는 IDistributedCache를 등록합니다.
출력 캐싱에 Redis를 사용할 수도 있습니다.
builder.AddRedisOutputCache("cache");
Entity Framework Core가 포함된 PostgreSQL
builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb", settings =>
{
settings.DisableRetry = false;
});
그러면 AppHost에 정의된 catalogdb 데이터베이스에 연결하여 DbContext가 등록됩니다. 여기에는 연결 풀링, 상태 확인 및 재시도 정책이 포함됩니다.
토끼MQ
builder.AddRabbitMQClient("messaging");
완전히 구성되고 상태가 확인된 RabbitMQ 클라이언트 라이브러리에서 IConnection를 등록합니다.
Azure 통합
Aspire에는 Azure 서비스를 위한 최고 수준의 구성 요소도 있습니다.
// Azure Blob Storage
builder.AddAzureBlobClient("blobs");
// Azure Service Bus
builder.AddAzureServiceBusClient("servicebus");
// Azure Key Vault
builder.AddAzureKeyVaultClient("secrets");
```패턴은 항상 동일합니다. AppHost의 한 줄은 리소스를 정의하고 소비 서비스의 한 줄은 리소스를 사용합니다. 연결 세부정보가 자동으로 전달됩니다.
## 개발자 대시보드
Aspire 대시보드는 실제로 사용하기 전까지는 가지고 있으면 좋을 것 같은 기능 중 하나입니다. Aspire 대시보드 없이 작업하는 것은 상상할 수 없습니다.
AppHost를 로컬로 실행하면 대시보드가 시작되고 다음을 제공합니다.
- **리소스 개요** — 상태 표시기를 통해 모든 서비스와 인프라를 한 눈에 볼 수 있습니다.
- **구조화된 로그** — 모든 서비스의 실시간 로그 스트리밍, 필터링 및 검색 가능
- **분산 추적** — 플레임 차트로 시각화된 여러 서비스에 걸친 엔드투엔드 요청 추적
- **지표** — 실시간 HTTP 요청 비율, 오류율, 지연 시간 및 사용자 지정 지표
- **콘솔 로그** — 각 컨테이너 및 프로젝트의 원시 stdout/stderr
분산 추적은 특히 중요합니다. 요청이 프런트엔드에 도달하고 카탈로그 API를 통해 흐르고 Redis 및 PostgreSQL에 도달하면 각 홉에 대한 타이밍과 함께 전체 체인을 볼 수 있습니다. 더 이상 병목 현상이 발생하는 위치를 추측할 필요가 없습니다.
디버깅하는 동안 대시보드가 가장 유용하다는 것을 알았습니다. 여러 터미널 창을 추적하거나 로그 파일 사이를 이동하는 대신 서비스 전반에 걸쳐 관련 이벤트를 연결하는 상관 관계 ID를 통해 모든 것이 한 곳에 있습니다.
대시보드는 독립형 컨테이너로도 제공됩니다. 즉, 스테이징 환경이나 CI 파이프라인에서 사용할 수 있습니다.
```bash
docker run --rm -p 18888:18888 \
mcr.microsoft.com/dotnet/aspire-dashboard:latest
배포
Aspire는 개발 중에 도움이 되지만 생산은 어떻습니까? 이것이 실제적인 일이 되는 곳입니다.
Azure 컨테이너 앱
가장 간단한 배포 경로는 최고 수준의 Aspire를 지원하는 ACA(Azure Container Apps)입니다. Azure 개발자 CLI를 사용하여 직접 배포할 수 있습니다.
azd init
azd up
azd init 명령은 Aspire AppHost를 감지하고 필요한 코드형 인프라를 생성합니다. azd up는 AppHost 토폴로지를 기반으로 컨테이너 레지스트리, 컨테이너 앱, 데이터베이스, Redis 인스턴스 등 모든 것을 프로비저닝합니다.
AppHost는 기본적으로 배포 매니페스트가 됩니다. “catalog-api는 PostgreSQL 및 Redis에 따라 달라집니다"를 정의하는 동일한 코드가 인프라 프로비저닝을 구동합니다.
쿠버네티스
Kubernetes 배포의 경우 Aspire는 매니페스트를 직접 생성하지 않지만 AppHost 토폴로지는 Kubernetes 리소스에 깔끔하게 매핑됩니다. 커뮤니티 도구 aspirate(Aspir8)는 AppHost에서 Helm 차트 또는 Kubernetes 매니페스트를 생성할 수 있습니다.
dotnet tool install -g aspirate
aspirate generate
aspirate apply
배포 고려 사항
명심해야 할 몇 가지 사항:- 연결 문자열은 환경에 따라 변경됩니다. 로컬에서 Aspire는 컨테이너를 회전하고 연결 문자열을 자동으로 주입합니다. 프로덕션에서는 관리형 서비스를 가리킵니다. Aspire는 표준 .NET 구성을 사용하므로 환경 변수와 Azure Key Vault가 예상대로 작동합니다.
- AppHost는 프로덕션 환경에서 실행되지 않습니다. 개발 및 배포 조정 도구입니다. 프로덕션에서 서비스는 환경 변수 및 오케스트레이터를 통해 구성되어 독립적으로 실행됩니다.
- 인프라 리소스는 관리형 서비스가 됩니다. 로컬 PostgreSQL 컨테이너는 Azure Database for PostgreSQL이 됩니다. 로컬 Redis 컨테이너는 Azure Cache for Redis가 됩니다. 소비 코드는 변경되지 않습니다.
실제 팁
한동안 프로덕션 환경에서 Aspire를 실행한 후 시간을 절약할 수 있었던 교훈은 다음과 같습니다.
1. 맞춤형 리소스 수명 주기 확인을 사용하세요
리소스가 준비되었는지 확인하기 위해 컨테이너 시작에만 의존하지 마세요. PostgreSQL은 실제로 쿼리를 제공할 준비가 되기 전에 TCP 연결을 수락할 수 있습니다.
var postgres = builder.AddPostgres("postgres")
.AddDatabase("catalogdb")
.WithHealthCheck();
Aspire는 리소스에 대해 상태 확인을 실행하고 실제로 준비될 때까지 종속 서비스를 보유할 수 있습니다.
2. 공통 패턴을 확장으로 추출
여러 서비스가 비슷한 구성을 공유하는 경우 확장 메서드를 만듭니다.
public static class AppHostExtensions
{
public static IResourceBuilder<ProjectResource> AddWorkerService(
this IDistributedApplicationBuilder builder,
string name,
IResourceBuilder<IResourceWithConnectionString> db,
IResourceBuilder<IResourceWithConnectionString> messaging)
{
return builder.AddProject<Projects.WorkerService>(name)
.WithReference(db)
.WithReference(messaging)
.WithReplicas(3);
}
}
3. 부하 테스트에 WithReplicas 활용
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
.WithReference(postgres)
.WithReference(redis)
.WithReplicas(5);
WithReplicas는 서비스의 여러 인스턴스를 로컬에서 실행합니다. 이는 클러스터에 배포하지 않고도 로드 밸런싱, 동시성 버그 및 분산 캐싱 동작을 테스트하는 데 유용합니다.
4. 민감한 값에 대한 매개변수 사용
로컬 개발의 경우에도 자격 증명을 하드코딩하지 마세요.
var dbPassword = builder.AddParameter("db-password", secret: true);
var postgres = builder.AddPostgres("postgres", password: dbPassword)
.AddDatabase("catalogdb");
로컬로 실행할 때 Aspire는 값을 묻는 메시지를 표시하거나 사용자 비밀에서 값을 읽습니다. CI/CD에서는 환경 변수에서 가져옵니다.
5. 통합 테스트가 쉬워졌습니다.
Aspire에는 분산 앱 통합 테스트를 매우 쉽게 만드는 테스트 패키지가 포함되어 있습니다.
[Fact]
public async Task CatalogApiReturnsProducts()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.MyCloudApp_AppHost>();
await using var app = await appHost.BuildAsync();
await app.StartAsync();
var httpClient = app.CreateHttpClient("catalog-api");
var response = await httpClient.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
var products = await response.Content
.ReadFromJsonAsync<List<Product>>();
Assert.NotEmpty(products);
}
이는 데이터베이스 및 메시지 브로커를 포함한 전체 분산 애플리케이션을 가동하여 이에 대해 테스트를 실행하고 해체합니다. CI 파이프라인에서 실제 인프라에 대한 실제 통합 테스트를 수행합니다. 조롱하지 마세요.
6. 로컬에서 리소스 사용량 모니터링
여러 컨테이너를 로컬에서 실행하는 경우 리소스 소비를 주의 깊게 살펴보세요. 관리 UI가 포함된 PostgreSQL, Redis 및 RabbitMQ 인스턴스는 2~3GB의 RAM을 쉽게 사용할 수 있습니다. 제한된 시스템을 사용하는 경우 더 가벼운 리소스 구성을 사용하는 것이 좋습니다.
var redis = builder.AddRedis("cache")
.WithContainerRuntimeArgs("--memory=256m");
결론
.NET Aspire는 분산 애플리케이션을 구축하는 방식을 근본적으로 변경했습니다. 혁신적인 개념을 도입했기 때문이 아닙니다. 상태 확인, OpenTelemetry 및 컨테이너 오케스트레이션은 새로운 것이 아닙니다. 하지만 기본값으로 만들기 때문입니다. 일반적으로 내려야 하는 수백 가지의 작은 결정을 내리고 이를 합리적인 기본값으로 구현하며 필요할 때 재정의할 수 있습니다.제 생각에는 AppHost 패턴이 가장 큰 승리라고 생각합니다. Docker Compose 파일, Kubernetes 매니페스트 및 README 문서에 분산되지 않고 코드로 표현된 분산 애플리케이션 토폴로지를 가짐으로써 시스템을 이해하기 쉽게 만듭니다. 새로운 팀 구성원은 AppHost에서 Program.cs를 열고 몇 분 안에 전체 아키텍처를 이해할 수 있습니다.
.NET으로 분산 애플리케이션을 구축하고 있다면 Aspire를 진지하게 살펴보세요. aspire-starter 템플릿으로 시작하여 대시보드를 탐색하고 필요에 따라 구성 요소를 점진적으로 채택하세요. 첫날부터 올인할 필요는 없습니다. Aspire는 설계상 추가 기능을 제공합니다.
인프라 상용구를 배선하는 데 첫 번째 스프린트를 소비하는 시대는 끝났습니다. Aspire가 배관 작업을 처리하도록 하여 실제로 중요한 것, 즉 애플리케이션에 집중할 수 있도록 하세요.