PROD欧州主権の BaaS プラットフォームダッシュボードを開く →

エンジニアリング · 10 分読み取り

マルチサービス Rust バックエンドの貨物ワークスペース

Affane Daylami · Fondateur · 2026年8月12日

ブログに戻る

Rust のマルチサービス バックエンドでは、すぐに同じ疑問が生じます。サービスごとに 1 つのデポですか、それとも単一の Cargo ワークスペースですか? Aurabase は 2 番目のオプションを選択しました。 11 のサービス、CLI、および 5 つの共有ライブラリが単一の Cargo.toml ルートに存在し、全員に単一の Cargo.lock が割り当てられます。このワークスペースがどのように構築されるかを 1 行ずつ読んで説明します。

この英語のテキストはフランス語のオリジナルから自動的に生成されたもので、まだレビューされていません。
このページは自動翻訳されました。英語版が正式です。

これは、この機会のために考え出された教育的な例ではありません。以下の各コード スニペットは、現在リポジトリに存在する Aurabase ルート Cargo.toml とそのクレート マニフェストから取得したものです。これには、この記事のために再読した際に見つかった 2 つの矛盾も含まれており、公開前にこっそり修正するのではなく、そのまま文書化しています。

必需品
  • Aurabase のルート Cargo ワークスペースは、18 の明示的なメンバー ( resolver = "2" および 1 つの Cargo.lockの下に) 11 のサービス、 aura-cliCLI、5 つの共有ライブラリ、および aurabase-rs SDK を宣言します。
  • [workspace.dependencies] は共有バージョンを一元管理します。各クレートは、クレートがサイレントにオーバーライドする場合を除き、独自の番号を設定するのではなく、{ workspace = true } を使用して継承します。
  • services/ の下のフォルダーは、自動的にはワークスペースのメンバーになりません。members リストは、非 Rust コードを除外できるように、グロブではなく明示的です。
  • [profile.release] はワークスペース全体に 1 回だけ適用されます。panic = "unwind" のような選択は、11 個のサービスすべてに一度に適用されます。
#
なぜワークスペースなのか

サービスごとにデポが 1 つだけですか、それとも Cargo.lock が 1 つだけですか?

ワークスペース Cargo は、単一の Cargo.lock および単一のターゲット ディレクトリの下に複数のクレートをグループ化します。これは、Rust のパッケージ マネージャーがこの場合に提供する機能そのものです。それぞれが独自のリポジトリにある 11 個の個別の Rust バイナリは、一見するとより独立しているように見えます。実際には、これは 11 の異なる Cargo.lock、時間の経過とともに異なる可能性がある 11 のバージョン解決を意味し、2 つのサービスが同じバージョンのaxum または sqlxをコンパイルする保証はありません。

A Cargo workspace solves this at the package manager level, not the team discipline level. All members share a single Cargo.lock at the root: a common dependency is resolved once, to an identical version, for the entire graph. This is also what makes a cross-service refactor (changing a signature in aura-core, for example) visible to a single cargo build --workspace, rather than discovered service by service in production. This is one of the choices that distinguishes our 100% unified Rust core from a heterogeneous stack assembled service by service.

#
解剖学

Cargo.toml ルート: リゾルバーとメンバー

