ビルドツールチェーンの選定 — Makefileで済ますか専用ビルドシステムを入れるかの判断

ビルドツールの選定を「プロジェクトが大きくなったらBazel、小さければMakefile」という規模論だけで決めると、後から設計負債が積み上がります。判断の核心は規模ではなく、依存グラフの複雑さ・再現性の要件・チームが長期でメンテナンスできるかの三軸です。本記事では、Makefile・Bazel・Gradle・タスクランナーの特性を判断軸で整理しながら、どの時点で専用ビルドシステムを導入すべきかの設計基準を解説します。

Makefile・Bazel・Gradle・タスクランナーの選定判断軸比較

ツールが解決しようとしている問題の違い

Makefileは「ファイルの依存関係とコマンド実行の抽象化」を目的としており、シェルスクリプトの延長線上にある設計です。BazelはGoogleが開発した「入力・出力のコンテンツハッシュによるタスクの冪等性担保」を中核に置いており、そもそも解決しようとしている問題が異なります。Gradleは主にJVMエコシステム向けで、Groovy・Kotlin DSLによるビルドスクリプトとプラグインエコシステムの充実が強みです。タスクランナー(Taskfile・Justなど)は冪等性よりもコマンドの整理と実行統一を重視し、言語非依存で小さなコストで導入できます。

4ツールの選定判断軸比較

ツール 主な適用領域 依存グラフ管理 キャッシュ機能 学習コスト
Makefile C/C++・小〜中規模単一言語 ファイル単位・手動定義 タイムスタンプ比較のみ 低
Bazel 大規模・マルチ言語モノレポ ターゲット単位・自動解析 リモートキャッシュ対応 高
Gradle JVM・Android・KotlinMP タスクグラフ自動生成 インクリメンタル・ビルドスキャン 中〜高
タスクランナー 言語非依存・ワークフロー整理 ほぼなし なし〜簡易 低

切り替えを検討すべき3つのトリガー

専用ビルドシステムへの移行を前向きに検討するタイミングとして、3つのトリガーがあります。①依存モジュール数が10を超えたとき、②CIのビルド時間が5分を超えて増加傾向にあるとき、③再現しないビルドによる障害が月1回以上発生したとき。この3条件のうち2つが重なった場合は、Makefileの拡張で対処するよりも専用ビルドシステムへの移行計画を立てる段階と考えてください。

モノレポとマルチレポでビルド設計が変わる理由の整理

モノレポでビルドが複雑化する構造的理由

モノレポは「変更した範囲だけをビルドする選択的ビルド」が設計の核になります。Makefileをディレクトリごとに用意する方式では、上位パッケージへの変更波及を把握できず、どのサービスを再ビルドすべきかの判断が属人化します。BazelやTurborepo・NxなどのモノレポツールはWHAT(変更ファイル)からWHICH(影響ターゲット)を自動算出するため、CI実行時間の削減に直結します。選択的ビルドを正しく動かすには、依存関係の宣言をビルドツール側に任せる設計が前提になります。

マルチレポでの依存管理と境界設計

マルチレポはリポジトリごとにビルドが独立するため、チームの自律性を保ちやすい反面、共有ライブラリのバージョンが分散しやすくなります。ライブラリをnpm・Maven・Pythonパッケージとして外部公開するとリリースサイクルが発生し、破壊的変更を制御するSemVerと変更管理フローが必須になります。リポジトリ構造の選択はビルドシステムの選定より先に行うべき判断で、後から構造を変えるとビルド定義の全面書き直しが発生します。

ハイブリッド構成を選ぶ条件

コア共通ライブラリをモノレポで管理しつつ、外部公開SDKやサードパーティ統合を別リポジトリにするハイブリッド構成も現実的な選択肢です。ただしモノレポ側と外部リポジトリのビルドシステムが異なる場合、CIのキャッシュ設計を別々に考える必要があります。ハイブリッドを選ぶ条件は「外部公開が必要・リリースサイクルが異なる・法的または契約上の分離が必要」の3点が該当するケースに絞り、それ以外はモノレポに統一する方がビルド管理の複雑化を避けられます。

ビルド速度のボトルネックをどこで計測し何で解決するか

計測なき最適化は方向が決まらない

