前回の記事では、Rust の Docker イメージサイズを 98%削減する方法を解説しました。その中で最も重要な役割を果たしているのがcargo-chef です。この記事では、cargo-chef の仕組みと動作原理を深く掘り下げていきます。
cargo-chef は、Docker のレイヤーキャッシングと Cargo のビルドモデルの根本的な不整合を解決し、Rust プロジェクトのDockerビルドを5倍高速化します。Luca Palmieri が「Zero to Production In Rust」のために作成したこのツールは、ソースコード変更のたびに 20 分以上かかっていたリビルドを、依存関係をアプリケーションコードから分離してキャッシュし、2〜3 分のビルドに変えました。
cargo-chef は依存関係情報のみを捉えた「レシピ」を作成し、ソースコードが変更されても有効なままの別レイヤーで高コストな依存関係のコンパイルをキャッシュできます。約 500 の依存関係を持つ商用コードベースでは、ビルド時間が約 10 分から約 2 分に短縮され、CI/CD の速度とインシデント対応時間に直接影響を与えます。
Docker のレイヤーキャッシングは、各命令(RUN、COPY、ADD)に対してレイヤーを作成します。いずれかのレイヤーが変更されると、そのレイヤーとそれ以降のすべてのレイヤーが無効化されます。標準的な Rust Dockerfile は重大な問題に直面します: 依存関係のマニフェストとソースコードの両方を一緒にコピーする必要があるため、ソースの変更があるとビルドキャッシュ全体が無効になってしまうのです。
問題のあるパターン:
FROM rust:1.75WORKDIR /appCOPY . . # マニフェストとソースを一緒にコピーRUN cargo build# 変更のたびにすべてを再ビルド
Python のpip install -r requirements.txt や Node のnpm install とは異なり、Cargoには依存関係のみをビルドするネイティブな方法がありません。cargo build コマンドは、依存関係とソースのコンパイルを統一された操作として扱います。cargo build --only-deps のようなフラグは存在しません。このアーキテクチャ上の制限により、他の言語では美しく機能する標準的な Docker パターンが、Rust では壊滅的に失敗してしまいます。
影響は開発ワークフロー全体に波及します。すべてのコード変更—たった 1 文字の修正でさえ—数百の依存関係の完全な再コンパイルを引き起こします。2〜4 コアの CI システムでは、ビルドが 30 分を超えることがあります。これにより、デプロイ速度、インシデント対応時間、開発者の反復サイクルに厳しい下限が生まれます。本番環境のインシデントで緊急パッチが必要な場合、その 20 分のビルドが 20 分のダウンタイムになります。
Rust のコンパイルモデルは、コンパイル時間の速度よりも実行時パフォーマンスを優先します。リリースビルド(--release)は、中規模のプロジェクトで 15〜20 分かかる広範なLLVM 最適化パスを実行します。ジェネリクス、トレイト特殊化、単相化の多用により、依存関係は各使用パターンに対して相当量のコードをコンパイルします。非同期エコシステム(tokio、actix-web、tonic)はこれを悪化させます—これらのクレートは単純なアプリケーションでもコンパイルが重いのです。
インクリメンタルコンパイルは存在しますが、リリースビルドではデフォルトで無効になっており、外部依存関係には役立ちません。Docker の本番ビルドは常に--release プロファイルを使用するため、遅いコンパイルパスを避けられません。依存関係のコンパイルは通常、総ビルド時間の 80〜90%を消費しますが、これらの依存関係はアプリケーションコードに比べてほとんど変更されません。この逆転した関係—最も遅い部分が最も変更されない—こそが、cargo-chef が活用するポイントです。
プロジェクト構造:
src/main.rs - コマンドパースを含むCLI エントリポイントsrc/lib.rs - ライブラリエントリポイントsrc/recipe.rs - レシピ生成、依存関係ビルド、クッキングロジックsrc/skeleton.rs - プロジェクトスケルトンの作成とダミーファイル生成cargo-chef のアーキテクチャは 2 つの抽象化を中心としています:RecipeとSkeleton。Recipe はシリアライズ可能なコンテナで、Skeleton は実際のマニフェストデータとロックファイルを含みます。これらの構造により、コアワークフローが可能になります: 分析 →シリアライズ → 再構築 → ビルド。
「レシピ」は、ソースコードなしで依存関係をビルドするために必要な最小限の情報を捉えたJSONファイル(recipe.json)です。これはPython の requirements.txt と同じ目的を果たしますが、Rust のより複雑なプロジェクト構造に対応しています。
レシピの内容:
pubstructRecipe {pub skeleton: Skeleton,}pubstructSkeleton { manifests:Vec<Manifest>, lock_file:Option<String>,}
この構造は人間が読めるJSON にシリアライズされ、レシピはデバッグ可能で検査可能です。明示的なターゲット宣言により、Cargo が通常ファイルの場所からターゲットを推測する場合でも、信頼性の高いキャッシュが保証されます。
cargo-chef は、マルチステージビルドで連携する 2 つのコマンドを提供します:
1.cargo chef prepare --recipe-path recipe.json
このコマンドは次のように現在のプロジェクトを分析します。
prepare コマンドは高速(通常 1 秒未満)です。ファイル構造を分析して TOML をパースするだけで、コンパイルは行わないためです。
2.cargo chef cook --release --recipe-path recipe.json
このコマンドは次のように再構築とビルドを行います。
skeleton.build_minimum_project() を呼び出してディレクトリ構造を再作成cargo build を実行skeleton.remove_compiled_dummies() 経由でコンパイル済みダミーアーティファクトを削除ダミーファイルトリック: cargo-chef は次のように最小限の有効な Rust ファイルを作成します。
// ダミーのmain.rsfnmain() {}// ダミーのlib.rs// (空または最小限)
これらは Cargo がコンパイル可能なプロジェクトを要求する条件を満たしますが、実際のロジックは含まれていません。その後、Cargo は通常通りすべての依存関係を解決してコンパイルし、キャッシュされたアーティファクトを生成します。ダミーアーティファクトは後でクリーンアップされ、外部依存関係のコンパイル結果のみが残ります。
重要な技術的制約:cook とその後のbuild コマンドは、同じ作業ディレクトリから実行すべきです。これは、target/debug/deps 内の Cargo の*.d ファイルにターゲットディレクトリへの絶対パスが含まれているためです。ディレクトリを移動するとキャッシュの利用が壊れます。これは cargo-chef の制限ではなく、cargo-chef が尊重する Cargo の動作です。
cargo-chef は、Docker のマルチステージビルド機能用に特別に設計されています。標準的なパターンは 3 つのステージを使用します:
標準的な3ステージパターン:
FROM lukemathwalker/cargo-chef:latest-rust-1 ASchefWORKDIR /app# ステージ1: Planner - レシピを生成FROM chef ASplannerCOPY . .RUN cargo chef prepare--recipe-path recipe.json# ステージ2: Builder - 依存関係をキャッシュFROM chef ASbuilderCOPY--from=planner /app/recipe.json recipe.jsonRUN cargo chef cook--release--recipe-path recipe.json# ↑ このレイヤーは依存関係が変更されるまでキャッシュされる# 次にソースをコピーしてアプリケーションをビルドCOPY . .RUN cargo build--release--bin app# ステージ3: Runtime - 最小限の本番イメージFROM debian:bookworm-slim ASruntimeWORKDIR /appCOPY--from=builder /app/target/release/app /usr/local/binENTRYPOINT["/usr/local/bin/app"]
キャッシングの仕組み:
各 Docker ステージは独立したキャッシングを維持します。ステージはCOPY --from 文を通じてのみやり取りします。この分離が cargo-chef の効果の鍵です。
COPY . . は planner キャッシュを無効化(ただしこれは高速)COPY --from=planner 経由で recipe.json のみを受け取るキャッシュ無効化ロジック:
ソースコード変更 → plannerステージ無効化 → recipe.json変更なし → builderの依存関係レイヤーキャッシュ済み ✓ → アプリケーションビルドのみ実行依存関係変更 → plannerステージ無効化 → recipe.json変更 → builderの依存関係レイヤー無効化 ✗ → フルリビルド必要
これはインセンティブを完璧に整合させます: 高コストな操作(依存関係コンパイル)は、そうあるべき時(依存関係が変更されていない時)にキャッシュされ、高速な操作(ソースコンパイル)は期待通り毎回の変更で実行されます。
cargo-chef は標準的な Cargo ワークフローとシームレスに統合し、ビルドカスタマイズの全範囲をサポートします:
ビルドコマンド:
build(デフォルト)check(--check フラグ経由)clippyzigbuildサポートされるオプション:
--release、--debug、カスタム--profile--features、--no-default-features、--all-features--target、--target-dir(ファーストクラスのクロスコンパイルサポート)--benches、--tests、--examples、--all-targets、--bins、--bin--workspace、--package、--manifest-path--offline、--frozen、--locked、--verbose、--timingscargo +nightly chef cookワークスペースサポートは自動です。cargo-chef はワークスペース内のすべてのクレートを検出し、正しく処理します。ファイルやクレートが移動しても、cargo-chef は自動的に適応します—Dockerfile の変更は不要です。これは、プロジェクト構造をハードコードする手動アプローチに対する大きな利点です。
ビルド済みDockerイメージは Docker Hub のlukemathwalker/cargo-chef で利用可能で、柔軟なタグ付けができます。
latest-rust-1.75.0(特定の Rust バージョンの最新 cargo-chef)0.1.72-rust-latest(最新の Rust の特定 cargo-chef)latest-rust-1.70.0-alpine3.18バージョンの一貫性: すべてのステージで同一のRustバージョンを使用すべきです。バージョンの不一致は、異なるコンパイラバージョンが異なるアーティファクトを生成するため、キャッシングを無効化します。
主なユースケース:
1. CI/CDパイプラインの最適化 - 標準的なユースケースです。すべてのコード変更が CI で Docker ビルドをトリガーします。cargo-chef なしでは、各ビルドが 500 以上のすべての依存関係を再コンパイルします(10〜20 分)。cargo-chef があれば、変更されていない依存関係はキャッシュされ、ビルドは 2〜3 分に短縮されます。これは次のような点に直接影響します。
2. マルチステージビルド - ビルド環境とランタイム環境を分離。ビルダーステージは完全な Rust ツールチェーン(800MB 以上)を含み、ランタイムステージは最小イメージ(25〜50MB)を使用します。cargo-chef は、高コストなビルダーステージをキャッシュ状態に保つことで、このパターンを実用的にします。
3.ワークスペース/モノレポプロジェクト - 依存関係を共有する複数のバイナリとライブラリを自動的に処理します。手動アプローチはワークスペースで破綻します; cargo-chef は透過的に処理します。
4. クロスコンパイル ---target フラグ経由でファーストクラスサポート。例: AlpineLinux デプロイのためにx86_64-unknown-linux-musl バイナリを CI でビルド。ターゲット指定は依存関係キャッシング中に尊重されます。
高度な最適化戦略:
sccacheとの組み合わせ:
FROM rust:1.75 ASbaseRUN cargo install--locked cargo-chef sccacheENV RUSTC_WRAPPER=sccache SCCACHE_DIR=/sccache# ... plannerステージ ...FROM base ASbuilderRUN--mount=type=cache,target=$SCCACHE_DIR,sharing=locked\ cargo chef cook--release--recipe-path recipe.json
この組み合わせは2層のキャッシングを提供します。
1 つの依存関係が変更された場合、cargo-chef はすべてを再ビルドしますが、sccache は個々のクレートコンパイルをキャッシュします。変更された依存関係のみが実際に再コンパイルされます。
BuildKitキャッシュマウント:
RUN--mount=type=cache,target=/usr/local/cargo/registry\--mount=type=cache,target=/usr/local/cargo/git\ cargo chef cook--release--recipe-path recipe.json
これは cargoレジストリ自体をキャッシュし、再ダウンロードを回避します。sccache および cargo-chef と組み合わせることで、Rust Docker ビルドの現在のベストプラクティスとなります。
作業ディレクトリの制約 -cargo cook とcargo build は、Cargo の*.d ファイル内の絶対パスのため、同じディレクトリから実行すべきです。これは Docker では煩わしくありませんが、認識すべきです。
ローカルパス依存関係 - プロジェクト外の依存関係(path = "../other-crate" で指定)は、変更されていなくてもゼロから再ビルドされます。これは、タイムスタンプベースのフィンガープリントに関連する Cargo の制限(issue #2644)です。コピーするとタイムスタンプが変更され、フィンガープリントが無効になります。
ローカル開発には不向き - cargo-chef はコンテナビルド専用に設計されています。既存のコードベースでローカルに実行すると、ファイルが上書きされる可能性があります。このツールは、ターミナル環境で実行される場合の安全警告を含みます。
ワークスペースの動作 -cargo chef cook はデフォルトですべてのワークスペースメンバーをビルドします。1 つのサービスのみが必要な大規模ワークスペースの場合、これによりビルド時間が増加する可能性があります。回避策には、ターゲットビルドフラグまたはサービスごとの個別の Dockerfile が含まれます。
最適なユースケース - cargo-chef は以下に最大の利益を提供します。
非常に小規模なプロジェクト(少数の依存関係)の場合、オーバーヘッドが利益を上回る可能性があります。
注目すべき技術的決定:
JSONレシピ形式 - バイナリ形式ではなくJSON を使用し、レシピは人間が読めてデバッグ可能です。recipe.json を検査して、cargo-chef が何を抽出したかを正確に確認できます。
明示的なターゲット宣言 - 正規の場所にある場合でも、すべてのターゲットを明示的に宣言するように Cargo.toml を変更します。これにより、キャッシュ無効化全体で Cargo がそれらを確実に認識します。
マニフェスト操作 - 手動パースではなく、ワークスペース構造へのプログラマティックアクセスにcargo_metadata クレートを使用します。これにより Cargo の進化に伴う堅牢性が提供されます。
TOML順序保持 -preserve_order 機能を持つ TOML を使用して、シリアライゼーションを通じたラウンドトリップ時にマニフェスト構造の整合性を維持します。
安全機能 -atty クレートを使用したターミナル検出。対話的に実行された場合の警告メッセージ。ローカル環境での偶発的なファイル上書きを防ぐために、明示的なユーザー確認が必要です。
採用された設計パターン:
cargo-chef は、Cargo 自体が提供しない依存関係とソースコンパイルの分離を作成することで、Rust 特有の Docker レイヤーキャッシング問題を解決します。このツールの優雅さはシンプルさにあります: 依存関係管理を再発明するのではなく、Cargo が最も得意とすることを可能にする最小限の有効なプロジェクト構造を作成し、Docker のレイヤーキャッシングメカニズムと完璧に整合します。
必須のベストプラクティス:
cargo-chefを使用すべき場合:
cargo-chef は、Docker 経由で Rust アプリケーションをデプロイするチームにとって不可欠なツールに成熟しており、より良い開発者体験、より速いデプロイ、削減されたインフラコストに直接変換される測定可能なパフォーマンス改善を提供します。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。