インクリメンタルビルドとコンテンツアドレス指定
ビルドが毎回フルで走り遅いのは、入力ハッシュをキーにした成果物キャッシュと依存グラフで解けます。ハーメティック性・リモートキャッシュ・ヒット率の原理を、Bazel系の仕組みから正確に押さえます。
- 1.成果物のキャッシュキーは入力(ソース・コマンドライン・ツールチェイン・環境)のハッシュであり、同じ入力なら同じ出力という決定性が前提。出力アドレスではなく入力ハッシュで引くのがコンテンツアドレス指定の核。
- 2.依存グラフ(DAG)上で入力ハッシュが変わったノードだけ再評価し、変わらないノードはキャッシュから復元する。これが最小再ビルド。グラフの正しさはハーメティック(封印された)実行で担保される。
- 3.リモートキャッシュは同じ入力ハッシュの成果物をチーム・CIで共有しヒット率を底上げする。ヒット率はハーメティック性・キーの粒度・ツールチェイン固定に支配され、非決定性が混じると静かに低下する。
なぜ「毎回フルビルド」は無駄なのか
大規模なコードベースでは、1行直しただけでも全体を再ビルドすると数十分かかります。だが直感的にわかるとおり、変えていないモジュールの成果物は前回とまったく同じはずです。変わった部分とその下流だけを作り直せば足りる——これがインクリメンタルビルドの出発点です。問題は「変わった部分」をどう正確に判定するかにあります。
素朴な手段はファイルの最終更新時刻(mtime)の比較です。Make が古典的に使う方式ですが、これは脆い。touch しただけ・チェックアウトし直しただけで mtime は更新され、中身が同じでも再ビルドが走ります。逆に時計が巻き戻れば、変わったのに再ビルドされない取りこぼしも起きます。タイムスタンプは変更の代理指標にすぎず、内容そのものを見ていないのが根本問題です。
そこで現代のビルドシステム(Bazel、Buck2、Gradle、Nx 等)は判定軸を内容のハッシュに置き換えます。入力の中身が同じなら出力も同じ、という前提を立て、入力ハッシュをキーにして成果物をキャッシュする。これが**コンテンツアドレス指定(content-addressable)**の発想です。
コンテンツアドレス指定ストレージ(CAS)一般では「データの中身のハッシュをそのデータのアドレスにする」(Git のオブジェクトストアが典型)。ビルドキャッシュでは一歩進めて、ある成果物を生む入力一式のハッシュをキーに、その成果物(出力)を値として格納します。同じ入力ハッシュで引けば、過去に誰がいつ作った成果物でもそのまま再利用できる——これが再現性とキャッシュ共有の土台です。
キャッシュキーは何のハッシュか
ここが最重要です。キャッシュキーに含めるべきは「ソースファイルの中身」だけではありません。出力を決定づけるすべての入力を漏れなく取り込む必要があります。1つでも抜けると、本当は違う出力になるべき2つのビルドが同じキーに衝突し、誤った成果物を返してしまいます。
あるアクション(コンパイル等)のキャッシュキー =
hash(
入力ソースファイルの内容ハッシュ群,
実行するコマンドライン(引数・フラグ),
ツールチェインの識別子(コンパイラのバージョン・ハッシュ),
関連する環境変数,
プラットフォーム(OS・CPUアーキテクチャ),
依存する他アクションの出力ハッシュ
)
最後の「依存する他アクションの出力ハッシュ」がグラフを連結します。あるノードのキーは入力ノードの出力ハッシュを含むので、上流が1つでも変われば、その下流のキーは芋づる式にすべて変わる。これにより「変更が波及した範囲だけキーが変わり、それ以外は不変」という性質が自動的に成立します。
| 判定軸 | mtime方式(Make) | 内容ハッシュ方式(Bazel系) |
|---|---|---|
| 変更検知 | 更新時刻の新旧比較 | 入力内容のハッシュ一致 |
| touch/再取得 | 中身同じでも再ビルド | ハッシュ不変なら再利用 |
| キャッシュ共有 | 基本ローカルのみ | ハッシュキーで他者・CIと共有可 |
| 取りこぼし | 時計ずれで発生しうる | 入力を網羅すれば原理的に起きない |
コンパイラのバージョン、PATH や LANG などの環境変数、システムヘッダ、タイムゾーン——これらは出力に影響するのにキーから漏れやすい入力です。漏れると、ローカルでは通るのに CI では別物が出る、あるいは古いキャッシュが誤ってヒットする、といった再現困難なバグになります。「同じ入力なら同じ出力」を保証する鍵は、出力に効く入力を一切キーから漏らさないことに尽きます。
依存グラフと最小再ビルド
ビルドは本質的に**有向非巡回グラフ(DAG)**です。ノードはアクション(コンパイル・リンク・コード生成など)、エッジは「この成果物はあの成果物を入力にする」という依存関係を表します。ビルドツールはまずこのグラフを構築し(解析フェーズ)、次に各ノードのキーを葉から根へ計算していきます。
最小再ビルドのアルゴリズムは概念的にこうです。
最小再ビルド(概念)
グラフを入力(葉)側からトポロジカル順に走査:
各ノード N について:
key = hash(N の入力内容 + 依存ノードの出力ハッシュ + コマンド + ツール)
if キャッシュに key が存在:
出力をキャッシュから復元(アクションは実行しない)
else:
アクションを実行し、出力と key をキャッシュに保存
ポイントは、変更が起きたノードのキーだけが変わり、そこから到達可能な下流のキーが連鎖的に変わることです。変更ノードの上流(祖先)や、変更と無関係なサブグラフはキー不変なのでキャッシュヒットし、再実行されません。結果として再ビルドの量は「変更の影響を受けるノード数」に比例し、コードベース全体の規模には依存しなくなります。
ここで効くのが早期カットオフ(early cutoff)です。あるノードを再実行した結果、出力が前回とバイト単位で同一だったとします。すると出力ハッシュは変わらないので、その下流のキーは変わらず、下流は再ビルド不要になる。たとえばコメントだけ直してオブジェクトファイルが同一になれば、リンク以降は走りません。mtime 方式は「上流が触られた」事実だけで下流まで連鎖再ビルドしてしまうので、この枝刈りができません。
依存グラフは実際の入出力依存を過不足なく反映している必要があります。宣言した依存より多くを実際に読む(未宣言依存=under-declared)と、その入力が変わってもキーが変わらず取りこぼします。逆に使いもしない依存を宣言する(over-declared)と、無関係な変更で過剰再ビルドします。Bazel が BUILD ファイルで依存を明示させ、サンドボックスで未宣言アクセスを検出するのは、この両方を機械的に潰すためです。
ハーメティック性:キャッシュが正しく効く前提
「同じ入力なら同じ出力」が成り立って初めて、入力ハッシュでのキャッシュは安全です。この決定性を支える性質が**ハーメティック性(hermeticity、封印性)**です。ハーメティックなビルドとは、宣言された入力以外に一切依存しないビルドを指します。
非ハーメティックな汚染源は多岐にわたります。
| 汚染源 | 何が混入するか | 対策 |
|---|---|---|
| システムのツール参照 | PATH上のローカルなコンパイラ等 | ツールチェインを固定しキーに含める |
| ネットワークアクセス | 外部URLの取得結果が時期で変化 | 依存をハッシュ固定で取得・サンドボックスで遮断 |
| 時刻・乱数・PID | 出力にタイムスタンプ等が混入 | ビルド中の非決定性を排除(再現可能ビルド) |
| 未宣言ファイル | グラフ外の入力を実際に読む | サンドボックス/リモート実行で隔離 |
Bazel 系がアクションをサンドボックス(宣言した入力だけを見せ、それ以外のファイルシステムとネットワークを遮断した一時環境)で実行するのは、ハーメティック性を強制するためです。サンドボックス内で未宣言のファイルを読もうとすれば失敗するので、グラフの不備がビルド時に顕在化します。これは /devops/iac/ で宣言的に環境を固定する発想や、/devops/gitops/ がリポジトリを単一の真実源とする発想と同じ系譜——入力を完全に明示し、暗黙の状態をなくす思想です。
非決定性が1つでも混じると、キャッシュは静かに壊れます。たとえば出力にビルド時刻を埋め込むと、同じソースでも毎回出力ハッシュが変わり、下流のキーが毎回変わってキャッシュが効きません。再現可能ビルド(reproducible build)——タイムスタンプ正規化・ファイル順序の固定・乱数シード除去——は、キャッシュ命中率を上げるための前提でもあるのです。
リモートキャッシュとヒット率の原理
ローカルキャッシュは「自分が一度ビルドしたもの」しか再利用できません。リモートキャッシュは、入力ハッシュをキーにした成果物をチーム全体・CI で共有します。コンテンツアドレス指定だからこそこれが成立する——キーは入力の内容だけで決まり、誰がどのマシンで作ったかに依存しないので、同僚や CI が作った成果物を自分がそのまま引けます。
Bazel の Remote Execution API はこれを2つのサービスに分けます。**CAS(Content-Addressable Storage)**は成果物(出力ファイルやディレクトリ)をその内容ハッシュで格納するストア。Action Cache は「アクションのキー → そのアクションの結果(出力の参照)」のマップです。流れはこうです。
リモートキャッシュ参照の流れ
1. アクションキーを計算(入力・コマンド・ツールのハッシュ)
2. Action Cache にそのキーがあるか問い合わせ
ヒット → 結果メタデータを取得し、必要な出力を CAS から取得
ミス → アクションを実行(ローカル or リモート実行)し、
出力を CAS に、キー→結果を Action Cache に登録
CI を考えると価値が際立ちます。プルリクエストごとにクリーン環境でフルビルドする代わりに、main ブランチのビルドが温めたキャッシュを各 PR が引けば、変更分だけ実行すれば済む。/devops/ci-cd/ のパイプライン時間が、コードベース規模ではなく差分の大きさに比例するようになります。生成された成果物自体は /devops/artifact-registry/ で配布管理するのに対し、リモートキャッシュは中間成果物の再計算回避が目的、という役割の違いも押さえておきましょう。
ヒット率(cache hit rate)は何で決まるのか。原理的には「今回のアクションキーが過去のキーと一致する確率」です。したがって次の要因がヒット率を支配します。
キーに余計な入力(絶対パス・ホスト名・ビルド番号・タイムスタンプ)が混じると、本来同一のはずのアクションキーがマシンや実行ごとに変わり、ヒット率が崩壊します。やっかいなのは、ビルド自体は成功し続けるので遅くなったことにしか気づけない点です。診断では「同じ入力で2回ビルドし、2回目がフルヒットするか」を確認し、ミスしたアクションのキー差分(どの入力ハッシュが食い違ったか)を追います。--execution_log 等でアクションごとの入力ハッシュを突き合わせるのが定石です。
ヒット率を上げる原則は単純です。第一にハーメティック性を高める——出力に効かない揺らぎ(時刻・パス・順序)を排除し、決定性を担保する。第二にキーの粒度を適切に保つ——粗すぎると無関係な変更で全滅し、細かすぎると管理オーバーヘッドが増える。第三にツールチェインを固定する——コンパイラやSDKのバージョンが人によってぶれると、同じソースでもキーが分裂します。ツールチェイン自体もハッシュしてキーに含めるのが正しい。
キャッシュキーは出力アドレスではなく入力(ソース・コマンド・ツール・環境・依存出力)のハッシュであること、最小再ビルドはDAG上でキーが変わったノードと下流だけを再評価し、出力が同一ならearly cutoff で連鎖を止めることを説明できると強いです。リモートキャッシュ共有とヒット率がハーメティック性と決定性に支配されること、Bazel が CAS と Action Cache を分離しサンドボックスで未宣言依存を弾くことも頻出です。mtime 方式との対比は基本中の基本。
まとめ
- インクリメンタルビルドの核は、mtime ではなく入力内容のハッシュで変更を判定すること。同じ入力なら同じ出力という決定性が大前提。
- キャッシュキーにはソース・コマンドライン・ツールチェイン・環境・依存ノードの出力ハッシュを漏れなく含める。1つでも漏れると誤ヒットや取りこぼしを生む。
- 最小再ビルドは DAG 上でキーが変わったノードと下流だけを再評価する。出力がバイト同一なら early cutoff で下流の連鎖を止められる。
- キャッシュが正しく効く前提はハーメティック性。サンドボックスと再現可能ビルドで暗黙の依存と非決定性を排除する。
- リモートキャッシュは入力ハッシュで成果物をチーム・CIに共有する。Bazel は CAS と Action Cache を分離。ヒット率はハーメティック性・キー粒度・ツールチェイン固定に支配され、非決定性が混じると静かに低下する。
DevOps/インフラ Article
インクリメンタルビルドとコンテンツアドレス指定を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ビルド
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
依存グラフ(DAG)上で入力ハッシュが変わったノードだけ再評価し、変わらないノードはキャッシュから復元する。これが最小再ビルド。グラフの正しさはハーメティック(封印された)実行で担保される。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ビルド / キャッシュ」に近いか確認する。
- 強みである「成果物のキャッシュキーは入力(ソース・コマンドライン・ツールチェイン・環境)のハッシュであり、同じ入力なら同じ出力という決定性が前提。出力アドレスではなく入力ハッシュで引くのがコンテンツアドレス指定の核。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。