マルチステージ

このセクションは マルチステージビルド(multi-stage build) を見ていきます。マルチステージビルドを使う理由は、主に2つあります。

  • 構築ステップを並列に実行できるため、構築パイプラインをより速くより効率的にできる。

  • プログラムを実行するために必要なものだけを含む、形跡が最小の最終イメージを作成できる。

Dockerfile 内では FROM 命令によって 構築ステージ(build stage) を表します。前のセクションにある Dockerfile では、マルチステージビルドを活用していませんでした。先ほどは、すべて1つの構築ステージでした。つまり、最終イメージはプログラムのコンパイルに使われたリソースで膨れあがっています。

$ docker build --tag=buildme .
$ docker images buildme
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
buildme      latest    c021c8a7051f   5 seconds ago   150MB

プログラムは実行可能なバイナリにコンパイルされているため、最終イメージ内に Go 言語ユーティリティは不要になります。

ステージの追加

マルチステージビルドの使用により、構築用と実行環境用で、異なるベースイメージが選べます。 構築の成果物(build artifacts) を構築用ステージから実行用ステージにコピーできます。

以下のように Dockerfile を編集します。この変更により、最小の scratch イメージをベースとして使う別のステージを作成します。最終の scratch ステージでは、前のステージで構築されたバイナリを、新しいステージのファイルシステムへとコピーしています。

  # syntax=docker/dockerfile:1
  FROM golang:{{site.example_go_version}}-alpine
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .
  RUN go build -o /bin/client ./cmd/client
  RUN go build -o /bin/server ./cmd/server
+
+ FROM scratch
+ COPY --from=0 /bin/client /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

これで、イメージを構築して調査したら、値が著しく小さくなったと分かるでしょう。

$ docker build --tag=buildme .
$ docker images buildme
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
buildme      latest    436032454dd8   7 seconds ago   8.45MB

150MB だったイメージは、ほんの 8.45MB になりました。最終的なイメージに含まれるのはバイナリだけであり、他になにもないからです。

並列処理

これまでは、イメージの形跡を減らしました。以下の手順では、マルチステージビルドの並列処理を使い、構築速度を改善できる方法を見ていきます。現時点の構築は、次々にバイナリを作成します。しかし、サーバの前にクライアントを作成する必要はなく、その逆の場合も同じです。

バイナリの構築ステップを、個々のステージに分割できます。最後の scratch ステージでは、それぞれの適切な構築ステージからバイナリをコピーします。それぞれの構築をステージに分ければ、 Docker は並列でそれらを実行できます。

構築用のステージでは、Go コンパイルツールとアプリケーション依存関係の、それぞれのバイナリが必要です。これらに共通するステップを、再利用可能な ベースステージ(base stage) として定義できます。これを可能とするには、 FROM イメージ AS ステージ名 のパターンを使い、ステージに対して名前を割り当てます。これでそのステージ名は、他のステージの FROM 命令内で参照できるようになります( FROM ステージ名 )。

また、バイナリ構築ステージでも名前を割り当て可能なため、バイナリを最終 scratch イメージにコピーするには COPY --from=ステージ名 としてステージ名を参照します。

  # syntax=docker/dockerfile:1
- FROM golang:{{site.example_go_version}}-alpine
+ FROM golang:{{site.example_go_version}}-alpine AS base
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .
+
+ FROM base AS build-client
  RUN go build -o /bin/client ./cmd/client
+
+ FROM base AS build-server
  RUN go build -o /bin/server ./cmd/server

  FROM scratch
- COPY --from=0 /bin/client /bin/server /bin/
+ COPY --from=build-client /bin/client /bin/
+ COPY --from=build-server /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

これで、最初のバイナリを次々に構築する方法に替わり、 build-clientbuild-server ステージが同時に処理されます。

並列にステージを実行

構築ターゲット

最終イメージは小さくなり、並列処理を使って構築できるようになりました。しかし、このイメージは少し変わってて、同じイメージ内にクライアントとサーバ両方のイメージが入っています。これらは2つのイメージに分けるべきではないでしょうか。

1つの Dockerfile を使って、複数のイメージの作成が可能です。 --target フラグを使い、構築の 対象となるステージ(target stage) を指定できます。名前の付いていない FROM scratch ステージの名前を書き換え、 clientserver の異なる2つのステージにします。

  # syntax=docker/dockerfile:1
  FROM golang:{{site.example_go_version}}-alpine AS base
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .

  FROM base AS build-client
  RUN go build -o /bin/client ./cmd/client

  FROM base AS build-server
  RUN go build -o /bin/server ./cmd/server

- FROM scratch
- COPY --from=build-client /bin/client /bin/
- COPY --from=build-server /bin/server /bin/
- ENTRYPOINT [ "/bin/server" ]

+ FROM scratch AS client
+ COPY --from=build-client /bin/client /bin/
+ ENTRYPOINT [ "/bin/client" ]

+ FROM scratch AS server
+ COPY --from=build-server /bin/server /bin/
+ ENTRYPOINT [ "/bin/server" ]

これで、クライアントとサーバのプログラムが分けられた Docker イメージ(タグ)を構築できます。

$ docker build --tag=buildme-client --target=client .
$ docker build --tag=buildme-server --target=server .
$ docker images buildme
REPOSITORY       TAG       IMAGE ID       CREATED          SIZE
buildme-client   latest    659105f8e6d7   20 seconds ago   4.25MB
buildme-server   latest    666d492d9f13   5 seconds ago    4.2MB

イメージは更に小さくなり、それぞれ 4 MB です。

この変更は、バイナリをそれぞれ構築する必要もありません。 client ターゲットの構築を選ぶ場合、 Docker は対象となるターゲットが必要なステージのみ構築します。 build-serverserver ステージは必要がなければ飛ばします。同様に、 server ターゲットの構築では、 build-clientclient ステージを飛ばします。

まとめ

マルチステージビルドは肥大化しないイメージの構築に役立ち、形跡をより小さくし、構築を速くするのにも役立ちます。

関連情報:

次のステップ

次のセクションでは、構築速度を更に改善するため、ファイルマウントの使い方を説明します。