Code Chrysalis Advent Calendar 2019、24日目の投稿です。
こんにちは。現在、CodeChrysalisのイマーシブブートキャンプ(Cohort 10)に参加中のなおとです。
ブートキャンプも残すところ1週間を切りました。今は12/26(木)に行われるDemo Dayという卒業発表会に向けてチーム開発を日々頑張っています。お時間ある方は是非Demo Dayにご参加ください!
Demo Day の詳細はこちらになります。
Code Chrysalisのブートキャンプには、**多言語週間(Polyglottal Week)**と呼ばれる、今まで使ったことのないプログラミング言語を1つ選択し、1週間で言語の習得からアプリケーション開発までを行うという機会があります。
私はこの多言語週間で、今まで扱ったことのなかったGolangを選択し、Golangを使ったフルスタックアプリケーションを作成しました。また、Dockerに興味があったので、Docker上でアプリをビルド・実行する方法も合わせて学びました。今回は、私が学んだ内容の一部についてご紹介したいと思います。
はじめに
本稿では、Dockerの基本的な知識があることを前提として、Dockerの重要な機能の1つであるマルチステージビルドについて解説していきます。
また、以下のレポジトリにサンプルコードを用意しました。今回説明する内容は、すべて以下のレポジトリをもとに検証を行っているので、ご自身の環境で試したい場合は合わせてご覧ください。
https://github.com/Imamachi-n/docker-multi-stage-build-101
Docker上でアプリケーションをビルド・実行してみよう
まず始めに、作成したアプリケーションをDockerコンテナ上でビルド・実行したい場合、どのような方法を取ればいいのでしょうか?まずは簡単な例として、Golangで作成したアプリケーション(REST APIサーバ)を以下のDockerfileを用いて、ビルド・実行する場合を考えてみましょう。
以下に、サンプルのDockerfileを示しました。Dockerのベースイメージとして、公式のgolang
イメージを使っています。
ファイルの内容を簡単に説明すると、
-
ENV
でGolangのビルド条件(OS、CPUアーキテクチャ等)を環境変数として設定。 -
WORKDIR
で作業ディレクトリを指定。 -
COPY
で、ローカル環境にあるGolangプロジェクトをDockerコンテナ内にコピー。 -
RUN
でGolangのアプリケーションをビルドするコマンドを実行。 -
EXPOSE
で9000番のポートを開け、このポートを通してDockerコンテナと通信ができるように設定。 - 最後に、
ENTRYPOINT
でDockerコンテナ起動時に、Golangのアプリケーションが起動するように指定。
という内容が記述されています。
FROM golang:1.13.4
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /docker-multi-stage-build-101
COPY . .
RUN go build -o "bin/goServer"
EXPOSE 9000
ENTRYPOINT ["/docker-multi-stage-build-101/bin/goServer"]
Dockerfileから、Dockerイメージをビルドし実行すると、Golangで作成したREST APIサーバが立ち上がります。
$ docker build . -f docker/01_raw/Dockerfile -t go-server-raw:dev
$ docker run --rm -p 9000:9000 go-server-raw:dev
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /api/user/:name/*action --> docker-multi-stage-build-101/route.GetAction (4 handlers)
[GIN-debug] GET /api/welcome --> docker-multi-stage-build-101/route.GetWelcome (4 handlers)
[GIN-debug] Listening and serving HTTP on :9000
2019/12/22 02:31:38 Defaulting to port :9000
続いて、以下の通りに、curl
コマンドを使ってDockerコンテナで立ち上げたREST APIサーバにアクセスしてみましょう。Hello Code Chrysalis
と表示されれば成功です。これで、Dockerコンテナ上でアプリケーションが起動していることが確認できました。
$ curl -X GET 'http://localhost:9000/api/welcome?firstname=Code&lastname=Chrysalis'
Hello Code Chrysalis
次に、docker images
コマンドを使って、Dockerイメージのサイズを見てみましょう。なんと、915MBというかなり大きなサイズになっていることがわかります。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-server-raw dev eb8f35015c94 11 seconds ago 915MB
これは、アプリケーションの実行に不要なもの(ビルド時に使用したライブラリ等)が、そのまま同じDockerイメージ内にゴミとして残ってしまっているためです。AWSやGCPなどの環境にデプロイすることを考えると、できればDockerイメージのサイズを小さくしたいですね。
この問題を解決するために、マルチステージビルドが登場する以前は、Builderパターンと呼ばれるコンテナ管理方法が利用されていました。
マルチステージビルド以前: Builderパターン
Builderパターンでは、ビルド用とデプロイ用の2つのDockerfileを用意します。ビルド用のDockerコンテナでアプリケーションをビルドし、アプリケーションの実行に必要なものだけを実行用のコンテナにコピーします。
結果として、Builderパターンでアプリケーションが動作するDockerコンテナを用意した場合、以下のものが必要となります。
- 2つのDockerfile(ビルド用と実行用)
- (ビルド環境から実行環境への)ビルド成果物の受け渡し用のシェルスクリプト
Dockerマルチステージビルド
そこで、満を持してマルチステージビルドの登場です。この機能は、Docker 17.05から追加された機能になります。端的に言うと、Builderパターンで実践していた内容を1つのDockerfileにまとめることができます。
ビルドステージ(中間コンテナイメージ)
普段ベースイメージを指定するために、Dockerfile内にFROM
命令を記述していると思います。マルチステージビルドでは、以下のように、1つのDockerfile内にFROM
命令を複数記述します。
前半のFROM
以下のブロックのことをビルドステージ(中間Dockerイメージ)と呼んでいます。この中間Dockerイメージは、実行用Dockerイメージにビルド成果物を渡した後、削除されます。そのため、最終的に生成される実行用のDockerイメージに含まれることはありません。
ビルドステージの命名
続いて、AS
を使うことで、ビルドステージに対して名前を付けることができます。下図の例は、ビルドステージの中間Dockerイメージに対してbuilder
という名前を指定しています。
ちなみに名前を指定しなかった場合、FROM
命令の順番に合わせて0, 1, 2,...
という連番名が自動で振られます。例えば、上図の場合だと、ビルドステージは0
、実行用のDockerイメージは1
という連番名が振られます。
ビルド成果物をビルド環境から実行環境のDockerイメージへコピー
ビルドステージで作成したビルド成果物を、後半のFROM
以下のブロック(アプリケーション実行用のDockerイメージ)にコピーすることができます。方法としては、COPY
の--from
オプションでビルドステージ名を指定し、ビルド成果物をビルドステージから実行用のDockerイメージにコピーします。
このように、マルチステージビルドを使うことで、ビルド用と実行用のDockerfileを分けることなく、1つのDockerfileで記述することができるようになります。実はそれ以外にもメリットがあります。
ビルド用と実行用にDockerベースイメージをそれぞれ指定することができるので、例えば、alpine
などの軽量コンテナイメージを実行用のDockerベースイメージとして選択することができます。こうすることで、作成されたDockerイメージ内に、アプリケーションの実行に必要なものだけを配置することができ、コンテナのサイズをよりスリム化させることができます。
マルチステージビルドの具体例
それでは、具体的な例として、最初にお見せしたGolangのアプリケーション(REST APIサーバ)をマルチステージビルドを使ったDockerfileに書き換えてみたいと思います。
以下がDockerfileの内容になります。先程とほとんど変わりませんが、COPY --from=builder
でビルドステージ内のビルド成果物(/docker-multi-stage-build-101/bin/goServer
)を、実行用のDockerイメージに/goServer
としてコピーしています。
# Builder image (intermediate container)
FROM golang:1.13.4 as builder
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /docker-multi-stage-build-101
COPY . .
RUN go build -o "bin/goServer"
# Runtime image
FROM alpine
COPY --from=builder /docker-multi-stage-build-101/bin/goServer /goServer
EXPOSE 9000
ENTRYPOINT ["/goServer"]
それでは、Dockerfileをビルドして、Dockerイメージのサイズがどれだけスリム化された確認してみましょう。
$ docker build . -f docker/02_multi-stage-build/Dockerfile -t go-server-multi:dev
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-server-multi dev 2d5f5819aa81 4 hours ago 21.4MB
21.4MBとかなり小さなDockerイメージとなっていることがわかると思います。最初の例では915MBだったので、Dockerイメージのサイズをおよそ1/40程度までスリム化できています。
最後に
Dockerのマルチステージビルドを利用することで、アプリケーションのビルド環境と実行環境の設定を1つのDockerfileに記述することができます。これは、今まで使われていたBuilderパターンなどと比較しても、より簡素で管理のしやすい方法だと感じました。Dockerfileを記述する際は、積極的に使っていくといいのではないかと思います。
参考
Use multi-stage builds
Dockerの公式ドキュメントの説明です。
https://docs.docker.com/develop/develop-images/multistage-build/
docker-multi-stage-build-101
今回の記事で使用したサンプルコードになります。
https://github.com/Imamachi-n/docker-multi-stage-build-101
BioRxivGo
Code Chrysalisの多言語週間中に作成したフルスタックアプリケーション(Vue, Golang, PostgreSQLなど)になります。こちらのアプリケーションではマルチステージビルドに加えて、Docker composeでアプリケーションを起動しています。
https://github.com/Imamachi-n/BioRxivGo