すべては宣言 [workspace] とそのリスト membersから始まります。 Aurabase では、このリストは手書きであり、 services/*のような一般的なパターンによって生成されるのではなく、サービス、ツール、ライブラリなどの役割ごとにグループ化されています。

Cargo.tomltoml
[workspace]
resolver = "2"

members = [
    # サービス
    "services/aura-gateway",
    "services/aura-auth",
    "services/aura-db",
    # … 他の 8 つのサービス (aura-realtime、aura-storage、aura-ai など)
    # ツール
    "aura-cli",
    # リブ
    "libs/aura-core",
    # …その他の 4 つのライブラリ
    "aurabase-rs",
]

resolver = "2" は表面的な詳細ではありません。 Cargo の リゾルバー v2 は、 build-dependencies およびターゲット固有の依存関係 (target.'cfg(windows)'など) の機能をグラフの残りの部分から分離します。これらは最終バイナリに漏れなくなります。また、それを共有するワークスペースのすべてのメンバー間で同じ依存関係のバージョンを統一します。つまり、11 個の独立した解決ではなく、単一の axumが 1 回だけです。

#
レガシー

workspace.package: 1 バージョン、1 エディション、原則として共有

[workspace.package] は、各クレートがコピーする代わりに version.workspace = true で継承できる共通フィールド (バージョン、エディション、作成者、ライセンス) を一度宣言します。

Cargo.toml (ルート)toml
[workspace.package]
version = "0.1.1"
edition = "2021"

# services/aura-gateway/Cargo.toml
[package]
name = "aura-gateway"
version.workspace = true

Aurabase の 11 のサービスはすべてこのパターンに従っています。 2 つのクレートが逸脱しています。これは、この記事の準備中に見つかった 2 つの矛盾のうちの 1 つ目です。aura-cli はハード コピーで version = "0.2.0" を宣言し、ライブラリ libs/aura-migrations は version = "0.1.0" を宣言しています。どちらもワークスペースの 0.1.1 とは異なります。

それが何を意味するか

version.workspace = true はオプションで、フィールドごと、クレートごとに指定されます。自発的(たとえば、別途公開されたツール)または忘れてしまった場合でも、クレートが独自の番号を付けることを妨げるものはありません。ワークスペース監査では、継承を想定するのではなく、このフィールドをクレートごとにチェックする必要があります。

#
重複排除

workspace.dependency: クレートがバイパスしない限り、真実のソース

[workspace.dependencies] は、複数のクレートによって共有される依存関係を一元化します。各サービスは、独自のバージョン制約を設定する代わりに、{ workspace = true } を使用してそれを参照します。

Cargo.toml (ルート)toml
[workspace.dependencies]
axum = { version = "0.8", features = ["ws", "multipart", "macros"] }
sqlx = { version = "0.8", default-features = false, features = […] }
tower_governor = { version = "0.8", features = ["axum"] }
governor = "0.10"

# services/aura-gateway/Cargo.toml — ワークスペースガバナーを継承しません
governor = "0.8"  # ローカルバージョン、異なる

これが 2 番目の実際の矛盾です。ワークスペースは、バージョン 0.10の governor を集中管理しますが、 aura-gateway は、継承する代わりに独自の行 governor = "0.8" を再宣言します。ゲートウェイは、 aura-auth、 aura-ai、 aura-db および aura-functions が通過するときに、裸の governor クレートでレート制限を適用します。ミドルウェアのtower_governor。ワークスペースは相違を防ぐものではなく、比較する手間を掛けた場合に相違を可視化するだけです。

ただし、すべての依存関係が一元化されるに値するわけではありません。 wasmtime は [workspace.dependencies]のどこにも表示されません。1 つのクレート aura-functionsのみが WASM ランタイムに使用するため、ローカルで宣言されたままになります。適用するルール: 依存関係は、2 つ以上のクレートがそれを共有する前ではなく、共有した瞬間からワークスペース レベルに引き上げられます。

#
内部グラフ

libs/ から services/: これは何に依存しますか

5 つの共有ライブラリ (aura-core、 aura-crypto、 aura-db-adapters、 aura-migrations、 aura-telemetry) は、 [workspace.dependencies] — aura-core = { path = "libs/aura-core" } — でパス依存関係として宣言され、各サービスは実際に必要なものを選択します。

結果として得られるグラフは、ファイル間で引き続き読み取り可能です。 aura-gateway は、5 つの内部ライブラリのうち 3 つ ( aura-core、 aura-crypto 、および aura-telemetry) にのみ依存します。 aura-db-adapters や aura-migrationsには触れずに、PostgREST サイドカーの JWT に署名し、そのトレースをエクスポートするだけで十分です。 aura-provisionerは aura-migrationsを追加します。プロジェクトの作成によって生じるスキーマの移行を再実行します。

最も狭いケースは aura-migratorで、これは CI/CD 移行を実行する専用バイナリです。 Aurabase 内部ライブラリの中で、そのプロダクション [dependencies] はaura-migrations のみをリストし、 aura-core はテストのために [dev-dependencies]にのみ表示されます。したがって、本番環境に配信されるバイナリにはaura-core コードは含まれません。これは cargo test中にのみ存在します。

#
コンクリートケース

1 つのクレート、複数のバイナリ: オーラリアルタイム部門

新しいデプロイ可能なプロセスが必要になるたびに、ワークスペースで新しいメンバーを作成する必要はありません。 aura-realtime はワークスペースの単一のメンバーのままですが、その Cargo.toml は、同じ共有 [lib]の周囲に 3 つの異なる [[bin]] テーブルを宣言します。

Cargo.tomltoml
[lib]
name = "aura_realtime"

[[bin]]
name = "aura-realtime"

[[bin]]
name = "aura-realtime-cdc-worker"
path = "src/bin/cdc_worker.rs"

[[bin]]
name = "aura-realtime-ws-front"
path = "src/bin/ws_front.rs"

The manifesto itself documents the boundary between the two. cdc-worker captures Postgres changes by polling and publishes to NATS in pub/sub core (Client::publish_with_headers), without ever opening a WebSocket server — JetStream, in this same service, exclusively serves the cross-instance presence KV, not the CDC fan-out. ws-front only consumes NATS and holds WebSocket/SSE connections, without ever touching the CDC — the full details are in thereal-time engine documentation. Both binaries share the same RLS filtering code via [lib], but deploy and scale independently in Kubernetes. This is the right signal to choose several binaries in a crate rather than a new workspace member: same internal logic, different deployment topologies.

#
避けるべき罠

services/ 下のファイルは Cargo メンバーである必要はありません

aura-edge-runtime フォルダは Aurabase リポジトリに存在します。ただし、これには Cargo.toml は含まれておらず、TypeScript (index.ts、 envelope.ts) のみが含まれており、ルート ワークスペースの members リストのどこにも表示されません。

アストゥチェ

これがまさに、Aurabase の members リストが services/*のような汎用パターンに置き換えられるのではなく、手書きされる理由です。グロブはこの非 Rust フォルダーをワークスペースに含めようとしますが、結果として解決が失敗します。明示的なリストにより、同じ言語を話さないフォルダーが同じ親 services/の下に共存できます。

このレッスンでは、services/ のサブフォルダーをリストして Rust バックエンドのサービスをカウントすると、誤った数値が得られることを一般化します。ルート Cargo.toml のみが、ワークスペースで実際にコンパイルされる内容に関して権限を持ちます。

#
編集

[profile.release]: ワークスペース全体に対する単一の設定

ワークスペースのルートで宣言された [profile.release] は、リリース モードでコンパイルされたすべてのメンバーに適用されます。調整するのは 1 か所のみで、11 か所ではありません。 Cargo は、利用可能なすべてのキーを コンパイル プロファイル参照に文書化します。 Aurabase は 5 つだけアクティブになります。

Cargo.toml (ルート)toml
[profile.release]
lto = "thin"          # LTO クロスクレート、パフォーマンスとビルド時間のバランス
codegen-units = 1     # 全体的に最高のインライン化
panic = "unwind"   # tokio/axum → 500 によって分離されたパニック、世界的なクラッシュではない
strip = "symbols"
opt-level = 3

"abort" ではなく panic = "unwind" の選択は、ファイルのコメントに直接文書化されています。axum ハンドラーのパニックは tokio によってインターセプトされ、タスクは 500 を返し、プロセスは他の同時リクエストの処理を続行します。abort のパフォーマンスの向上は、クエリ間の分離を失う価値はありません。

#
実際に

ツールチェーンと重要なコマンドを凍結する

統合ワークスペースは依存関係のバージョンを設定しますが、コンパイラー自体のバージョンは設定しません。 rust-toolchain.tomlはルートで、リポジトリ内の cargo または rustc への直接呼び出しに対してツールチェーン (今日ではchannel = "1.93" ) をフリーズします。

このファイルが存在するのは、まさにサイレント ドリフトが発生したためです。リリース Docker イメージが凍結されたバージョンでコンパイルされている間に、CI アクションが rust-toolchain.tomlを読み取らずにその日の stable チャネルをインストールしました。現在の安定版 Rust ではコミットがすべてのテストに合格し、その後イメージのビルドで中断される可能性があります。マージ前ではなくマージ後に発見されます。 rustupは、あらゆる直接コマンドに対してこのファイルを尊重します。これは、CI ワークフローに影響を与えることなく、必要な唯一の修正でした。

terminalbash
# ワークスペース全体をコンパイルする
cargo build --workspace

# 単一のクレートをテストします (ワークスペース全体ではありません)。
cargo test -p aura-auth

# ワークスペース全体に対する厳密な lint、警告 = エラー
cargo clippy --workspace --all-targets -- -D warnings

# 書式設定
cargo fmt --all

最後に、マニフェストを実行する前に読み取る人のために説明します。 aura-cli クレートは、テーブル [[bin]] の名前が auraではなく aurabaseであるバイナリをコンパイルします。公開された npm ラッパー @aurabase/cliは、2 つのコマンド (aura と aurabase) を公開しており、どちらも同じスクリプトを指します。 [[bin]] Cargo の名前、クレートの名前、および npm ラッパーによって公開される名前は、3 つの別個のものです。他の 2 つからは何も推測できません。

#
要約

チェックリスト: どこで何を宣言するか

Cargo ワークスペースにクレートを追加するたびに、5 つの決定事項が表示されます。上記で説明した Aurabase の例に基づいて、それぞれが宣言されている場所は次のとおりです。

メンバー一覧[ワークスペース]メンバールート — 明示的なリスト、決してグロブではない
共有バージョン/エディション[ワークスペース.パッケージ]ルート — version.workspace = true、クレート別、オプション
2 つ以上のクレートによって共有される依存関係[ワークスペース.依存関係]ルート - 各クレートで { workspace = true }
単一の消費者への依存クレートの[依存関係]ワークスペースを経由せずにローカルで
ビルドプロファイル[プロフィール.リリース]ルートのみ — すべてのメンバーに適用されます

既存の Cargo ワークスペース (自分のワークスペース、または引き継いだプロジェクトのワークスペース) を監査するには、上記のような矛盾を見つけるには 4 つのチェックで十分です。

  1. [workspace.package].version を各クレートの version と比較します。異なる値は必ずしもバグではありませんが、文書化する価値があります。
  2. [workspace.dependencies] を各クレートによってローカルに宣言された依存関係と比較します。バージョンが異なる両側に存在する名前を見つけます。
  3. ルート Cargo.toml の members リストをリポジトリ内の実際のサブフォルダーと比較します。members にフォルダーが欠落していても、必ずしも見落としがあるわけではありません。
  4. コンパイルされた各バイナリ ([[bin]] name) がクレート名または npm ラッパーによって公開される名前と一致すると仮定するのではなく、実際の名前を確認してください。

導入の準備はできていますか?

5 分でバックエンドが完成します。

クレジット カードは不要 · 500 MB 無料 · 50,000 MAU