docker

Dockerのマルチステージビルドを使う

この記事はモバイルファクトリー AdventCalendar 2017 31日目 25日目の記事です。
24日目は @mattak さんの『UniRx Quiz』でした。

はじめに

本記事は、Dockerのマルチステージビルド(multi-stage builds)に関する公式ドキュメントの非公式拙訳です。

本編

マルチステージビルドを使う

マルチステージビルドは、Docker17.05以上で利用できる新機能です。
マルチステージビルドは可読性、保守性を保ちながらDockerfileを最適化するのに苦労している人の役に立ちます。

謝辞: 彼自身のブログへの投稿Builder pattern vs. Multi-stage builds in Dockerを、以降の例のベースとして使用することを許可してくれたAlex Ellisに感謝します。

マルチステージビルド以前

サイズを小さく保ちながらDockerイメージをビルドすることは、最もやりがいのあることのひとつです。
Dockerfile内の各々の命令では、イメージにレイヤが追加されます。
したがって、次のレイヤを作成する(次の命令に移る)前に、不要な生成物のクリーンアップする必要があります。

本当に効率的なDockerfileを作成するには、レイヤをできる限り小さく保ち、各レイヤが前のレイヤの生成物から必要なものを確保するために、シェルのトリックやその他のロジックを採用する必要がありました。

実際に、開発用にはアプリケーションのビルドに必要なすべてが含まれるDockerfileを使用し、プロダクト用にはアプリケーションおよび実行に必要なもののみが含まれるスリム化されたDockerfileを使用することは非常に一般的でした。
これがいわゆる"ビルダーパターン"です。
2つのDockerfilesを保守することは、理想的ではありません。

ここでの Dockerfile.buildDockerfile の例は、ビルダーパターンを利用したものです。

Dockerfile.build
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

この例では、イメージに追加のレイヤを作成しないように、Bashの && 演算子を利用することで、2つの RUN 命令をわざとまとめています。
これは失敗しやすく、保守も困難です。
例えば、別のコマンドを挿入したときに \ で行の継続をし忘れるなどの事態は容易に発生します。

Dockerfile
FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]
build.sh
#!/bin/sh
echo Building alexellis2/href-counter:build

docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \  
    -t alexellis2/href-counter:build . -f Dockerfile.build

docker create --name extract alexellis2/href-counter:build  
docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app  
docker rm -f extract

echo Building alexellis2/href-counter:latest

docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app

build.sh を実行する時、最初のイメージをビルドし、成果物をコピーするためにコンテナを生成し、その後に2つめのイメージをビルドする必要があります。
両方のイメージはシステム上の場所をとりますし、同様に app もローカルディスクに残り続けます。

マルチステージビルドは、この状況を非常にシンプルにします!

マルチステージビルドを利用する

マルチステージビルドでは、Dockerfileで複数の FROM 命令を利用します。
FROM 命令では、異なるベースイメージを使用することができ、それぞれで新しいビルドステージを開始します。
あるステージの生成物は、別のステージへとコピーすることができます。1

Dockerfile
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

こうすることで、 Dockerfile のみを利用すれば良くなります。
別のビルドスクリプトや、その他のものは必要ありません。
ビルドするには単に docker build を実行します。

$ docker build -t alexellis2/href-counter:latest .

実行結果は、前の例と同様の小さなイメージですが、複雑さは大きく低減されています。
中間イメージを作成する必要がなく、生成物をローカルシステムに抽出する必要も一切ありません。

どのように動作しているのでしょう?
2つめの FROM 命令は、 alpine:latest イメージをベースとして新しいビルドステージを開始します。
COPY --from=0 の行は、前のステージでビルドされた成果物をこの新しいステージへコピーします。
Go SDKと中間イメージは取り残され、最終的なイメージへは保存されません。

ビルドステージに名前をつける

デフォルトではビルドステージには名前がついていないので、参照するためにはそれらを示す整数値を用います。
この整数値は、最初の FROM 命令以降、0から順に番号付けされます。

しかし、 FROM 命令に as <NAME> を追加することで、ステージに名前をつけることができます。
以下の例では、 COPY 命令で名前のつけられたステージを利用することにより、前の例を改善します。
こうすることにより、後でDockerfile内で命令の順序を変更しても COPY 命令が壊れることはありません。

Dockerfile
FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

次のステップ

これらの例の完全なソースコードとウォークスルーのために、Builder pattern vs. Multi-stage builds in Dockerを確認してください。

おわりに

モバイルファクトリーAdvent Calendar 2017最終日の記事でした!
ここまでお読みいただきありがとうございます!
それでは良いお年を!


  1. 原文は "You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image." あるステージの生成物(artifacts)は、最終的なイメージ内に望ましくないものを残して、別のステージへと選択的にコピーすることができます。