現代のソフトウェア開発における課題の 1 つは、ソフトウェアを頻繁に自信を持ってリリースできることです。 これは、ソフトウェアをテストし、人間の介入を最小限に抑えてリリースできる優れたCI/CDセットアップが整っている場合にのみ実現できます。 しかし、最新のソフトウェアアプリケーションは、さまざまなサードパーティの依存関係も使用しており、多くの場合、複数のオペレーティングシステムとアーキテクチャで実行する必要があります。
この投稿では、 Bazel と Testcontainers の組み合わせが、 密閉型ビルド システムを提供することで、開発者がソフトウェアをビルドしてリリースするのにどのように役立つかを説明します。
Bazel コンテナと Testcontainers を一緒に使用する
Bazel は、多言語、マルチプラットフォーム プロジェクトをビルドおよびテストするために Google によって開発されたオープンソースのビルド ツールです。 いくつかの大手IT企業は、次のようなさまざまな理由でモノレポを採用しています。
- コードの共有と再利用性
- プロジェクト間のリファクタリング
- 一貫性のあるビルドと依存関係の管理
- バージョン管理とリリース管理
多言語サポートと再現可能なビルドに重点を置いた Bazel は、このようなモノレポの構築に優れています。
Bazel の重要な概念は 密閉性であり、これは、すべての入力が宣言されたときに、ビルドシステムが出力を再構築する必要があるタイミングを知ることができることを意味します。 このアプローチは、同じ入力ソース コードと製品構成が与えられた場合、ビルドをホスト システムへの変更から分離することで、常に同じ出力を返すという決定論をもたらします。
Testcontainers は、開発およびテストのユース ケース用に使い捨てのオンデマンド コンテナーをプロビジョニングするためのオープン ソース フレームワークです。 テストコンテナを使用すると、データベース、メッセージブローカー、Webブラウザ、またはDockerコンテナで実行できるほぼすべてのものを簡単に操作できます。
Bazel と Testcontainers を一緒に使用すると、次の機能が提供されます。
- Bazel は、C、C++、Java、Go、Python、Node.js などのさまざまなプログラミング言語を使用してプロジェクトをビルドできます。
- Bazel は、分離されたビルド / テスト環境を目的の言語バージョンで動的にプロビジョニングできます。
- Testcontainerは、必要な依存関係をDockerコンテナとしてプロビジョニングできるため、テストスイートは自己完結型になります。 データベースやメッセージブローカーなど、必要なサービスを手動で事前プロビジョニングする必要はありません。
- すべてのテストの依存関係は、Testcontainers API を使用してコードで表現でき、テスト間でそのようなリソースを共有することで、気密性を損なうリスクを回避できます。
Bazel と Testcontainers を使用して、さまざまな言語を使用するモジュールでモノレポをビルドおよびテストする方法を見てみましょう。
Javaproducts
を使用するモジュールとGoを使用するモジュールを含むcustomers
モノレポを探索します。どちらのモジュールもリレーショナルデータベース(PostgreSQL)と対話し、テストにTestcontainersを使用します。
Bazel を使ってみる
まず、Bazel の基本概念について理解していきましょう。Bazel をインストールする最善の方法は、 Bazelisk を使用することです。 公式のインストール手順に従って、Bazeliskをインストールします。インストールが完了すると、Bazelisk version コマンドと Bazel version コマンドを実行できるようになります。
$ brew install bazelisk
$ bazel version
Bazelisk version: v1.12.0
Build label: 7.0.0
Bazel を使用してプロジェクトをビルドする前に、そのワークスペースを設定する必要があります。
ワークスペースは、プロジェクトのソースファイルを保持するディレクトリであり、次のファイルが含まれています。
- この
WORKSPACE.bazel
ファイルは、ディレクトリとその内容を Bazel ワークスペースとして識別し、プロジェクトのディレクトリ構造のルートにあります。 MODULE.bazel
Bazel プラグインへの依存関係を宣言するファイル(「ルールセット」と呼ばれます)。- 1 つ以上
BUILD
(またはBUILD.bazel
) ファイルには、プロジェクトのさまざまな部分のソースと依存関係が記述されています。 ファイルを含むBUILD
ワークスペース内のディレクトリはパッケージです。
最も単純なケースでは、 MODULE.bazel
ファイルは空のファイルであり、ファイル BUILD
には次のように 1 つ以上の汎用ターゲットを含めることができます。
genrule(
name = "foo",
outs = ["foo.txt"],
cmd_bash = "sleep 2 && echo 'Hello World' >$@",
)
genrule(
name = "bar",
outs = ["bar.txt"],
cmd_bash = "sleep 2 && echo 'Bye bye' >$@",
)
ここでは、と の 2 つのターゲットがあります。 foo
bar
これで、次のように Bazel を使用してこれらのターゲットをビルドできます。
$ bazel build //:foo <- runs only foo target, // indicates root workspace
$ bazel build //:bar <- runs only bar target
$ bazel build //... <- runs all targets
モノレポでの Bazel ビルドの構成
ここでは、testcontainers-bazel-demo リポジトリで Bazel の使用について説明します。このリポジトリは、Javaを使用したモジュールとproducts
Goを使用したモジュールを含むcustomers
モノレポです。その構造は次のようになります。
testcontainers-bazel-demo
|____customers
| |____BUILD.bazel
| |____src
|____products
| |____go.mod
| |____go.sum
| |____repo.go
| |____repo_test.go
| |____BUILD.bazel
|____MODULE.bazel
Bazel では、プロジェクトの種類ごとに異なるルールを使用してビルドします。 Bazel
は、Java パッケージの構築、Go パッケージの構築、 rules_go
rules_python
Python パッケージの構築などに使用します rules_java 。
また、追加機能を提供する追加のルールを読み込む必要がある場合もあります。 Javaパッケージをビルドするには、外部のMaven依存関係を使用し、テストの実行にJUnit 5 を使用する場合があります。 その場合、Mavenの依存関係を使用できるようにロード rules_jvm_external
する必要があります。
新しい外部依存関係サブシステムである Bzlmod を使用して、外部依存関係をロードします。 MODULE.bazel
ファイルでは、次のように追加のrules_jvm_external
ものをcontrib_rules_jvm
ロードできます。
bazel_dep(name = "contrib_rules_jvm", version = "0.21.4")
bazel_dep(name = "rules_jvm_external", version = "5.3")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
name = "maven",
artifacts = [
"org.postgresql:postgresql:42.6.0",
"ch.qos.logback:logback-classic:1.4.6",
"org.testcontainers:postgresql:1.19.3",
"org.junit.platform:junit-platform-launcher:1.10.1",
"org.junit.platform:junit-platform-reporting:1.10.1",
"org.junit.jupiter:junit-jupiter-api:5.10.1",
"org.junit.jupiter:junit-jupiter-params:5.10.1",
"org.junit.jupiter:junit-jupiter-engine:5.10.1",
],
)
use_repo(maven, "maven")
ファイル内の MODULE.bazel
上記の構成を理解しましょう。
- Bazel Central レジストリ から ルールを
rules_jvm_external
ロードし、サードパーティの Maven 依存関係を使用するための拡張機能をロードしました。 - アーティファクト構成で
maven.install
Maven座標を使用して、すべてのJavaアプリケーションの依存関係を構成しました。 - JUnit 5テストの実行をサポートするルールをスイートとしてロード
contrib_rules_jvm
しています。
これで、プログラムを実行して @maven//:pin
、推移的な依存関係のJSONロックファイルを、後で使用できる形式で rules_jvm_external
作成できます。
bazel run @maven//:pin
生成されたファイルrules_jvm_external~4.5~maven~maven_install.json
maven_install.json
の名前を に変更します。次に、依存関係を固定したことを反映して、を更新し MODULE.bazel
ます。
lock_file
属性maven.install()
を そして、 use_repo
依存関係の更新に使用されるリポジトリも公開 unpinned_maven
するように呼び出しを更新します。
maven.install(
...
lock_file = "//:maven_install.json",
)
use_repo(maven, "maven", "unpinned_maven")
これで、依存関係を更新するときに、次のコマンドを実行してロックファイルを更新できます。
bazel run @unpinned_maven//:pin
ファイルで customers/BUILD.bazel
ビルドターゲットを次のように構成しましょう。
load(
"@bazel_tools//tools/jdk:default_java_toolchain.bzl",
"default_java_toolchain", "DEFAULT_TOOLCHAIN_CONFIGURATION", "BASE_JDK9_JVM_OPTS", "DEFAULT_JAVACOPTS"
)
default_java_toolchain(
name = "repository_default_toolchain",
configuration = DEFAULT_TOOLCHAIN_CONFIGURATION,
java_runtime = "@bazel_tools//tools/jdk:remotejdk_17",
jvm_opts = BASE_JDK9_JVM_OPTS + ["--enable-preview"],
javacopts = DEFAULT_JAVACOPTS + ["--enable-preview"],
source_version = "17",
target_version = "17",
)
load("@rules_jvm_external//:defs.bzl", "artifact")
load("@contrib_rules_jvm//java:defs.bzl", "JUNIT5_DEPS", "java_test_suite")
java_library(
name = "customers-lib",
srcs = glob(["src/main/java/**/*.java"]),
deps = [
artifact("org.postgresql:postgresql"),
artifact("ch.qos.logback:logback-classic"),
],
)
java_library(
name = "customers-test-resources",
resources = glob(["src/test/resources/**/*"]),
)
java_test_suite(
name = "customers-lib-tests",
srcs = glob(["src/test/java/**/*.java"]),
runner = "junit5",
test_suffixes = [
"Test.java",
"Tests.java",
],
runtime_deps = JUNIT5_DEPS,
deps = [
":customers-lib",
":customers-test-resources",
artifact("org.junit.jupiter:junit-jupiter-api"),
artifact("org.junit.jupiter:junit-jupiter-params"),
artifact("org.testcontainers:postgresql"),
],
)
この BUILD
構成を理解しましょう。
- Javaバージョンをロード
default_java_toolchain
して構成しました 17. - 本番jarファイルをビルドする名前
customers-lib
のターゲットを構成しjava_library
ました。 - すべてのテストを実行するテストスイートを定義する名前
customers-lib-tests
を持つターゲットを定義しjava_test_suite
ました。また、他のターゲットcustomers-lib
と外部の依存関係への依存関係も構成しました。 - また、Java以外のソース(ログ設定ファイルなど)を依存関係としてテストスイートターゲットに追加するために、別の
customers-test-resources
ターゲットを名前で定義しました。
customers
パッケージには、顧客の詳細を PostgreSQL データベースに格納および取得するクラスがありますCustomerService
。そして、 を使用してメソッドTestcontainers
をテストしCustomerService
ますCustomerServiceTest
。完全なコードについては、 GitHub リポジトリ を参照してください。
手記: Bazel ビルド ファイル ジェネレータである Gazelle を使用して、手動でファイルを書き込む代わりにファイルを生成 BUILD.bazel
できます。
Testcontainers テストの実行
Testcontainers テストを実行するには、Testcontainers がサポートするコンテナー ランタイムが必要です。 Docker Desktop を使用してローカルの Docker がインストールされているとします。
これで、Bazel ビルド構成で、パッケージを customers
ビルドしてテストする準備が整いました。
# to run all build targets of customers package
$ bazel build //customers/...
# to run a specific build target of customers package
$ bazel build //customers:customers-lib
# to run all test targets of customers package
$ bazel test //customers/...
# to run a specific test target of customers package
$ bazel test //customers:customers-lib-tests
ビルドを初めて実行するときは、必要な依存関係をダウンロードしてからターゲットを実行するのに時間がかかります。 ただし、コードや構成を変更せずにビルドまたはテストを再試行すると、Bazel はビルドまたはテストを再実行せず、キャッシュされた結果を表示します。 Bazel には、コードの変更を検出し、実行に必要なターゲットのみを実行する強力なキャッシュ メカニズムがあります。
Testcontainers を使用する際には、Docker イメージ名とタグ (Postgres:16など) を使用して、必要な依存関係をコードの一部として定義します。 そのため、コード(Docker イメージ名やタグなど)を変更しない限り、Bazel はテスト結果をキャッシュします。
同様に、Go パッケージ用に Bazel ビルドを構成するために Gazelle を使用できます rules_go
。 Go パッケージでの Bazel の構成の詳細については、 MODULE.bazel
と products/BUILD.bazel
ファイルを参照してください。
前述したように、Testcontainers テストを実行するには、Testcontainers でサポートされているコンテナー ランタイムが必要です。 複雑な CI プラットフォームに Docker をインストールするのは困難な場合があり、複雑な Docker-in-Docker セットアップを使用する必要がある場合があります。 さらに、一部のDockerイメージはオペレーティングシステムアーキテクチャと互換性がない場合があります(例:Apple M1)。
Testcontainers Cloud は、localhostまたはCIランナーにDockerを配置し、クラウドVM上でコンテナを透過的に実行する必要性を排除することで、これらの問題を解決します。
GitHub Actions を使用して Testcontainers Cloud で Bazel を使用して Testcontainers テストを実行する例を次に示します。
name: CI
on:
push:
branches:
- '**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure TestContainers cloud
uses: atomicjar/testcontainers-cloud-setup-action@main
with:
wait: true
token: ${{ secrets.TC_CLOUD_TOKEN }}
- name: Cache Bazel
uses: actions/cache@v3
with:
path: |
~/.cache/bazel
key: ${{ runner.os }}-bazel-${{ hashFiles('.bazelversion', '.bazelrc', 'WORKSPACE', 'WORKSPACE.bazel', 'MODULE.bazel') }}
restore-keys: |
${{ runner.os }}-bazel-
- name: Build and Test
run: bazel test --test_output=all //...
GitHub Actions ランナーにはすでに Bazelisk がインストールされているため、すぐに Bazel を使用できます。 Secretsを使用して環境変数を設定し TC_CLOUD_TOKEN
、Testcontainers Cloudエージェントを開始しました。 ビルドログを確認すると、Testcontainers Cloudを使用してテストが実行されていることがわかります。
概要
Bazel ビルドシステムを使用して、異なるプログラミング言語を使用する複数のモジュールでモノレポをビルドおよびテストする方法を示しました。 Testcontainers と組み合わせることで、ビルドを自己完結型で密閉型にすることができます。
Bazel と Testcontainers は自己完結型のビルドに役立ちますが、密閉型ビルドにするには追加の対策を講じる必要があります。
- Bazel は、JDK 17や Go 1など、特定のバージョンの SDK を使用するように設定できます。20など、ビルドがホストマシンにインストールされているバージョンではなく、常に同じバージョンを使用するようにします。
- Testcontainers テストでは、コンテナーの依存関係に Docker タグ latest を使用すると、非決定論的な動作が発生する可能性があります。 また、一部の Docker イメージ発行元は、同じタグを使用して既存のイメージをオーバーライドします。 ビルド/テストを決定論的にするには、常に Docker イメージ ダイジェストを使用して、ビルドとテストで常にまったく同じバージョンのイメージを使用し、再現可能で密閉されたビルドを提供するようにします。
- Testcontainers Cloud を使用して Testcontainers テストを実行すると、Docker セットアップの複雑さが軽減され、決定論的なコンテナー ランタイム環境が提供されます。
詳細については、 Testcontainers の Web サイト にアクセスし、 無料のアカウントを作成して Testcontainers Cloud の使用を開始してください。
さらに詳しく
- Testcontainers の Web サイトにアクセスします。
- 無料アカウントを作成して、Testcontainers Cloud の使用を開始してください。
- Docker デスクトップの最新リリースを入手します。
- 質問がありますか? Docker コミュニティがお手伝いします。
- ドッカーは初めてですか? 始めましょう。