ビルドが遅い原因を「なんとなくコンパイルが重い」で片付けると、効果のない最適化に工数を費やします。計測すべき指標は3つです。①フルビルド時間(変更ゼロ状態でのビルド全体)、②インクリメンタルビルド時間(1ファイル変更後)、③CIキャッシュのヒット率。Bazelには組み込みのプロファイラがあり、GradleにはBuild Scan機能があります。Makefileにはネイティブのプロファイラがないため、ターゲット単位の計測を自前で用意する必要があります。

並列化・キャッシュ・分散ビルドの使い分け

ボトルネックが特定できたら、解決手段を選択します。並列化はCIのコア数に応じてスケールするため、マルチコア環境では即効性があります。キャッシュはローカルとリモートの2層で考え、チーム規模が10人を超えるとリモートキャッシュの共有が大きな時間削減につながります。分散ビルド(Bazelのリモート実行など)は専用インフラを要するため、フルビルド時間が30分を超えるような大規模プロジェクトを対象として位置づけてください。

キャッシュ無効化の設計が速度を左右する

キャッシュが適切に無効化されないと、古い成果物を使い続けるリスクが生まれます。Bazelは入力ファイルのコンテンツハッシュでキャッシュキーを生成するため、ツールチェーン・環境変数・コンパイラバージョンを全て入力として明示する必要があります。Makefileはタイムスタンプ比較のため、gitのclone直後などタイムスタンプが変わらない状態でキャッシュが正しく機能しないケースがあります。キャッシュ無効化の設計は、次節で述べる再現性の定義と表裏一体の関係にあります。

再現性のあるビルドをどう定義し、どこで担保するか

再現性の3層モデルで考える

再現性を「同じソースから同じ成果物が作れること」だけと定義すると、設計が甘くなります。再現性には3層あります。①ソースレベル(同一コミットから同一バイナリ)、②環境レベル(異なるCIマシンでも同結果)、③時間レベル(半年後にも同じビルドが通る)。多くのプロジェクトが対応しているのは①だけで、②③は意識して設計しないと達成できません。障害対応時のリビルドで②が壊れている状況は、一般的な失敗パターンとして広く見られます。

環境依存を排除する設計方針

環境レベルの再現性を担保するには、ビルドで使うツールチェーン・コンパイラ・ランタイムバージョンをビルド定義に含めます。Dockerイメージをビルド環境として固定する方法が広く使われていますが、イメージ自体のビルド定義もバージョン管理しなければ時間レベルの再現性が壊れます。Nixのようなパッケージマネージャで環境をコード化する方法は学習コストが高い反面、時間レベルの再現性まで構造的に担保できます。

ロックファイルとツールバージョンのピン留め

依存ライブラリのロックファイル(package-lock.json・Cargo.lock・go.sumなど)はビルド再現性の最低条件です。ロックファイルを.gitignoreに追加するアンチパターンは一般的なつまずきの筆頭として挙げられます。ビルドツール自体のバージョンもピン留めが必要で、.tool-versions(asdf)・.nvmrc・Gradleのwrapperのように、ツールバージョンをリポジトリに含める仕組みを採用することで、「自分の環境だけビルドが落ちる」という問題を構造的に防げます。

依存関係の変更がどこまで波及するかを管理する設計

依存グラフの可視化を先行させる

依存関係の管理は、まず依存グラフの可視化から始まります。Bazelはクエリコマンドでターゲット間の依存を出力でき、GradleはdependenciesタスクでツリーをHTMLレポートとして確認できます。Makefileは依存グラフの可視化機能がないため、ツール規模が大きくなる前に可視化スクリプトを自前で用意するか、専用ツールへの移行を検討するタイミングの一つとして捉えてください。グラフを可視化せずにライブラリをアップデートすると、影響範囲の見積もりが困難になります。

循環依存の検出と設計での解消

循環依存はビルドシステムの崩壊につながる根本的な問題です。BazelとGradleはビルド定義の段階で循環依存をエラーとして検出します。Makefileは循環依存を検出しないため、手動での管理が必要です。アーキテクチャ的には依存関係を上位レイヤーから下位レイヤーへの一方向に徹底し、相互依存が発生した場合はインターフェース層を挟んで解消します。この設計ルールをビルド定義で強制できるかどうかが、ツール選定の判断材料の一つになります。

外部依存のアップデート戦略

外部ライブラリの依存管理は、ビルドシステムと自動更新ツールを組み合わせて設計します。DependabotやRenovateで自動PRを生成する場合、プロジェクトが50モジュール以上になると週に数十件のPRが作成されることがあるため、バージョン更新の優先度付けとグループ化のルールを事前に設計してください。セキュリティアップデートと機能アップデートで承認フローを分けることで、対応漏れを防ぎやすくなります。

