
Summary
この文書の要点
- 依存グラフとツールチェーンを宣言し、環境差ではなく入力から成果物を決める。
- BUILD定義は手編集せず、workspaceの一次情報から生成してドリフトを検出する。
- ビルド成功だけで完了とせず、成果物を起動してヘルスチェックまで確認する。
- 本番の切り替えは可逆に保ち、小さな単位から段階的に進める。
なぜビルドの仕組みを作り直すのか
pnpmで管理するモノレポに、サービスごとのDockerfileや個別スクリプトが増えていくと、ビルドの成立条件が見えにくくなります。ローカルのNode.jsの版、依存パッケージの巻き上げ、暗黙に参照している共有ファイルが混ざると、手元では通るのにCIやコンテナ内では失敗する状態が起きます。規模が大きいほど、原因の切り分けにも時間がかかります。
もう一つの問題は、変更の影響範囲です。ファイルの置き場所だけで再ビルド対象を選ぶと、共有パッケージを変えた時に本当に確認すべきサービスを取りこぼしたり、反対に無関係なサービスまでビルドしたりします。ビルドが依存関係の全体像を持たない限り、差分検証を正確にすることはできません。
そこで、ソース、依存パッケージ、ツールチェーンを明示的な入力として扱い、宣言外の環境に依存しないhermetic buildを採用します。Bazelは依存グラフを保持するため、同じグラフの上でビルド、テスト、コンテナイメージを定義できます。採用理由はツールの多機能さではなく、再現性と影響範囲を構造として扱える点にあります。
成果物の種類を先に整理し、同じグラフに載せる
対象のサービスは、静的フロントエンド、バックエンドまたはワーカー、リバースプロキシという三種類の成果物に整理できます。静的フロントエンドはViteやReact Routerで生成した配信ファイルをオブジェクトストレージへ置き、バックエンドとワーカーはバンドルしてOCIイメージにし、ゲートウェイも独立したOCIイメージとして扱います。
ここで重要なのは、コンテナという実装手段と配信要件を混同しないことです。SSRを必要としない画面をコンテナで配信し続けるより、静的成果物として配信する方が、責務と運用経路を小さくできます。一方、ホスト名や経路で複数の配信先を振り分けるゲートウェイは、コンテナとして残す理由があります。
静的配信、OCIイメージ、テストを別々の自動化として持つのではなく、いずれもBazelのターゲットとして扱います。成果物の種別ごとに実行方法は異なっても、どの入力がどの成果物へつながるかを一つの依存グラフで追えることが、差分ビルドの前提になります。
- 静的フロントエンド: 配信ファイルを生成し、オブジェクトストレージへ配置する。
- バックエンド・ワーカー: バンドルとOCIイメージを生成し、実行環境へ渡す。
- ゲートウェイ: 配信先を振り分ける責務を独立したイメージに閉じ込める。
BUILD定義は生成器を唯一の情報源にする
多数のworkspaceにBUILDファイルを手で置き、依存変更のたびに人が追随する運用は長続きしません。すでにpackage.jsonに書かれているworkspaceと依存関係を一次情報として読み取り、各workspaceのBUILD定義、集約ターゲット、テストターゲットを生成する方式にします。人が編集する対象を、実態を表すpackage.jsonと生成器へ絞ることが目的です。
生成したBUILD定義はリポジトリへ含めます。レビューで依存グラフの変化を確認でき、Bazelを実行しない環境でも定義を参照できるためです。ただし、生成物を正としないことが重要です。CIでは再生成した差分がないかを検査し、BUILDファイルだけを直接書き換えた変更は失敗として扱います。
生成器には、依存の向きも検査させます。アプリケーションから機能パッケージ、機能パッケージから共有パッケージへ、というように許可する方向を決めておくと、逆方向の参照が入り込んだ時点で止められます。生成は単なる手間の削減ではなく、依存規律を継続して守るための仕組みです。
hermetic化で表面化する、見えにくい依存
hermetic buildへ移すと、以前はたまたま通っていた経路が失敗として現れます。それはBazelの制約というより、暗黙の依存や環境差が可視化された状態です。失敗を回避するためにサンドボックスを緩めるより、どの入力が足りないかを一つずつ宣言へ戻す方が、後の保守では安全です。
代表的なのは、ツールチェーンの固定、内部パッケージの入力への追加、依存の明示です。Node.jsやBunの版を範囲指定のままにせず、取得元とハッシュを含めて固定します。ViteやReact Routerが相対参照する内部ソース、複数のtsconfig.jsonが共有するextends先も、ビルド入力として漏れなく渡します。
特にpnpmの巻き上げに頼る構成では、宣言していないパッケージを偶然importできることがあります。パッケージ単位で厳格に依存を解決する環境では失敗しますが、修正は単純です。使うパッケージを、そのパッケージ自身のpackage.jsonへ明示します。この確認を通すことで、依存グラフの精度が上がります。
- Node.jsやBunは完全なバージョンと取得物のハッシュまで固定する。
- Viteはbuild用のサブコマンドを明示し、開発サーバーが待ち続けないようにする。
- 内部workspaceのソースと共有設定は、参照経路を含めて入力へ加える。
- Bunの出力先は、symlinkの解決結果に任せず明示的に固定する。
- 宣言されていない依存は、巻き上げで通してしまわずpackage.jsonへ戻す。
- 実行環境のCPUアーキテクチャを決め、開発機との差はクロスビルドとして扱う。
ビルドの成功と、起動できることを分けて検証する
テストも依存グラフ上のターゲットにします。そうすると、共有パッケージを変更した時に影響を受けるテストだけを選んで実行できます。ただし、型チェックやテストが通っても、生成したコンテナが起動するとは限りません。出力の配置、起動コマンド、ファイル所有者、実行ユーザーは、静的なビルド検証だけでは保証できないためです。
この差を埋めるため、生成したイメージを実際に起動し、ヘルスチェックまで確認するsmokeを用意します。バックエンドは接続が遅延する外部サービスに到達しなくても起動できる最小設定で確認し、ゲートウェイは想定どおりに上流へ振り分けられることまで見ます。外部サービスの成功を無理に再現するのではなく、コンテナ自身の起動責務を確かめます。
この検証で見つかりやすいのは、バンドルをイメージへ配置する際にディレクトリが一段深くなり、起動コマンドが期待するエントリを見つけられない問題です。非rootで動かすゲートウェイでは、設定や一時ファイルの書き込み権限が不足することもあります。どちらもビルドは成功し得るため、起動smokeをパイプラインから外さないことが重要です。
- 単体テストと型チェックをBazelのターゲットとして定義する。
- イメージを対象CPUアーキテクチャで起動し、ヘルスチェックを確認する。
- 起動コマンド、成果物の配置、実行ユーザー、書き込み権限を別の検証項目として扱う。
- 失敗時は、ビルドログだけでなくコンテナの標準出力と終了コードを保存する。
環境差は同じターゲットの設定として扱う
静的フロントエンドでは、認証やAPIの接続先をビルド時にバンドルへ埋め込むことがあります。本番用に作った成果物をそのまま検証環境へ置くと、利用者を本番の接続先へ誘導してしまうおそれがあります。成果物を環境ごとに作り分ける必要があるなら、その違いを人の手順ではなくビルド設定として明示します。
Bazelでは、環境を表すビルド設定とselect()を使い、同じターゲットに対して値だけを切り替えられます。環境ごとの変換規則は一箇所の共有モジュールへ集約し、デプロイ選択のスクリプトと別々に書かないようにします。規則を二重に持つと、片方だけが古くなり、検証環境でだけ発生する不具合につながります。
ただし、環境別の成果物はキャッシュと識別子の扱いを複雑にします。どの設定で生成した成果物かを出力メタデータへ残し、本番と検証環境のキャッシュを混同しないようにします。環境差をなくせない場合ほど、差がどこにあるかをビルドグラフ上で追えることが重要です。
切り替えは可逆にし、未完了の課題を残す
Bazelで成果物を作れることと、既存のデプロイ経路を置き換えられることは別の段階です。切り替え前には、CI上で使うビルドイメージ、認証、リモートキャッシュ、検証環境での起動確認を準備する必要があります。準備が終わるまでは従来経路を既定のまま残し、新しい経路を明示的に選んだ場合だけ実行できるようにします。
切り替えは、ゲートウェイまたは小さな静的配信を一つ確認し、次にバックエンドを一つ、最後に残りへ広げる順序が安全です。各段階で、ビルド、対象テスト、起動smoke、配信後のヘルスチェックを通し、異常があれば従来経路へ戻せることを確認します。一度に全サービスを切り替えないことが、移行自体の障害範囲を小さくします。
すべてを同時に緑にしようとしないことも大切です。厳格な依存解決へ移すと、型定義用パッケージなど、これまで巻き上げに依存していた箇所が残課題として現れます。どのチェックを移行ゲートにするか、どの失敗を明示的な修正対象として残すかを分け、現状を曖昧にしない運用が必要です。
移行で持ち帰る判断軸
hermetic buildの価値は、宣言された入力だけで成果物を作れることに加え、宣言されていなかった依存を見つけられることにあります。最初は既存のビルドより厳しく感じても、その失敗をpackage.json、入力定義、ツールチェーンの固定へ戻していくことで、依存関係を実態に近づけられます。
また、ビルドの緑と起動の緑は別です。成果物の配置、起動コマンド、権限といった実行時の条件は、コンテナを起動するまで分かりません。依存グラフ上のテストと、起動smokeを組み合わせて初めて、成果物を配る前の確認が一続きになります。
大規模なモノレポで重要なのは、Bazelを導入すること自体ではありません。BUILD定義をどこから生成するか、環境差をどう表現するか、どの検証を必須にするか、失敗時にどこまで戻せるかを先に決めることです。移行を段階的かつ可逆に設計すれば、再現性を高めながら日々のリリースを止めずに進められます。


