鋼鉄のアーキテクチャ:CargoワークスペースによるRust大規模開発の極致
Rustのプロジェクトが数百万行に達したとき、単一の `Cargo.toml` ではコンパイル時間の地獄と、依存関係のスパゲッティ化が避けられない。多くのエンジニアが「なんとなく」分割を行うが、真のアーキテクトは 「コンパイル単位の局所化」と「依存グラフのトポロジー最適化」 を設計の基点に据える。
本稿では、単なるディレクトリ分割を超えた、CI/CDとの完全同期とビルドパフォーマンスの極限を追求するワークスペース設計を解剖する。
—
1. ワークスペース構造:依存グラフの「疎結合化」
大規模プロジェクトにおける最大の敵は、無秩序な依存関係による「再コンパイルの連鎖」だ。ワークスペースを定義する際は、物理的な階層だけでなく、論理的な境界(Bounded Context) をパッケージごとに切り分ける。
ルートの Cargo.toml
ここで最も重要なのは `resolver = “2”` の明示と、依存関係の「継承」だ。
[workspace]
members = [
“crates/”,
“services/”,
]
resolver = “2” # 2021 Editionの機能をフル活用し、依存関係の解決を最適化する
[workspace.package]
version = “0.1.0”
edition = “2021”
[workspace.dependencies]
共通依存をここで管理し、各crateではバージョン指定を省略する。
これにより、crates間でバージョンの不一致による二重ビルドを防ぐ。
tokio = { version = “1.36”, features = [“full”] }
serde = { version = “1.0”, features = [“derive”] }
tracing = “0.1”
アーキテクトの視点:
`resolver = “2”` は必須だ。これにより、異なるフィーチャフラグを持つ依存関係が競合した際、Cargoがより賢明な解決策を選択するようになる。また、`[workspace.dependencies]` で定義を集中させることは、セキュリティ監査(`cargo-audit`)のコストを劇的に下げる。
—
2. CI/CDパイプラインとの高度な連携:ビルドキャッシュの魔術
大規模環境では、Dockerのレイヤーキャッシュだけでは不十分だ。Rustのコンパイルは、依存パッケージのビルドが全体の8割を占める。
Dockerfileの最適化ハック
`cargo chef` を使うのが一般的だが、真のプロは `sccache` をバックエンドに据え、リモートキャッシュをS3等にマウントする。
ビルドステージ:依存関係だけを先にコンパイルする
FROM rust:1.75-slim-bookworm AS planner
WORKDIR /app
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare –recipe-path recipe.json
FROM rust:1.75-slim-bookworm AS builder
RUN apt-get update && apt-get install -y pkg-config libssl-dev
RUN cargo install sccache
S3バケットをキャッシュ先としてエクスポート
ENV SCCACHE_BUCKET=my-build-cache-bucket
ENV RUSTC_WRAPPER=/usr/local/cargo/bin/sccache
COPY –from=planner /app/recipe.json recipe.json
キャッシュが効く状態でビルド
RUN cargo chef cook –release –recipe-path recipe.json
COPY . .
RUN cargo build –release
現場の知見:
`RUSTC_WRAPPER` に `sccache` を設定することで、CIインスタンスが変わってもコンパイル済みオブジェクトが再利用される。これは数十人規模の開発チームにおいて、CI時間を平均60%短縮させる。
—
3. 自動化スクリプト:Cargoの背後を操る
ワークスペースが巨大になると、特定のクレートだけをチェックしたいという欲求が生まれる。`cargo` コマンドをラップした `taskfile` や `justfile` を導入せよ。
変更があったパッケージだけをテストする最強のコマンド
test-affected:
@echo “変更されたクレートを特定中…”
git diff –name-only origin/main | xargs -I {} dirname {} | sort -u | \
xargs -I {} cargo test -p {} — –nocapture
全クレートの依存関係ツリーを視覚化し、肥大化を検知する
graph:
cargo tree –workspace –prefix indent
アーキテクトの視点:
`cargo tree` の出力をCIパイプラインのアーティファクトに含めよ。どの依存がビルド時間を食い荒らしているか、定期的に可視化してレビューを行う文化こそが、プロジェクトの長寿化を支える。
—
4. メモリとコンパイルパフォーマンスの最適化
大規模なワークスペースでは、`linker` の負荷が最大の問題になる。特に開発環境のリンク時間を削るために `lld` を導入し、デバッグビルドの設定を極限まで削ぎ落とせ。
ルートの Cargo.toml に設定
[profile.dev]
incremental = true
split-debuginfo = “unpacked” # リンク時間を短縮する秘策
[profile.dev.package.””]
opt-level = 0 # 依存パッケージは最適化しない
[profile.release]
lto = “thin” # フルLTOは時間がかかりすぎる。Thin LTOが最適解。
codegen-units = 16 # 並列度を調整してメモリ消費を抑える
結論:技術の真髄は「規律」にある
ワークスペースの設計とは、単なるファイルの配置ではない。それは、「どの境界線でコードを分離し、どの依存を共有し、どのようにCIのキャッシュを汚染させないか」 という、プロジェクトの「呼吸」をデザインする行為である。
今回紹介した手法は、単なるツール利用の枠を超え、あなたのチームが「コードを書く」ことに集中するためのインフラを構築する。もし、この構成を導入してなおビルドが遅いと感じるなら、それはあなたのコードが「結合度が高すぎる」というシグナルだ。Rustコンパイラは、あなたのアーキテクチャの鏡であることを忘れてはならない。