ビルドスクリプトのメンテナンス負荷をどこで見切るか

メンテナンス負荷が増大するサイン

ビルドスクリプトのメンテナンス負荷は「修正するたびにビルドが壊れる」「新メンバーがビルド手順を理解するのに2〜3日かかる」「ビルドスクリプトを積極的に誰も触りたがらない」という状態で可視化されます。特にMakefileが数百行を超え、条件分岐と変数展開が複雑に絡み合い始めるとこのサインが現れやすくなります。この段階で「Makefileを継続改善するか」「専用ビルドシステムに移行するか」の判断が必要になります。

移行を判断するためのチェックリスト

移行コストとメンテコストのトレードオフを判断するために、以下の基準を参考にしてください。3項目以上が該当する場合は、移行の検討を優先してください。

  • ビルドスクリプトの変更頻度が月3回以上ある
  • ビルド失敗の原因特定に30分以上かかることが週1回以上ある
  • 新メンバーへのビルド周りの説明に半日以上かかっている
  • 再現しないビルドの問題が月1回以上発生している
  • CI時間がコード変更量に比べて増加傾向にある

想定例として、10サービス・3言語・5人チームの構成でBazelへの移行には2〜4週間の工数を見込む必要があります。この移行コストを「現状のメンテナンス負荷がどれだけ累積するか」と比較して判断の材料にしてください。

段階的移行の設計方針

一括移行は失敗リスクが高いため、段階的移行を設計します。新規サービスだけ新ビルドシステムで立ち上げ、既存サービスは旧ビルドシステムのまま残す「二重管理期間」を設けます。この期間は最大3か月を目安とし、それ以上続くと二重管理自体がメンテ負荷になります。移行の完了条件を「旧ビルドシステムの定義ファイルがゼロになった時点」と明示し、曖昧な完了宣言をしないことが移行成功の要件です。

落とし穴 — ツール移行で行き詰まりやすいビルド設計の負債パターン

シェルスクリプトを大量に埋め込んだMakefileの移行困難

MakefileにシェルスクリプトをインラインŸで大量に埋め込んだ構成は、依存グラフをMakefileが管理しつつロジックをシェルが持つ混在状態になり、専用ビルドシステムへの移行時に最も行き詰まりやすいパターンです。ビルドツールの移行とシェルスクリプトのリファクタリングが同時に必要になり、当初見積もりより工数が大幅に増えることが一般的な失敗として報告されています。シェルスクリプトをビルド定義から切り離し、独立したスクリプトファイルとして管理しておくことが対策になります。

環境変数に依存したビルド定義

ビルド定義が環境変数を多用すると、再現性の層が崩れます。よくあるつまずきとして、CIサーバーとローカル環境で異なる環境変数が設定されており「CIでは通るがローカルで落ちる」「ローカルでは通るがCIで落ちる」という非決定的なビルドが発生します。Bazelはビルドで使う環境変数を明示的にリストアップしない限り無視する設計になっており、この問題を構造的に防いでいます。Makefileを使う場合は、環境変数をビルド定義内でデフォルト値付きで宣言し、未設定時の挙動を明示してください。

ビルドとデプロイの責任境界の混在

ビルドスクリプトの中にデプロイ処理・データベースマイグレーション・通知送信が含まれると、ビルドシステムとデプロイパイプラインの責任境界が曖昧になります。この混在は10〜30サービス規模の中規模プロジェクトで起こりやすく、ビルドシステムの移行時に同時整理が必要になって移行コストが膨らむ要因になります。ビルドシステムの責任を「コードから成果物(バイナリ・コンテナイメージ・パッケージ)への変換のみ」と定義し、デプロイはCDパイプラインに委譲する設計を徹底してください。

▶ 手を動かして本番まで到達する

脱属人化の第一歩は「読める・書けるスクリプト」から始まります

ここまでの設計判断は、読めば腹に落ちます。ですが「自分の手で本番まで通しきる自信」は、それとは別の話になります。

自動化の設計を、手を動かして身につける2日間です。手順書に閉じ込めた構築を、他人が読めて再現できる形へ変えていきます。

ビルドツールチェーンの設計をスクリプトで実装するシェルスクリプト実践コースへ →

シェルスクリプトマスターセミナー|自動化の設計を手で身につける2日間