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