Dockerとコンテナ技術 その12:マルチステージビルド

今回の記事ではDockerイメージを効率的に作成するためのテクニックであるマルチステージビルドについてまとめていきます。
マルチステージビルドをすることで、効率的なビルドやビルドイメージの軽量化、セキュリティリスクの軽減などの効果が期待できます。

マルチステージビルドとは

マルチステージビルドとは、通常、1つのイメージのみで構築するDockerfileの中に複数のイメージを利用して構築する手法です。この時の一つ一つのイメージのことをステージといいます。
各ステージでは別のイメージを利用することができ、ビルド時のみに必要なコンパイラなどを分けることができます。
このようにステージ毎に必要なイメージを分けることで、最終的に必要なものだけを最終イメージに含めることができ、イメージの容量の軽量化や効率的なビルドを実現することができます。

マルチステージビルド実践

マルチステージビルドの簡単な実装例を通じて、より深く理解できるようにしていきましょう。

実装例①:ビルドと本番でステージを分ける

最初の例として、ビルド時と本番でステージを分けるケースを見てみます。
ビルド時と本番で分けることで、ビルド時にだけ必要なコンパイラなどのツールを最終イメージからは除くことができます。
今回は、簡単なC言語のコードをビルドステージでコンパイルして、コンパイル後のファイルを最終イメージで実行するようなマルチステージビルドを行っていきます。
以下のようなフォルダ構成を作成してください。

multi-stage
├── Dockerfile
└── hello.c

次に、hello.cファイルを以下のように編集してください。
こちらはコードが何を意味するかなどは理解しなくても問題ありません。
とりあえず、”Hello, World!”と出力されるコードということだけ認識しておいてください。

#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}

ではこのファイルをイメージビルド時にコンパイルして、コンパイル後のファイルを実行するイメージを作成するDockerfileを作成していきます。

# ビルドステージ
# gccというC言語のファイルをコンパイルするツールを扱うイメージを使用
# AS 〇〇と付けることでイメージに別名をつけることができる
FROM gcc:12.2.0 AS compiler
WORKDIR /app
COPY ./hello.c .
RUN gcc hello.c

# 本番ステージ
# 最終イメージとなる、ubuntuのLinux環境でビルドステージで作成したファイルを実行する
FROM ubuntu:20.04
WORKDIR /app
# "--from=イメージ"を指定して他のステージのイメージを指定している
# つまり、compilerというイメージから/app/a.outというファイルをコピーするコマンドになっている
COPY --from=compiler /app/a.out .
CMD ["./a.out"]

上記のように、FROMコマンドを複数記述することでマルチステージビルドを実行することができます。
上から順に実行され最後のステージのイメージが最終イメージとなります。
また、COPY --from=compiler /app/a.out .の部分で指定している通り、他のステージのファイルなどをコピーしてくることもできます。
では、こちらのDockerfileを利用してイメージをビルドします。
以下のコマンドを実行してください。

docker image build -t multi-image .

以下のように表示されれば成功です。

以下のコマンドでコンテナを実行してみましょう。

docker container run --rm multi-image

以下のように”Hello, World!”と表示されれば成功です。

ちなみに、マルチステージビルドを利用しないでイメージを作成する場合、Dockerfileは以下のようになります。

FROM gcc:12.2.0
WORKDIR /app
COPY ./hello.c .
RUN gcc hello.c
CMD ["./a.out"]

こちらをイメージして、先ほどのマルチステージビルドと大きさを比べてみると以下のようになります。

multi-imageがマルチステージビルドを利用していて、single-imageは利用していません。
SIZEを見てみるとマルチステージビルドを利用した方が小さくなっているのがわかります。

実装例②:環境毎にイメージを分ける

マルチステージビルドを利用すると、ベースイメージなどの共通部分を持つ複数のイメージをビルドすることができます。
これを利用して、開発環境と本番環境など環境毎にイメージを分けることを効率的に行えます。
Dockerfileを任意のフォルダに作成して以下のように編集してください。

# 開発と本番の共通部分となるベースイメージを定義
FROM ubuntu:20.04 AS base
RUN apt update
CMD ["sh", "-c", "echo My name is $my_name"]

# 開発環境を定義
FROM base AS dev
ENV my_name=TEST

# 本番環境を定義
FROM base AS prod
ENV my_name=PROD

上記の例では最初にbaseという別名を持つベースイメージを作成しています。
RUN apt updateCMD ["sh", "-c", "echo My name is $my_name"]といったコマンドは開発でも本番でも共通するためこちらに記述されています。
次に開発環境をbaseをベースイメージとして作成し、環境変数my_nameTESTという値を設定しています。
最後に本番環境もbaseをベースイメージとして、環境変数my_namePRODという開発環境と異なる値を設定しています。
では、ここで開発環境のイメージと本番環境のイメージを作成し、コンテナを実行してみましょう。
まずは開発環境から作成してきます。
以下のコマンドを実行してください。

docker image build --target dev -t dev_image .

以下のように表示されれば完了です。

では、コンテナを実行してみましょう。

docker container run --rm dev_image

以下のようにMy name is TESTと出力されれば成功です。
開発環境で設定した環境変数my_name=TESTがしっかり反映されているのが確認できます。

では、本番も同様にビルドしていきます。

docker image build --target prod -t prod_image .

以下のように表示されれば成功です。
開発環境との共通部分がしっかりと実行されているのが確認できます。

ではコンテナを実行してみましょう。

docker container run --rm prod_image

以下のようにMy name is PRODと表示されれば成功です。

まとめ

今回は一つのDockerfileに複数のイメージを定義してイメージビルドの効率化をするテクニックであるマルチステージビルドについてまとめてきました。
本文中で紹介した、ビルドの工程毎にステージを分ける場合と共通部分のベースイメージをもとに環境毎のイメージを作成する場合の2通りが主な利用シーンとなります。
利用しなくても実装可能ではありますが、利用例を見てみると理解するのが難しいものではないと思うので、理解してより効率的で安全な開発を進めていきましょう。
ここまで読んでいただき、ありがとうございました。
それでは、また。