dotnet aspire csharp communitytoolkit
C# kaigi で LT をさせていただいた内容の補足です。
個人リポジトリで作っていた RustFs 向けの Aspire Hosting Integration を、CommunityToolkit/Aspire の Pull Request #1295 として作り直しました。
利用側のコードはどちらも AddRustFs() と AddBucket() が中心です。一見すると同じ機能を移植しただけに見えますが、完成した実装を比較すると、単なるリネームやコードスタイルの修正では済みませんでした。
この記事ではコントリビューションの経緯ではなく、個人利用向けの実装から、Community Toolkit で配布・保守できる実装にするため、具体的に何を作り直したのかを整理します。
比較対象
以下の2つを比較しました。 あまり行数で語るものじゃないですが、わかりやすいので変更点を整理してみました。
- 個人版:
konnta0/Aspire.Extensionsのf4aca49 - Community Toolkit版: PR #1295 最終コミット
40d1915
| 項目 | 個人版 | Community Toolkit版 |
|---|---|---|
| プロダクションコードのファイル数 | 2 | 5 |
| プロダクションコード | 189行 | 653行 |
| テストのファイル数 | 0 | 5 |
| テストコード | 0行 | 687行 |
| プロダクションコード+テスト | 189行 | 1,340行 |
| 公開される拡張メソッド | 4 | 6 |
プロダクションコードだけで約3.5倍、テストまで含めると約7.1倍になりました。PR全体では、サンプル、README、プロジェクトとソリューションへの登録も含めて19ファイル、1,480行の追加でした。
AddRustFs() の基本部分は大きく変わっていない
まず、すべてを捨てて一から書いたわけではありません。次の処理は個人版からCommunity Toolkit版へほぼそのまま引き継いでいます。
- アクセスキーとシークレットキーのパラメーターを作る
RustFsResourceを登録する- S3 API用の9000番と、コンソール用の9001番を公開する
- RustFsの環境変数を設定する
/healthを使ったヘルスチェックを登録する- データ保存用の名前付きボリュームを追加する
つまり、RustFsコンテナを起動するだけであれば、個人版の時点で必要な実装はおおむね揃っていました。
作り直しの中心は、起動したRustFsを「ほかのAspireリソースからどう利用するか」と、バケットを「Aspireのリソースとしてどう扱うか」です。
1. RustFsResource を接続可能なリソースにした
個人版の RustFsResource は ContainerResource を継承し、認証パラメーターを保持するだけのクラスでした。
public sealed class RustFsResource(...) : ContainerResource(name)
{
public ParameterResource AccessKey { get; } = accessKey;
public ParameterResource SecretKey { get; } = secretKey;
}Community Toolkit版では IResourceWithConnectionString を実装しました。
これに伴い、次の情報をリソースから参照できるようにしています。
- S3 APIのホストとポート
http://{host}:{port}形式のURI- アクセスキーとシークレットキー
Endpoint=...;AccessKey=...;SecretKey=...形式の接続文字列- バケット作成時に使う署名リージョン
これで WithReference(rustfs) を使う側へ接続情報を渡せます。RustFsコンテナを起動するだけのクラスから、Aspireのリソース間参照に参加できるクラスへ変わりました。
この変更だけで RustFsResource.cs は34行から102行に増えています。
2. バケット作成を補助コンテナからAspireのリソースへ変えた
最も大きく設計を変えたのは AddBucket() です。
個人版
個人版の AddBucket() は、minio/mc のコンテナを追加していました。
RustFsResource
└── ContainerResource (minio/mc)
└── mc alias set ...; mc mb ...;RustFsの起動を待ち、/bin/sh から mc alias set と mc mb を実行します。少ないコードで動かせる一方、Aspireから見ると存在するのは「バケット」ではなく「一度だけコマンドを実行するコンテナ」です。
この方式には次の特徴がありました。
AddBucket()の戻り値はIResourceBuilder<ContainerResource>- バケット作成のために
minio/mcイメージが必要 - 複数バケットも1つのシェル文字列として組み立てる
- Dashboardにはバケットではなく補助コンテナが表示される
- バケット作成の状態をAspireのリソース状態として表現できない
Community Toolkit版
Community Toolkit版では、新しく RustFsBucketResource を作りました。
RustFsResource
└── RustFsBucketResource
AppHost
└── 署名付き PUT /{bucket} ──> RustFs S3 API1つのバケットを1つの子リソースとして登録し、親となるRustFsがReadyになったタイミングで、AppHostからS3 APIへリクエストを送ります。
具体的には次の実装が増えました。
RustFsBucketResourceと親子関係の定義- バケット用の接続文字列への
Bucketの追加 - S3バケット名とAspireのリソース名の分離
.などを含むバケット名から有効なリソース名への変換- 複数バケットを、それぞれ独立した子リソースとして登録
Waiting→Creating→Runningの状態更新- 作成失敗時のログ出力とError状態への更新
このため、AddBucket("images") が返す型も IResourceBuilder<ContainerResource> から IResourceBuilder<RustFsBucketResource> に変わりました。Dashboard上でも、バケットそのものと作成状態を確認できます。
3. minio/mc を外す代わりにSigV4署名を実装した
補助コンテナを使わない代わりに、Community Toolkit版では RustFsS3Signer を追加しました。
これは汎用的なAWS SDKの代替ではなく、空のボディを持つS3の PUT /{bucket} に用途を限定したAWS Signature Version 4の実装です。
次の処理を実装しています。
- Canonical Requestの構築
- Credential ScopeとString to Signの構築
- 日付、リージョン、サービス名を使った署名キーの導出
- HMAC-SHA256による署名
Authorization、x-amz-date、x-amz-content-sha256ヘッダーの生成- バケット名のパスセグメント用エンコード
バケット作成側には、さらに次の処理が必要です。
- 接続文字列からエンドポイントを取り出す
- 認証パラメーターを解決する
- 30秒のタイムアウト付きでHTTP PUTを送る
- 成功時にリソースをRunningへ更新する
409 Conflict、BucketAlreadyOwnedByYou、BucketAlreadyExistsを作成済みとして扱う- それ以外のレスポンスは本文を含めてエラーにする
外部ツールへ任せていた処理をAppHostへ移したため、実行時に必要なコンテナは減りました。一方で、署名、HTTP通信、冪等性、失敗時の状態管理はIntegration自身の責務になりました。
4. 配布するIntegrationとしての機能を追加した
Community Toolkit版では、個人版にはなかった次の機能も追加しています。
データ保存方法の追加
個人版は名前付きボリュームを使う WithDataVolume() のみでした。Community Toolkit版では、ホストの任意ディレクトリを指定できる WithDataBindMount() も追加しています。
署名リージョンの変更
デフォルトは us-east-1 ですが、WithSigningRegion() でバケット作成時のリージョンを変更できます。
コンテナイメージの固定
個人版は rustfs/rustfs をタグなしで指定していました。Community Toolkit版はイメージ情報を RustFsContainerImageTags に分離し、PR最終時点では 1.0.0-beta.8 に固定しています。
公開APIとしての整備
各拡張メソッドには引数チェック、XMLドキュメント、使用例、AspireExport を追加しました。個人版では自分の利用方法だけを想定できましたが、公開パッケージでは不正な引数に対する挙動や、ツールから見えるAPIの形も契約になります。
5. 687行のテストを追加した
個人版にはテストがありませんでした。Community Toolkit版では5ファイル、687行、33個のテストメソッドを追加しています。
| テスト | 確認していること |
|---|---|
AddRustFsTests | リソース登録、ヘルスチェック、エンドポイント、接続文字列、親子関係、リソース名、複数バケット |
RustFsPublicApiTests | null や空文字など、公開APIの引数チェック |
RustFsS3SignerTests | 署名ヘッダー、日付形式、ハッシュ、署名の決定性、入力差による署名差 |
RustFsFunctionalTests | 実際のバケット作成、ヘルスチェック、ボリュームとBind Mountの永続化 |
AppHostTests | サンプルAppHostでS3 APIと管理コンソールが応答すること |
特に大きいのは、コードの形を確認するテストだけでなく、DockerでRustFsを起動してバケット作成とデータ永続化を確認している点です。
実装653行に対してテスト687行なので、テストの方が34行多くなっています。
増えた464行の内訳
プロダクションコードは189行から653行になり、464行増えました。ファイル単位で分けると次のようになります。
| 実装 | 個人版 | Community Toolkit版 | 増分 |
|---|---|---|---|
RustFsBuilderExtensions.cs | 155 | 354 | +199 |
RustFsResource.cs | 34 | 102 | +68 |
RustFsBucketResource.cs | 0 | 52 | +52 |
RustFsS3Signer.cs | 0 | 134 | +134 |
RustFsContainerImageTags.cs | 0 | 11 | +11 |
| 合計 | 189 | 653 | +464 |
増分のうち197行は、バケットリソース、SigV4署名、イメージ情報という新しいファイルです。残り267行は、既存のリソースと拡張メソッドへ接続情報、状態管理、永続化、入力検証、ドキュメントを追加した分です。
さらにテストが687行あるため、個人版を基準にすると、プロダクションコードとテストだけで1,151行の追加になりました。
どの程度の「作り直し」だったのか
RustFsコンテナを起動する部分は、個人版を土台として利用できました。一方、Integrationとして重要な接続情報とバケット作成は、ほぼ別の設計になっています。
とくに AddBucket() は、minio/mc を実行する補助コンテナから、接続文字列と状態を持つAspireの子リソースへ変わりました。見た目が同じメソッドでも、内部ではリソースモデル、イベント処理、SigV4署名、HTTP通信、状態通知まで作り直しています。
個人版の目的は「自分のAppHostでRustFsとバケットを起動できること」でした。Community Toolkit版では、そこに次の条件が加わっています。
- 他のリソースから接続情報を参照できる
- Dashboardで構成と状態を理解できる
- 作成済みのバケットがあっても再実行できる
- 複数の保存方法と署名リージョンを選べる
- 不正な入力や失敗時の挙動が定義されている
- コンテナを使う実際の動作まで継続的に検証できる
結果として、APIの名前は似ていても、実装としては「個人用コードを整えた」というより、「個人版で確認したアイデアを、配布可能なAspire Integrationとして再設計した」と表現するのが近いです。