pants でスマートな docker build をしよう!
TL;DR
- pants は docker をネイティブサポートしている monorepotool の1つであり、git と連携して docker イメージの差分ビルドや、docker イメージ同士の依存性(参照順)を考慮した並列ビルドが可能
- pants のビルド設定ファイルから、
docker buildで外部から変数を与えることの可能な--build-argのパラメータや multi-satge build の対象ステージ指定をする--target、イメージのタグ--tagを渡すことができるため、Dockerfile の処理とデータを分離を促進し、柔軟な docker イメージビルドの実現を支援
はじめに
pants は、 ビルド設定・フローを記述に 完全な python を利用可能な docker や go、java、shell、helm などの言語に加え、多くの linter、formatter をサポートするビルドツールです。
特に、pants は monorepo を上手に管理・運用するためのツールとしてよく知られる monorepotool の中でも docker をネイティブサポートする数少ない monorepotool である特徴をもちます。
本記事では、docker イメージ間にビルド依存性のある複数のイメージに対する、pants を利用したスマートな docker build の方法について紹介します。
この記事を読んで欲しい人
- (バックエンド・インフラ視点で)monorepotool の技術調査をしている人
- 自前で Docker ファイル書いて ゴリゴリ
docker buildするよって人 - docker イメージ間で依存性のある
docker buildをスマートにやりたい人
前提知識
本記事では、下記の技術について基本的な知識を前提とします。
- Linux
- bash
- docker
- python
複数 image の docker build は複雑!? (問題共有パート)
まず、pants の導入する前に複数の docker イメージをビルドしなければならない環境で起こる問題について、認識を合わせたいと思います。
本記事では、例として下記のような2つのプロジェクトをもつ monorepo 考えます。それぞれのプロジェクトは docker build によって複数の docker イメージを自炊して運用していることを想定します。
この時点では、ディレクトリの構造のみをざっくり理解していただければ、問題ありません。ファイルの具体的な中身について気になる人は github のリポジトリをのぞいてみて下さい。
.
├── README.md
├── pants.toml
├── .editorconfig
├── .gitignore
├── .pants.bootstrap
├── example-nest-project
│ ├── README.md
│ ├── app
│ │ ├── README.md
│ │ ├── backend
│ │ │ └── README.md
│ │ └── frontend
│ │ └── README.md
│ └── docker
│ ├── README.md
│ ├── backend
│ │ ├── api
│ │ │ ├── BUILD
│ │ │ ├── Dockerfile
│ │ │ └── extensions.json
│ │ └── batch
│ │ ├── BUILD
│ │ ├── Dockerfile
│ │ └── extensions.json
│ ├── common
│ │ ├── mise
│ │ │ ├── BUILD
│ │ │ └── Dockerfile
│ │ └── ubuntu
│ │ ├── BUILD
│ │ └── Dockerfile
│ └── frontend
│ └── vite
│ ├── BUILD
│ ├── Dockerfile
│ └── extensions.json
└── example-rust-project
├── README.md
├── app
│ ├── README.md
│ ├── backend
│ │ └── README.md
│ └── frontend
│ └── README.md
└── docker
├── README.md
├── backend
│ ├── api
│ │ ├── BUILD
│ │ ├── Dockerfile
│ │ └── extensions.json
│ └── batch
│ ├── BUILD
│ ├── Dockerfile
│ └── extensions.json
├── common
│ ├── mise
│ │ ├── BUILD
│ │ └── Dockerfile
│ ├── rust
│ │ ├── BUILD
│ │ └── Dockerfile
│ └── ubuntu
│ ├── BUILD
│ └── Dockerfile
└── frontend
└── vite
├── BUILD
├── Dockerfile
└── extensions.json
-
example-nest-projectとexample-rust-projectの 2つのプロジェクトをもつ - 各プロジェクトはアプリケーションコードを管理する
appディレクトリと、docker イメージを管理するdockerディレクトリをもつ -
dockerディレクトリは、用途に応じてcommon・frontend・backendの3カテゴリに分かれている -
commonでは、各プロジェクトでイメージのベースとなるubuntuイメージ、ubuntuイメージをベースに複数開発言語のバージョンマネージャ mise を導入したmiseイメージ、example-rust-projectではそれらに加え、rust言語のツールチェーンが導入されたrustイメージをもつ -
frontendでは、miseイメージをベースにしてnodeや VSCode 拡張が導入されたフロントエンド向けの開発環境viteイメージをもつ -
backendでは、miseやrustイメージをベースにした、VSCode 拡張などが導入されたバックエンド向けの開発環境api・batchイメージをもつ
上記を整理すると docker イメージのビルド依存関係は次のような図で表されます。
ここで、docker イメージの名前は、開発用を表すdevまたは本番用を表すprodを接頭辞として、カテゴリ(common・frontend・backend)とサービス名(ubuntu・mise・rustなど)をハイフンで繋いで表します。下記の図では、単純のため開発用イメージdevにのみ注目します。

