nabeo がピーしているブログ (仮)

どーも、nabeop です

手元で Dockerfile から docker イメージを作る時に使っている Makefile の書き方

手元環境で Dockerfile から docker イメージを作る時はいちいち docker build コマンドを直接実行するのはダルいのでラッパースクリプトなどで実行するようにしています。

管理している Dockerfile や docker イメージの種類が多くなってくると素朴なラッパースクリプトでは作成したい docker イメージを狙い撃ちで生成するにはラッパースクリプトで一工夫が必要になります。これだとラッパースクリプトが必要以上に多機能になってしまい、作業の本質以外に気を取られてしまいます。

古くから使われている GNU make を使えば、Dockefile から docker イメージを作成するルールをいい感じに作れたので、作り方のメモを書いておきます。

まず、Makefile のルールの書き方は

生成物(ターゲット名): 生成元のリスト

といった感じに書いていきます。ターゲット名で指定しているファイルと生成元のリストで列挙されているファイルのタイムスタンプを比較して、生成物よりも新しい生成元のファイルがあればルールが実行されます。

ただし、docker build で生成されるのは docker イメージなので、ファイルのタイムスタンプを取ることが GNU make の世界ではできません。そこで docker build の後に docker inspect コンテナイメージ | jq -r .[].RepoTags[] > .build といったように生成したコンテナイメージの情報を書いたファイルを GNU make の世界での生成物とするうにします。以下のような感じです。

.build: Dockerfile
  docker build -t hoge:latest -f Dockerfile .
  docker inspect hoge:latest | jq -r .[].RepoTags[] > .build

生成物である .build ファイルに記述する内容は生成した docker イメージが特定できればなんでも良いと思いますが、 make clean などで消す時の利便性を考えて docker イメージのレポジトリとタグ名を入れています。clean ターゲットは以下のように書いています

clean:
  cat .build | xargs -I {} docker rmi {}

また、docker で multi stage build を使用している場合はターゲットの生成元にベースになる Docker イメージを生成した時の .build ファイルを指定しておけば依存関係に組み込まれて便利です。

.build-base: Dockerfile.base
  docker build -t base:latest -f Dockerfile.base .
  docker inspect base:latest | jq -r .[].RepoTags[] > .build-base

.build: Dockerfile .build-base
  docker build -t hoge:latest -f Dockerfile .
  docker inspect hoge:latest | jq -r .[].RepoTags[] > .build

さらに Dockerfile 内で ADD 命令や COPY 命令などでコンテナイメージにファイルを配置しておきたい場合は Dockefile を置いているディレクトリに roo-hogehoge ディレクトリをコンテナ内の / として見立てて配置しておくと Makefile のターゲットでは $(shell find root-hogehoge -type f) として依存関係に組み込んでいます。

具体的には nginx を nginx-build で作るときに -c で指定するスクリプトをコンテナ内の /tmp/nginx-build/nginx_configure.s として使用するときは以下のように配置しています。

.
├── Dockerfile
├── Makefile
└── root
    └── tmp
        └── nginx-build
            └── nginx_configure.sh

このときの Makefile は以下のようにターゲットを指定しています。

.build: Dockerfile $(shell find root -type f)
  docker build -t nginx:latest -f Dockerfile .
  docker inspect nginx:latest | jq -r .[].RepoTags[] > .build

実際に使っている Makefile は以下のようになっています

MAKEFLAGS = -j 4
DOCKER_BUILD_ARGS := --no-cache --squash --rm
IMAGE_NAME := varnish
USER_NAME := $(shell whoami)
BUILD_FILES = .build-4.1 .build-5.2 .build-6.2 .build-6.0lts

.PHONY: build
build: $(BUILD_FILES)

.build-4.1: Dockerfile.jessie
  docker build $(DOCKER_BUILD_ARGS) --build-arg VARNISH_VERSION=41 -t '$(USER_NAME)/$(IMAGE_NAME):jessie-4.1' -f Dockerfile.jessie .
  docker inspect '$(USER_NAME)/$(IMAGE_NAME):jessie-4.1' | jq -r '.[].RepoTags[]' > .build-4.1

.build-5.2: Dockerfile.jessie
  docker build $(DOCKER_BUILD_ARGS) --build-arg VARNISH_VERSION=52 -t '$(USER_NAME)/$(IMAGE_NAME):jessie-5.2' -f Dockerfile.jessie .
  docker inspect '$(USER_NAME)/$(IMAGE_NAME):jessie-5.2' | jq -r '.[].RepoTags[]' > .build-5.2

.build-6.2: Dockerfile.stretch
  docker build $(DOCKER_BUILD_ARGS) --build-arg VARNISH_VERSION=62 -t '$(USER_NAME)/$(IMAGE_NAME):stretch-6.2' -f Dockerfile.stretch .
  docker inspect '$(USER_NAME)/$(IMAGE_NAME):stretch-6.2' | jq -r '.[].RepoTags[]' > .build-6.2

.build-6.0lts: Dockerfile.stretch
  docker build $(DOCKER_BUILD_ARGS) --build-arg VARNISH_VERSION=60lts -t '$(USER_NAME)/$(IMAGE_NAME):stretch-6.0lts' -f Dockerfile.stretch .
  docker inspect '$(USER_NAME)/$(IMAGE_NAME):stretch-6.0lts' | jq -r '.[].RepoTags[]' > .build-6.0lts

.PHONY: clean
clean: $(BUILD_FILES)
  cat $^ | xargs -I {} docker rmi {}
  rm -f $^

BUILD_FILES に対象となる .build 相当のファイルを列挙して、build ターゲットや clean ターゲットで指定しておくことで -j オプションで並列化できるので便利です。

なんらかの CI/CD パイプラインを構築している場合はこのままでは使えないけど、手元でテストする分にはこんな感じで割と便利に使えています。