前のページを見ていくと、1 つのテーマが何度も繰り返されています。独自のコードの管理は比較的簡単ですが、その依存関係の管理ははるかに困難です。依存関係にはさまざまな種類があります。タスクへの依存関係(「リリースを完了としてマークする前にドキュメントを push する」など)もあれば、アーティファクトへの依存関係(「コードをビルドするには、最新バージョンの Computer Vision ライブラリが必要」など)もあります。コードベースの別の部分への内部依存関係がある場合もあれば、別のチーム(組織内またはサードパーティ)が所有するコードまたはデータへの外部依存関係がある場合もあります。いずれにしても、「これがないとあれができない」という考え方は、ビルドシステムの設計で繰り返し発生するものであり、依存関係の管理はビルドシステムの最も基本的なジョブと言えるでしょう。
モジュールと依存関係の処理
Bazel などのアーティファクト ベースのビルドシステムを使用するプロジェクトは、一連
のモジュールに分割されます。モジュールは BUILD
ファイルを介して相互に依存関係を表します。これらのモジュールと依存関係を適切に整理すると、ビルドシステムのパフォーマンスとメンテナンスに必要な作業量の両方に大きな
影響を与える可能性があります。
細かい設定が可能なモジュールと 1:1:1 ルールの使用
アーティファクト ベースのビルドを構造化する際に最初に問題となるのは、
個々のモジュールがどの程度の機能を含む必要があるかを決定することです。Bazel では、
モジュールは java_library や go_binary などのビルド可能なユニットを指定するターゲットで表されます。一方の極端な例として、ルートに 1 つの BUILD ファイルを配置し、
そのプロジェクトのすべてのソースファイルを再帰的に glob することで、プロジェクト全体を
1 つのモジュールに含めることができます。もう一方の極端な例として、ほぼすべてのソースファイルを独自のモジュールにすることができます。つまり、各ファイルは、依存する他のすべてのファイルを BUILD ファイルにリストする必要があります。
ほとんどのプロジェクトはこれらの極端な例の中間に位置し、選択には
パフォーマンスと保守性のトレードオフが伴います。プロジェクト全体に 1 つのモジュールを使用すると、外部依存関係を追加する場合を除き、BUILD ファイルを変更する必要がない可能性がありますが、ビルドシステムは常にプロジェクト全体を一度にビルドする必要があります。つまり、ビルドの一部を
並列化または分散したり、すでにビルドした部分を
キャッシュに保存したりすることはできません。ファイルごとに 1 つのモジュールを使用する場合は逆になります。ビルドシステム
のビルドステップのキャッシュ保存とスケジューリングの柔軟性は最大になりますが、
エンジニアは、どのファイルがどのファイルを参照するかを変更するたびに、依存関係のリストを維持するために多くの労力を費やす必要があります。
正確な粒度は言語によって異なりますが(
言語内でも異なることがよくあります)、Google では、タスクベースのビルドシステムで通常記述されるよりもはるかに小さいモジュールを優先する傾向があります。Google
の一般的な本番環境バイナリは、多くの場合、数万のターゲットに依存しています。中規模のチームでも、コードベース内に数百のターゲットを所有している可能性があります。パッケージングの概念が組み込まれている
Java などの言語では、通常、各ディレクトリに 1 つのパッケージ、ターゲット、BUILD ファイルが含まれています(Pants は Bazel に基づく別のビルドシステムで、これを 1:1:1 ルールと呼びます)。パッケージングの規則が弱い言語では、
ファイルごとに複数のターゲットが定義されることがよくあります。BUILD
ビルド ターゲットが小さいほど、大規模な場合にメリットが大きくなります。これは、分散ビルドが高速化され、ターゲットの再ビルドの必要性が低くなるためです。テストが導入されると、メリットはさらに大きくなります。
粒度の細かいターゲットを使用すると、ビルドシステムは、
特定の変更の影響を受ける可能性のあるテストの限定されたサブセットのみを
実行するようになります。Google は、ターゲットを小さくすることのシステム上のメリットを信じているため、デベロッパーの負担を軽減するために、BUILD ファイルを自動的に管理するツールに投資することで、デメリットを軽減してきました。
これらのツールの一部(buildifier、buildozer など)は、
Bazel の
buildtools ディレクトリで使用できます。
モジュールの可視性の最小化
Bazel などのビルドシステムでは、各ターゲットに可視性を指定できます。これは、
どの他のターゲットが依存できるかを決定するプロパティです。プライベート ターゲット
は、独自の BUILD ファイル内でのみ参照できます。ターゲットは、明示的に定義された BUILD ファイルのリストのターゲット、または公開可視性の場合はワークスペース内のすべてのターゲットに対して、より広範な
可視性を付与できます。
ほとんどのプログラミング言語と同様に、通常は可視性をできる限り最小限に抑えることをおすすめします。
一般に、Google のチームは、ターゲットが Google のどのチームでも利用できる広く使用されているライブラリを表す場合にのみ、ターゲットを公開します。
コードを使用する前に他のユーザーとの調整が必要なチームは、ターゲットの可視性として顧客ターゲットの許可リストを
維持します。各
チームの内部実装ターゲットは、チームが所有するディレクトリ
のみに制限されます。ほとんどの BUILD ファイルには、プライベートでないターゲットが 1 つだけ含まれます
。
依存関係の管理
モジュールは相互に参照できる必要があります。コードベースを細かい設定が可能なモジュールに分割するデメリットは、これらのモジュール間の依存関係を管理する必要があることです(ツールで自動化できます)。通常、これらの
依存関係を表すことが、BUILDファイルの内容の大部分を占めます。
内部依存関係
細かい設定が可能なモジュールに分割された大規模なプロジェクトでは、ほとんどの依存関係は 内部依存関係です。つまり、同じ ソースリポジトリで定義およびビルドされた別のターゲットに依存します。内部依存関係は、ビルドの実行中にプリビルドされたアーティファクトとしてダウンロードされるのではなく、ソースからビルドされる点が外部依存関係と異なります。また、内部依存関係には「バージョン」の概念がありません。ターゲットとそのすべての内部依存関係は、リポジトリ内の同じコミット/リビジョンで常にビルドされます。内部依存関係に関して慎重に処理する必要がある問題の 1 つは、 推移的依存関係の処理方法です(図 1)。ターゲット A がターゲット B に依存し、ターゲット B が共通ライブラリ ターゲット C に依存しているとします。ターゲット A はターゲット C で定義されたクラス を使用できますか?
図 1. 推移的依存関係
基盤となるツールに関する限り、これに問題はありません。 B と C の両方がビルド時にターゲット A にリンクされるため、 C で定義されたシンボルは A に認識されます。Bazel は長年にわたってこれを許可していましたが、Google の成長に伴い、問題が発生し始めました。B がリファクタリングされ、C に依存する必要がなくなったとします。B の C への依存関係が削除されると、A と B への依存関係を介して C を使用する他の ターゲットが破損します。事実上、ターゲットの 依存関係は公開コントラクトの一部となり、安全に 変更することはできません。つまり、依存関係が時間の経過とともに蓄積され、Google でのビルドが遅くなり始めました。
Google は最終的に、Bazel に「厳密な推移的 依存関係モード」を導入することでこの問題を解決しました。このモードでは、Bazel は、ターゲットが直接依存せずにシンボルを 参照しようとしているかどうかを検出し、参照しようとしている場合は、エラーと、依存関係を自動的に挿入するために使用できるシェルコマンドで 失敗します。この変更を Google のコードベース全体にロールアウトし、 数百万のビルドターゲットのそれぞれをリファクタリングして依存関係を明示的にリストすることは、 数年がかりの取り組みでしたが、それだけの価値はありました。ターゲットに不要な依存関係が少なくなったため、ビルドが 大幅に高速化されました。エンジニアは、依存するターゲットを破損させることを心配することなく、不要な依存関係を 削除できます。
通常どおり、厳密な推移的依存関係の適用にはトレードオフが伴いました。頻繁に使用されるライブラリは、偶発的にプルされるのではなく、多くの場所で明示的にリストする必要があるため、ビルドファイルが冗長になりました。エンジニアは、BUILDファイルに依存関係を追加するために多くの労力を費やす必要がありました。その後、多くの欠落している
依存関係を自動的に検出し、デベロッパーの
介入なしにBUILDファイルに追加するツールを
開発しました。このようなツールがなくても、コードベースが拡大するにつれて、トレードオフは価値があることがわかりました。依存関係をBUILD ファイルに明示的に追加することは 1 回限りのコストですが、暗黙的な推移的依存関係を処理すると、ビルドターゲットが存在する限り、継続的な問題が発生する可能性があります。Bazel
は、厳密な推移的依存関係
をデフォルトで Java コードに適用します。
外部依存関係
依存関係が内部依存関係でない場合は、外部依存関係である必要があります。外部依存関係とは、 ビルドシステムの外部でビルドおよび保存されるアーティファクトへの依存関係です。依存関係は、アーティファクトリポジトリ(通常はインターネット経由でアクセス)から直接インポートされ、ソースからビルドされるのではなく、そのまま使用されます。外部依存関係と内部依存関係の最大の違いの 1 つは、外部依存関係にはバージョンがあり、そのバージョンはプロジェクトのソースコードとは独立して存在することです。
依存関係の手動管理と自動管理
ビルドシステムでは、外部依存関係のバージョンを
手動または自動で管理できます。手動で管理する場合、ビルドファイルは、アーティファクトリポジトリからダウンロードするバージョンを明示的にリストします。多くの場合、セマンティックバージョン文字列などの1.1.4を使用します。自動的に管理する場合、ソースファイルは許容されるバージョンの範囲を
指定し、ビルドシステムは常に最新バージョンをダウンロードします。たとえば、Gradle
では、依存関係のバージョンを「1.+」として宣言して、メジャー バージョンが 1 である限り、依存関係のマイナー バージョンまたはパッチ
バージョンが許容されることを指定できます。
自動的に管理される依存関係は、小規模なプロジェクトには便利ですが、 規模が大きく、複数のエンジニアが作業しているプロジェクトでは、通常は問題が発生します。自動的に 管理される依存関係の問題は、バージョンがいつ 更新されるかを制御できないことです。外部のユーザーが破壊的な 更新を行わないことを保証する方法はありません(セマンティックバージョニングを使用すると主張している場合でも)。そのため、ある日に機能していたビルドが、次の日に破損し、何が変更されたかを簡単に検出したり、動作状態にロールバックしたりすることができない可能性があります。ビルドが破損しなくても、追跡できない微妙な動作やパフォーマンスの変化が生じる可能性があります。
一方、手動で管理される依存関係では、ソース 管理の変更が必要になるため、簡単に検出してロールバックできます。また、リポジトリの古いバージョンをチェックアウトして、古い依存関係でビルドすることも可能です。 Bazel では、すべての依存関係のバージョンを手動で指定する必要があります。中規模でも 、手動バージョン管理のオーバーヘッドは、提供される安定性のために 価値があります。
1 つのバージョンルール
通常、ライブラリのバージョンごとに異なるアーティファクトが使用されます。 理論的には、同じ外部依存関係の異なるバージョンを、ビルドシステムで異なる名前で宣言できない理由はありません。 これにより、各ターゲットは使用する依存関係のバージョンを 選択できます。実際には多くの問題が発生するため、Google ではコードベース内のすべてのサードパーティ依存関係に厳格な 1 つのバージョンルール を適用しています。
複数のバージョンを許可する場合の最大の問題は、ダイヤモンド依存関係 の問題です。ターゲット A がターゲット B と外部ライブラリの v1 に依存しているとします。ターゲット B がリファクタリングされ、同じ 外部ライブラリの v2 に依存関係が追加されると、ターゲット A は破損します。これは、同じライブラリの 2 つの異なるバージョンに暗黙的に依存するようになったためです。事実上、ターゲットから複数のバージョンを持つサードパーティライブラリに新しい依存関係を追加することは安全ではありません。これは、そのターゲットのユーザーがすでに別のバージョンに依存している可能性があるためです。1 つのバージョンルールに従うと、この競合は発生しません。ターゲットがサードパーティライブラリへの依存関係を追加すると、既存の依存関係はすでに同じバージョンになっているため、共存できます。
推移的な外部依存関係
外部依存関係の推移的依存関係の処理は 特に困難です。Maven Central などの多くのアーティファクトリポジトリでは、アーティファクトがリポジトリ内の他のアーティファクトの特定のバージョンに依存関係を指定できます。Maven や Gradle などのビルドツールは、デフォルトで各 推移的依存関係を再帰的にダウンロードします。つまり、プロジェクトに 1 つの依存関係を追加すると、合計で数十のアーティファクトがダウンロードされる可能性があります。
これは非常に便利です。新しいライブラリへの依存関係を追加する場合、そのライブラリの推移的依存関係をすべて追跡して手動で追加するのは 非常に面倒です 。ただし、大きなデメリットもあります。ライブラリごとに同じサードパーティ ライブラリの異なるバージョンに依存する可能性があるため、この 戦略では 1 つのバージョンルールに違反し、ダイヤモンド 依存関係の問題が発生します。ターゲットが、同じ依存関係の異なるバージョンを使用する 2 つの外部ライブラリに依存している場合、どちらのバージョンを取得するかはわかりません。また、新しいバージョンが依存関係の競合するバージョンをプルし始めると、外部依存関係を更新すると、コードベース全体で無関係に見える 障害が発生する可能性があります。
このため、Bazel は推移的依存関係を自動的にダウンロードしません。
残念ながら、万能薬はありません。Bazel の代替手段は、リポジトリのすべての外部依存関係と、リポジトリ全体でその依存関係に使用される明示的なバージョンをリストする
グローバルファイルが必要です。幸いなことに、Bazel には、Maven
アーティファクトのセットの推移的依存関係を含むこのようなファイルを自動的に
生成できるツールが用意されています。このツールを 1 回実行して、プロジェクトの初期 WORKSPACE ファイル
を生成し、そのファイルを後で手動で更新して、各依存関係のバージョン
を調整できます。
ここでも、選択は利便性とスケーラビリティのどちらかになります。小規模な プロジェクトでは、推移的依存関係 の管理を心配する必要がなく、自動推移的 依存関係を使用できる場合があります。組織 とコードベースが拡大するにつれて、この戦略の魅力は低下し、競合や予期しない結果がますます 頻繁に発生するようになります。大規模な場合、依存関係を手動で管理するコストは、依存関係の自動管理によって発生する問題に対処するコストよりもはるかに 低くなります。
外部依存関係を使用したビルド結果のキャッシュ保存
外部依存関係は、ライブラリの安定版をリリースするサードパーティによって提供されることが多く、ソースコードが提供されないこともあります。組織によっては、独自のコードの一部をアーティファクトとして 公開し、他のコードが内部依存関係ではなくサードパーティ依存関係として 依存できるようにすることもあります。理論的には、アーティファクト のビルドに時間がかかり、ダウンロードに時間がかからない場合は、ビルドを高速化できます。
ただし、これには多くのオーバーヘッドと複雑さが伴います。これらのアーティファクトのビルドとアーティファクト リポジトリへのアップロードを担当するユーザーが必要であり、クライアントは最新バージョンに常に更新されていることを確認する必要があります。また、システムのさまざまな部分がリポジトリのさまざまなポイントからビルドされ、ソースツリーの一貫したビューがなくなるため、デバッグがはるかに難しくなります。
アーティファクトのビルドに時間がかかる問題を解決するより良い方法は、 前述のように、リモートキャッシュをサポートするビルドシステムを使用することです。このような ビルドシステムは、すべてのビルドの結果のアーティファクトをエンジニア間で共有される場所 に保存します。そのため、デベロッパーが他のユーザーによって最近ビルドされたアーティファクト に依存している場合、ビルドシステムはビルドするのではなく自動的にダウンロード します。これにより、アーティファクトに直接依存することのパフォーマンス上のメリットをすべて享受しながら、常に同じソースからビルドされた場合と同じようにビルドの一貫性を確保できます。これは Google が内部で使用している 戦略であり、Bazel はリモート キャッシュを使用するように構成できます。
外部依存関係のセキュリティと信頼性
サードパーティ ソースのアーティファクトに依存することは、本質的にリスクがあります。サードパーティソース(アーティファクトリポジトリなど)が
ダウンすると、外部依存関係をダウンロードできなくなるため、ビルド全体が停止する可能性があります。セキュリティリスクもあります。サードパーティシステム
が攻撃者によって侵害された場合、攻撃者は参照される
アーティファクトを独自の設計のものに置き換えることで、ビルドに任意のコード
を挿入できます。どちらの問題も、依存するアーティファクトを制御するサーバーにミラーリングし、ビルドシステムが
Maven Central などのサードパーティ アーティファクト
リポジトリにアクセスできないようにすることで軽減できます。トレードオフとして、
これらのミラーの維持には労力とリソースが必要になるため、使用するかどうかは
プロジェクトの規模によって異なります。また、各サードパーティアーティファクトのハッシュをソースリポジトリで指定することで、セキュリティの問題を完全に防ぐことができます。アーティファクトが改ざんされた場合、ビルドは失敗します。問題を完全に
回避するもう 1 つの方法は、プロジェクトの依存関係をベンダリングすることです。プロジェクトが
依存関係をベンダリングすると、ソースまたはバイナリとして、
プロジェクトのソースコードとともにソース管理にチェックインされます。つまり、プロジェクトのすべての外部依存関係が内部依存関係に変換されます。Google ではこのアプローチを内部で使用しており、Google 全体で参照されるすべてのサードパーティ
ライブラリを Google のソースツリーのルート
にある third_party ディレクトリにチェックインしています。ただし、これは Google の
ソース管理システムが非常に大規模なモノレポを処理するようにカスタムビルドされている場合にのみ機能するため、
すべての組織でベンダリングがオプションになるとは限りません。