さて、この高々数個の docker image を build すれば良いわけですが、1つずつ依存性を考慮して手動でdocker buildを毎回実行するわけにもいかないので、これらの作業を自動化したいと考えるはずです。したがって、自動化のためには次のような要件が挙げられるかと思います。
- 依存性をチェックしてビルド可能(依存先のイメージがなかったらそのイメージもビルド、上流のイメージに変更が入ったら下流のイメージも再ビルド)
- 並列ビルドできるところは自動で並列ビルド可能(例:
example-nest-projectのdev-common-miseに依存している3つの image は並列ビルド可能) - Docker ファイルやイメージ内に
COPYされるファイルなどビルドに使用するファイルの変更検知してビルド可能 - 特定のタグ(開発用や本番用、特定プロジェクト)でフィルタしてビルド可能
- ビルドで使用するパラメータやイメージタグを外部(
shellや環境変数)から与えることが可能
このような機能要件となると、bashでスクリプトを書いたり、makeでやるには少々荷が重そうだと感じて頂けると思います。
本記事では、上記の要件を満たすビルドツールとして、pants が良い感じだったので、紹介させて頂きたいと思います。
pants のセットアップ
本記事では、pants が CI/CD の用途で利用されることやセットアップ方法を容易にすることを考慮して [1]、Windows や macOS に直接インストールは実施せず、Linux 環境(Ubuntu 24.04)に pants の環境を構築します。
Windows や macOS をお使いの方は、docker がインストールされた Ubuntu 24.04の環境を各自ご用意ください。
docker 環境構築の方法については本記事では解説しませんが、おすすめの方法として Multipass を利用した Ubuntu 仮想環境構築の方法について記事を書いているので、よろしければこちらをご参照下さい。
インストール
Ubuntu 環境における pants のインストール手順について下記に示します。
# 依存パッケージの導入
sudo apt update
sudo apt install ca-certificates curl unzip python3-dev build-essential
# NOTE: build-essential に gcc が含まれています。
# NOTE: python3-distutils は Ubuntu 24.04 にデフォルトで入っている python 3.12 から distutils が廃止になったので不要です。
snap info go
# NOTE: 新しいバージョンがあれば 1.22 の部分を書き換えて下さい
# pants の docker build が go への依存性があるらしく?入れておく
snap install go --classic --channel=1.22/stable
# scie-pants による pants のインストール
curl --proto '=https' --tlsv1.2 -sSfL 'https://static.pantsbuild.org/setup/get-pants.sh' | bash
# バージョンを指定して pnats をインストールする場合
# curl --proto '=https' --tlsv1.2 -sSfL 'https://static.pantsbuild.org/setup/get-pants.sh' | bash -s -- -V 0.12.0
# パスを通す
echo 'export PATH=~/.local/bin:$PATH' >> /home/<ユーザー名>/.bashrc
source ~/.bashrc
pants のバージョンは github の scie-pants リポジトリのリリースタグから確認できます。
以上の導入が完了したら、サンプルの monorepo であるリポジトリを clone して、リポジトリルートでpants --versionを実行しましょう。
pants 設定ファイル
pants.tomlを pants を実行するリポジトリルートに下記のような内容で配置します。
-
pants_versionにはpants --versionコマンドで確認した値 -
backend_packagesには、pants で使用したい機能について記載します。今回はdocker_buildをしたいのでpants.backend.dockerを追加 -
interpreter_constraintsには Ubuntu のシステムにインストールされている python のバージョンを記載 -
root_patternsの/はリポジトリルートを表し、そのディレクトリをパスの起点に設定 -
[docker]ブロックについては後ほど説明するので、一旦スルー
詳細は下記のリンクを参照して下さい。
BUILD ファイル
pantsを実行するための下準備ができたので、pants におけるビルド設定を記述するためのBUILDファイルについて作成していきます。ここでは、example-nest-projectにおける例に挙げてまずルートとなるイメージubuntuをビルドする方法について解説します。その後、依存性のあるイメージのビルドについて解説します。
ルートとなるイメージのビルド
まず、example-nest-projectにおけるビルドイメージのルートとなる ubuntu イメージについて Dockerファイルを要約すると次のことを実施します。
- ベースイメージは 公式の Ubuntu イメージ
- 変数
UBUNTU_VERSION、USER_NAME、USER_PASSWORDはdocker build --build-argで渡されることを想定 - multii-stage build として、ステージ
base、dev、prodをもつ -
aptパッケージはセキュリティに影響するものといくつかの基本的なパッケージを導入 -
sudo権限をもつ 一般ユーザーを作成
docker buildを利用した場合、dev-common-ubuntuは下記のコマンドで実行可能です。
# git リポジトリ内で実施
GIT_CURRENT_BRANCH="$(git branch --show-current)"
GIT_REPOSITORY_ROOT="$(git rev-parse --show-superproject-working-tree --show-toplevel | head -n 1)"
docker build -f ${GIT_REPOSITORY_ROOT}/example-nest-project/docker/common/ubuntu/Dockerfile \
--target dev \
-t dev-common-ubuntu:${GIT_CURRENT_BRANCH} \
--build-arg USER_NAME=numa \
--build-arg USER_PASSWORD=kotatu \
--build-arg UBUNTU_VERSION=24.04 \
${GIT_REPOSITORY_ROOT}
上記に相当するビルドを pants で実施するにはBUILDファイルにdocker_image関数を記述する必要があります。
dev-common-ubuntuとprod-common-ubuntu イメージをビルドするための、BUILDファイルは次のように表されます。
BUILDファイルのdocker_build関数について簡単に説明します。
-
name: タスク名とビルドイメージ名を指定 -
description: はタスクの説明を記載 -
tags: ビルド時にこのリストに記載のタグでフィルタしてビルド可能 -
source: ビルドに使用するDockerfileを指定 -
image_tags: イメージタグを設定 -
target_stage: multi-stage build の target を指定 -
dependencies: 依存性のあるファイルやタスクを指定 -
extra_build_args:--build-argの変数と値を与える
file関数は、ビルドに必要な依存性のあるファイルを定義し、ファイル変更検知によるビルドのトリガ対象にしたい場合に使用します。上記の設定では buildをBUILDファイルとして設定し、dependenciesで:buildとして参照しています。
また、extra_build_argsに定義されていないUSER_NAMEはどこから与えているか説明すると、リポジトリルートのpants.tomlのbuild_argsと.pants.bootstrapを利用しています。
pants.tomlのbuild_argはBUILDファイルのextra_build_argsと異なり、pants における docker build --build-argの共通の値として利用することができます。それらの変数の値は、.pants.bootstrapによってタスクの実行時に決定されます。
以上で、example-nest-projectにおけるubuntuイメージを pants でビルドする準備が整ったので、pants packageコマンドを利用してビルドします。
# リポジトリルートで実施
pants package example-nest-project/docker/common/ubuntu:dev-common-ubuntu
依存性のあるイメージのビルド
ルートとなるdev-common-ubuntuをビルドすることができたので、dev-common-ubuntuに依存性をもつイメージとして、miseイメージを例に挙げて、別のイメージに依存性のあるイメージのビルドについて、解説したいと思います。
miseイメージの Dockerファイルを要約すると次のことを実施します。
- ベースイメージは
${TARGET}-common-ubuntu:${GIT_CURRENT_BRANCH} - 変数
TARGET、GIT_CURRENT_BRANCH、USER_NAMEはdocker build --build-argで渡されることを想定 - multii-stage build として、ステージ
base、dev、prodをもつ -
aptパッケージはセキュリティに影響するパッケージを導入 - 複数言語バージョンマネージャ
miseを導入
docker buildを利用した場合、dev-common-miseは下記のコマンドでビルド可能です。
# リポジトリルートで実施
TARGET='dev'
GIT_CURRENT_BRANCH="$(git branch --show-current)"
GIT_REPOSITORY_ROOT="$(git rev-parse --show-superproject-working-tree --show-toplevel | head -n 1)"
docker build -f ${GIT_REPOSITORY_ROOT}/example-nest-project/docker/common/mise/Dockerfile \
--target dev \
-t dev-common-mise:${GIT_CURRENT_BRANCH} \
--build-arg TARGET=${TARGET} \
--build-arg GIT_CURRENT_BRANCH=${GIT_CURRENT_BRANCH} \
--build-arg USER_NAME=numa \
${GIT_REPOSITORY_ROOT}
上記コマンドに対応するBUILDファイルは下記で表されます。
ubuntuイメージとの違いとして、dependenciesにdev-common-ubuntuへの依存性が記載されています。
別のイメージへの依存性を記載しておくことで、ubuntuのイメージが無い場合はビルドを実施してから、miseイメージをビルドしてくれるようになります。
# リポジトリルートで実施
pants package example-nest-project/docker/common/mise:dev-common-mise
同様にして、他のdev-common-rust、dev-frontend-vite、dev-backend-api、dev-backend-batchについてもBUILDファイル作成することで、pants でリポジトリ全体の docker build を管理可能になります。
ここまで、docker build に関するBUILDファイルの基本的な設定を扱いましたが、ここで紹介できなかったdocker_image関数のオプションは公式ページを確認してみて下さい。
ビルドコマンド
リポジトリの docker buildを、pants で一元的に管理できるようになったので、対象を絞ってビルドできるように--tagオプションを利用します。
--tagオプションは--tag='dev-image,prod-image'とした場合にはORの条件、--tag='example-nest-project' --tag='dev-image'のようにした場合はANDの条件でフィルタを実施します。
また、--changed-sinceによるgitの差分検知を利用した差分ビルドについても紹介します。
タグでフィルタしてビルド
example-nest-project
-
example-nest-projectかつdev-imageのタグをもつイメージをビルド
pants --tag='example-nest-project' --tag='dev-image' package ::
# dev-common-ubuntu
# ↓
# dev-common-mise
# ↓ 並列ビルド
# dev-frontend-vite・dev-backend-api・dev-backend-batch
-
example-nest-projectかつdev-imageのタグをもつイメージをビルド
pants --tag='example-nest-project' --tag='dev-common-image' package ::
# dev-common-ubuntu
# ↓
# dev-common-mise
-
example-nest-projectかつdev-front-imageのタグをもつイメージをビルド
pants --tag='example-nest-project' --tag='dev-frontend-image' package ::
# dev-common-ubuntu
# ↓
# dev-common-mise
# ↓
# dev-frontend-vite
-
example-nest-projectかつdev-backend-imageのタグをもつイメージをビルド
pants --tag='example-nest-project' --tag='dev-backend-image' package ::
# dev-common-ubuntu
# ↓
# dev-common-mise
# ↓ 並列ビルド
# dev-backend-api・dev-backend-batch
example-rust-project
-
example-rust-projectかつdev-imageのタグをもつイメージをビルド
pants --tag='example-rust-project' --tag='dev-image' package ::
# dev-common-ubuntu
# ↓ 並列ビルド
# dev-common-mise・dev-common-rust
# ↓ 並列ビルド
# dev-frontend-vite・dev-backend-api・dev-backend-batch
-
example-rust-projectかつdev-imageのタグをもつイメージをビルド
pants --tag='example-rust-project' --tag='dev-common-image' package ::
# dev-common-ubuntu
# ↓ 並列ビルド
# dev-common-mise・dev-common-rust
-
example-rust-projectかつdev-frontend-imageのタグをもつイメージをビルド
pants --tag='example-rust-project' --tag='dev-frontend-image' package ::
# dev-common-ubuntu
# ↓
# dev-common-mise
# ↓
# dev-frontend-vite
-
example-rust-projectかつdev-backend-imageのタグをもつイメージをビルド
pants --tag='example-rust-project' --tag='dev-backend-image' package ::
# dev-common-ubuntu
# ↓
# dev-common-rust
# ↓ 並列ビルド
# dev-backend-api・dev-backend-batch
gitで変更検知をした対象をビルド
-
example-nest-projectかつdev-imageのタグをもち、依存するファイルに最新コミットから差分があった場合に依存関係のあるイメージをすべてビルド
pants --changed-since='HEAD' --changed-dependents='transitive' --tag='example-nest-project' --tag='dev-image' package
# mise の Dockerfile に変更を加えた場合
# dev-common-mise
# ↓ 並列ビルド
# dev-frontend-vite・dev-backend-api・dev-backend-batch
-
example-rust-projectかつdev-imageのタグをもち、依存するファイルにorigin/mainブランチから差分があった場合に依存関係のあるイメージをすべてビルド
pants --changed-since='origin/main' --changed-dependents='transitive' --tag='example-rust-project' --tag='dev-image' package
pants のいまいちなところ
ここまでで、pants が便利なツールだという話をしてきましたが、個人的に pants がいまいちだな...と感じた点について簡単に下記にまとめます。
- python への依存性があるため、python のバージョン管理コストが発生する点
- python を
pyenvでバージョン管理している場合は別途パスの設定等が必要なこと(またmiseなどの他の python のバージョンマネージャ使った場合は設定方法が不明) - 公式ページに事前インストールすべきツールとして
goが記載されておらず、エラーの解決に時間を要したこと(別の言語を丸ごと追加することになるとは思わなかった...) - monorepotool に要求されているプロジェクトの依存性可視化機能が、CLI しかサポートされておらずグラフィカルでない(プラグインも存在するが、古い pants へのバージョン指定あり)
- VSCode で
BUILDファイルのシンタックスハイライトが効かないこと (python ファイルとして認識されない) - VSCode 拡張の suspenders があまり活発に開発されていなさそう
おわりに
本記事では、 pants を利用した docker build の方法についてご紹介しました。
pants を利用することで、docker イメージ間にビルド依存性があるイメージでもスマートにビルド実施を行うことができました。pants についてはまだまだ知らない機能も多いので引き続き調査していきたいです。
一方で、docker build をいい感じにするためだけに pants を導入するのはミドルウェアとしての管理コストが高い気もするので、docker をネイティブサポートしている別のツールも検討しても良いという印象を受けました。
今代わりのツールとして個人的に注目しているのは、docker や k8s に焦点を当てた開発支援ツールである Tilt でこれが良さげな感じがするので調査を進めていきたいと思います。
-
あくまで想定するだけで本記事で pants の CI/CD 運用方法は説明しません。本記事では、ローカル環境の Ubuntu 仮想マシン上に pants の環境を構築して、pnats で docker build を実行するところまでをゴールとします。 ↩︎
Discussion