キャディのバックエンドエンジニアをして働いている高藤です。
キャディではRustを使ったAPIサーバを開発しています。今回はその開発の過程で導入したcargo workspaceを使ったプロジェクト構成についてまとめました。
今回のアプリケーションについて
- Rustで記述
- ドメイン駆動設計を用いて設計をしており、ドメイン層を明確に分離している
- アプリケーションの役割はgRPCでAPIを提供したり、MessageQueueからくるメッセージの処理を行う
実装しているアプリケーションで使っている技術や設計手法などは弊社エンジニアが書いた別の記事もご参照下さい。
workspaceを使うようになるまでの経緯
開発初期、cargo newコマンドで生成されたプロジェクトを以下のような構造にして実装していました。
application_name
├─ app
│ └─ main.rs
├─ src
│ ├─ domain/
│ │ ├─ aaa.rs
│ │ └─ ...etc
│ ├─ usecase/
│ │ ├─ bbb.rs
│ │ └─ ...etc
│ └─ infrastructure/
│ ├─ grpc/
│ │ └─ ...etc
│ └─ mq/
│ │ └─ ...etc
├─ Cargo.toml
ドメイン層などをディレクトリを使い階層構造でmoduleを配置しています。処理をどこに記述すべきかを理解しやすくするためこのような構成にしていました。この構造でプロジェクトが進むにつれ、各ディレクトリ内のmoduleは増え続けると共にビルド時間が増大し、開発の効率を悪化させる事象が発生しました。
cargo workspace の利用
上記の問題を解決するため、cargo workspace という機能でプロジェクトを複数のcrateに分離しました。
workspaceを使うメリット
crateを分割するメリットとしては保守性や再利用性の向上ももちろんありますが、今回のケースとしてはビルド時間を少しでも短縮することが当初の目的でした。
なぜならRustのビルドツールcargoでは依存関係のないcrateは並列にコンパイルする事が出来ます。
上記のケースではinfrastructureの中にあるコードはdomain,usecaseに依存しています。他方でinfrastructure内部のgrpc, mqなどの処理はお互いに依存はないため、分割することでコンパイル速度を向上させることが可能です。
workspaceを使ったプロジェクト構成
application_name
├─ app
│ ├─ src/main.rs
│ └─ Cargo.toml
├─ domain
│ ├─ src/...etc
│ └─ Cargo.toml
├─ usecase
│ ├─ src/...etc
│ └─ Cargo.toml
├─ grpc
│ ├─ src/...etc
│ └─ Cargo.toml
├─ mq
│ ├─ src/...etc
│ └─ Cargo.toml
├─ Cargo.toml
上記の構成では5つのcrateに分割しています。
workspaceの作り方
application_name/Cargo.tomlを以下のように定義します。
[workspace]
members = [
"app",
"domain",
"usecase",
"grpc",
"mq",
]
workspace配下に配置するcrateを上記の様にmembersとして記述をします。
それぞれのcrateの中にはCargo.tomlを用意する必要があります。
なお、membersに記述のはpathになるため、必ずしも同一階層に全てのcrateを配置しなくても定義可能です。
例: ./infrastructure/grpc, ./infrastructure/mqのように定義することも可能。
workspace適用後の効果
今回のケースの場合、下記グラフの通り最終的に10分前後かかっていたビルド時間が、2分弱の時間で実行できるようになりました。

workspaceの使い方メモ
workspaceに関する詳細は各ドキュメント等を参考にしてください。簡単な説明となってしまいますが箇条書きでいくつか利用方法等をご紹介させてもらいます。
workspaceの中ではコンパイル成果物が格納されるtargetディレクトリはworkspace直下に配置されます。(上記例だとapplication_name/target)Cargo.lockも同様にworkspace直下に配置されます。これによりworkspace配下のcrateが依存するcrateのバージョンを保証しています。workspaceを利用しているときも通常のプロジェクトと同様にcargoコマンドでビルドを行うことが出来ます(cargo check,cargo build,cargo run...etc)workspace配下のcrateにカレントディレクトリを変更してビルドを行った場合そのcrateを対象にビルドができます。- カレントディレクトリを変更したくない場合は
--packageオプションを使ってビルドも可能です(cargo check --package domain)
- カレントディレクトリを変更したくない場合は
- The Rust Programming Language ch14-03
- The Cargo Book
最後に
私達が開発するアプリケーションは現在16 crateまで分割しています。正直まだ分離させられる余地もあり、成長と共にビルド時間が増えたり、保守観点から分離すべきタイミングで分けるべきだと考えています。
また、参考までにRust製のservice meshであるlinkerd2-proxyを確認すると55のcrateから構成されています。
このようにアプリケーションが成長し規模や複雑さに応じて簡単にworkspaceを使って分離できるのはかなり有用かと思っています。
ある程度の成長が予測されるアプリケーションなどは最初からworkspaceの構成を考えておくなどしておくと良いと思います。
参考: linkerd2-proxy