.NET Aspire: クラウドネイティブ アプリケーションを正しい方法で構築する
.NET で分散アプリケーションを構築したことがある場合は、その詳細をご存知でしょう。 Web API を起動し、バックグラウンド ワーカーを追加し、キャッシュには Redis、永続化には PostgreSQL、おそらくメッセージングには RabbitMQ を投入します。そして突然、ビジネス ロジックの作成よりもインフラストラクチャの配線に多くの時間を費やすことになります。 appsettings.json ファイルに散在する接続文字列、設定を忘れたヘルスチェック、常に「次のスプリントの問題」である可観測性。
私はそこに行ったことがある。私が認めたい以上の回数。
まさに、.NET Aspire が解決するために設計された問題です。実稼働環境で数か月間実行した後、良い点、素晴らしい点、そして欠点など、学んだことを共有したいと思います。
.NET Aspire とは何ですか?
.NET Aspire は、.NET を使用して、監視可能で本番環境に対応した分散アプリケーションを構築するための独自のスタックです。これは従来の意味でのフレームワークではありません。ASP.NET Core を置き換えたり、新しいプログラミング モデルを強制したりするものではありません。代わりに、すでに知っていることの上に置かれ、クラウドネイティブ アプリを構築する際に常に存在していたギャップを埋めます。
Aspire はその中核として 4 つのことを提供します。
- AppHost — 分散アプリケーション トポロジ全体を定義するプロジェクト。どのサービスが存在し、何に依存し、どのように接続するのか。
- サービスのデフォルト — ヘルスチェック、復元ポリシー、OpenTelemetry などの横断的な懸念事項をすべてのサービスに対して 1 回で構成する共有プロジェクト。
- コンポーネント — 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
これにより、4 つのプロジェクトを含むソリューションが生成されます。
MyCloudApp.AppHost— オーケストレーターMyCloudApp.ServiceDefaults— 共有構成MyCloudApp.ApiService— サンプル Web 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 では、1 行ですべての処理が行われます。
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Your service-specific configuration...
var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();
この 1 回の AddServiceDefaults() 呼び出しで次のことが得られます。
- OpenTelemetry による構造化ログ、メトリクス、分散トレース
- ライブネスおよびレディネスエンドポイントによるヘルスチェック
- サービス検出 により、サービスが名前で相互に検索できるようになります
- すべての発信 HTTP 呼び出しに対する 復元ポリシー (再試行、サーキット ブレーカー、タイムアウト)
これは、「自分のマシンで動作する」デモと運用準備が整ったシステムを区別するものです。そして、Aspire はそれを後付けではなくデフォルトにしています。
Aspire コンポーネント
Aspire コンポーネントは、サービスがバッキング インフラストラクチャに接続する方法を標準化する NuGet パッケージです。これらは単なるクライアント ライブラリではなく、ヘルス チェック、ロギング、トレース、すぐに使える構成可能な復元機能が含まれています。
レディス
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddRedisDistributedCache("cache");
それだけです。接続文字列は、WithReference 経由で AppHost から取得されます。このコンポーネントは、Redis によってバックアップされた IDistributedCache を登録し、ヘルスチェックと OpenTelemetry インストルメンテーションがすでに接続されています。
Redis を出力キャッシュに使用することもできます。
builder.AddRedisOutputCache("cache");
Entity Framework Core を使用した PostgreSQL
builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb", settings =>
{
settings.DisableRetry = false;
});
これにより、AppHost で定義された catalogdb データベースへの接続を使用して DbContext が登録されます。これには、接続プーリング、ヘルスチェック、および再試行ポリシーが含まれます。
RabbitMQ
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 で 1 行でリソースを定義し、1 行で使用側サービスでそれを使用します。接続の詳細は自動的に表示されます。
## 開発者ダッシュボード
Aspire ダッシュボードは、実際に使用するまでは、あったら便利だと思われる機能の 1 つです。実際に使用すると、これなしでの作業は考えられなくなります。
AppHost をローカルで実行すると、ダッシュボードが起動して次の情報が表示されます。
- **リソースの概要** — すべてのサービスとインフラストラクチャをステータス インジケーターとともに一目で確認できます。
- **構造化されたログ** — すべてのサービスからのリアルタイムのログ ストリーミング、フィルタリングと検索が可能
- **分散トレース** — フレーム チャートとして視覚化された、複数のサービスにわたるエンドツーエンドのリクエスト トレース
- **メトリクス** — リアルタイムの HTTP リクエスト レート、エラー レート、レイテンシ、およびカスタム メトリクス
- **コンソール ログ** — 各コンテナおよびプロジェクトからの生の stdout/stderr
分散トレースは特に価値があります。リクエストがフロントエンドに到達し、カタログ API を通過し、Redis と PostgreSQL にアクセスすると、チェーン全体が各ホップのタイミングとともに表示されます。ボトルネックがどこにあるのかを推測する必要はもうありません。
デバッグ中にダッシュボードが最も役立つことがわかりました。複数のターミナル ウィンドウをたどったり、ログ ファイル間を移動したりする代わりに、サービス全体で関連するイベントをリンクする相関 ID を使用して、すべてが 1 か所にまとめられます。
ダッシュボードはスタンドアロン コンテナーとしても利用できます。つまり、ステージング環境または CI パイプラインで使用できます。
```bash
docker run --rm -p 18888:18888 \
mcr.microsoft.com/dotnet/aspire-dashboard:latest
デプロイメント
Aspire は開発中は役に立ちますが、本番環境ではどうなるのでしょうか?ここからが現実的なことになります。
Azure コンテナー アプリ
最も簡単なデプロイ パスは、ファーストクラスの Aspire サポートを備えた Azure Container Apps (ACA) です。 Azure Developer CLI を使用して直接デプロイできます。
azd init
azd up
azd init コマンドは、Aspire AppHost を検出し、必要なコードとしてのインフラストラクチャを生成します。 azd up は、AppHost トポロジに基づいて、コンテナー レジストリ、コンテナー アプリ、データベース、Redis インスタンスなどすべてをプロビジョニングします。
AppHost は基本的に展開マニフェストになります。 「catalog-api は PostgreSQL と Redis に依存する」を定義する同じコードがインフラストラクチャのプロビジョニングを推進します。
Kubernetes
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 ~ 3 GB の RAM を消費する可能性があります。制約のあるマシンを使用している場合は、より軽量なリソース構成の使用を検討してください。
var redis = builder.AddRedis("cache")
.WithContainerRuntimeArgs("--memory=256m");
結論
.NET Aspire は、分散アプリケーションの構築方法を根本的に変えました。革新的な概念を導入しているからではありません。ヘルスチェック、OpenTelemetry、コンテナオーケストレーションは新しいものではありません。しかし、それはそれらを「デフォルト」にしてしまうからです。通常は行う必要がある 100 の小さな決定を必要とし、それらを適切なデフォルトで実装し、必要に応じて上書きできるようにします。私の意見では、AppHost パターンが最大の成果です。分散アプリケーション トポロジを Docker Compose ファイル、Kubernetes マニフェスト、README ドキュメントに散在せずにコードとして表現すると、システムが理解しやすくなります。新しいチーム メンバーは、AppHost で Program.cs を開いて、アーキテクチャ全体を数分で理解できます。
.NET を使用して分散アプリケーションを構築している場合は、Aspire を真剣に検討してください。 aspire-starter テンプレートから始めて、ダッシュボードを探索し、必要に応じてコンポーネントを徐々に採用していきます。初日から全力を尽くす必要はありません。Aspire は追加的な設計になっています。
最初のスプリントをインフラストラクチャのボイラープレートの配線に費やす時代は終わりました。 Aspire に配管の処理を任せて、実際に重要なこと、つまりアプリケーションに集中できるようにします。