dotnet aspire csharp communitytoolkit

C# kaigi で LT をさせていただいた内容の補足です。

個人リポジトリで作っていた RustFs 向けの Aspire Hosting Integration を、CommunityToolkit/Aspire の Pull Request #1295 として作り直しました。

利用側のコードはどちらも AddRustFs() と AddBucket() が中心です。一見すると同じ機能を移植しただけに見えますが、完成した実装を比較すると、単なるリネームやコードスタイルの修正では済みませんでした。

この記事ではコントリビューションの経緯ではなく、個人利用向けの実装から、Community Toolkit で配布・保守できる実装にするため、具体的に何を作り直したのかを整理します。

比較対象

以下の2つを比較しました。 あまり行数で語るものじゃないですが、わかりやすいので変更点を整理してみました。

項目個人版Community Toolkit版
プロダクションコードのファイル数25
プロダクションコード189行653行
テストのファイル数05
テストコード0行687行
プロダクションコード+テスト189行1,340行
公開される拡張メソッド46

プロダクションコードだけで約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 API

1つのバケットを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リソース登録、ヘルスチェック、エンドポイント、接続文字列、親子関係、リソース名、複数バケット
RustFsPublicApiTestsnull や空文字など、公開APIの引数チェック
RustFsS3SignerTests署名ヘッダー、日付形式、ハッシュ、署名の決定性、入力差による署名差
RustFsFunctionalTests実際のバケット作成、ヘルスチェック、ボリュームとBind Mountの永続化
AppHostTestsサンプルAppHostでS3 APIと管理コンソールが応答すること

特に大きいのは、コードの形を確認するテストだけでなく、DockerでRustFsを起動してバケット作成とデータ永続化を確認している点です。

実装653行に対してテスト687行なので、テストの方が34行多くなっています。

増えた464行の内訳

プロダクションコードは189行から653行になり、464行増えました。ファイル単位で分けると次のようになります。

実装個人版Community Toolkit版増分
RustFsBuilderExtensions.cs155354+199
RustFsResource.cs34102+68
RustFsBucketResource.cs052+52
RustFsS3Signer.cs0134+134
RustFsContainerImageTags.cs011+11
合計189653+464

増分のうち197行は、バケットリソース、SigV4署名、イメージ情報という新しいファイルです。残り267行は、既存のリソースと拡張メソッドへ接続情報、状態管理、永続化、入力検証、ドキュメントを追加した分です。

さらにテストが687行あるため、個人版を基準にすると、プロダクションコードとテストだけで1,151行の追加になりました。

どの程度の「作り直し」だったのか

RustFsコンテナを起動する部分は、個人版を土台として利用できました。一方、Integrationとして重要な接続情報とバケット作成は、ほぼ別の設計になっています。

とくに AddBucket() は、minio/mc を実行する補助コンテナから、接続文字列と状態を持つAspireの子リソースへ変わりました。見た目が同じメソッドでも、内部ではリソースモデル、イベント処理、SigV4署名、HTTP通信、状態通知まで作り直しています。

個人版の目的は「自分のAppHostでRustFsとバケットを起動できること」でした。Community Toolkit版では、そこに次の条件が加わっています。

  • 他のリソースから接続情報を参照できる
  • Dashboardで構成と状態を理解できる
  • 作成済みのバケットがあっても再実行できる
  • 複数の保存方法と署名リージョンを選べる
  • 不正な入力や失敗時の挙動が定義されている
  • コンテナを使う実際の動作まで継続的に検証できる

結果として、APIの名前は似ていても、実装としては「個人用コードを整えた」というより、「個人版で確認したアイデアを、配布可能なAspire Integrationとして再設計した」と表現するのが近いです。

参照