おはようございます。こんにちは。こんばんは。
OPTIMIND x Acompany Advent Calendar 2021の9日目を担当することになりました、
Acompanyエンジニアの田中です。
みなさん12月いかがお過ごしでしょうか。令和3年も終わろうとしていますね。(え?もう令和4年目?)
僕は先日ストーブの前でみかんを食べていたら急に年末を感じました。みかん美味しい。
さて、今回はタイトルの通り、マルチステージビルドを使ってDockerfileで複数のステージを継承しながらイメージを定義する方法について紹介します。
はじめに
マルチステージビルドはDockerのバージョン17.05以上で使用可能なビルド方法で、1つのDockerfileの中に複数のFROM
命令を書くことでイメージをステージごとにビルドすることができる機能です。各ステージは独立にビルドされますが、ステージ間でファイルをコピーすることも可能です。
実際に適当なアプリケーションを例に使い方を見ていきましょう。
今回は例としてHello World
と喋るmain.goを用意しました。
package main
import (
"fmt"
)
func main(){
fmt.Println("Hello world")
}
マルチステージビルドの使い方
まずマルチステージビルドを使わずに普通にDockerfileを書いてみます。
# syntax=docker/dockerfile:1
FROM golang:1.17
WORKDIR /
COPY main.go /
RUN go build main.go
CMD ["./main"]
ビルドすると以下のようになります。
$ docker build -t myapp:latest .
...
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp latest 510459185c19 4 seconds ago 943MB
golang 1.17 2e7da682bb63 5 days ago 941MB
alpine latest c059bfaa849c 2 weeks ago 5.59MB
$ docker run myapp:latest
Hello World
実行できました。
ではこれを実行する最強のコンテナのイメージをマルチステージビルドで作ってみましょう。
# syntax=docker/dockerfile:1
FROM golang:1.17
WORKDIR /
COPY main.go /
RUN go build main.go
FROM alpine:latest
WORKDIR /
COPY --from=0 /main ./
CMD ["./main"]
FROM golang:1.17
から始まる1つ目のステージではアプリケーションをビルドしていますが、このステージでは実行を行なっていません。続く FROM alpine:latest
から始まる2つ目のステージで1つ目のステージからビルド済みのバイナリのみをCOPY
でもってきて実行しています。
こんな感じでビルドと実行をステージで分けてやることで、実行時に使用するイメージには必要最小限のものしか入れないようにして軽量化することができます。非常に便利ですね。
このDockerfileからビルドする際は先ほどと同様build
コマンドを実行してやるだけです。
$ docker build -t myapp2:latest .
...
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp2 latest 5e37a5aeed6c 4 seconds ago 7.35MB // <- 2つ目のステージのイメージ
<none> <none> cd5ea6a13fae 4 seconds ago 943MB // <- 1つ目のステージのイメージ
golang 1.17 2e7da682bb63 5 days ago 941MB
alpine latest c059bfaa849c 2 weeks ago 5.59MB
$ docker run myapp:latest
Hello world
実行できました。
なんということでしょう!
先程のマルチステージビルドを使わない場合だと943MBだった実行用イメージが、
alpineをベースにした7.35MBの軽量なイメージに早変わり!
かなりお手軽に軽量化することができました。デプロイ時間もだいぶ短縮できそうですね。
マルチステージビルドを使うと開発環境用ステージ、テスト用ステージ、本番環境用ステージのように複数の環境を切り分けてイメージにすることが可能です。その際それぞれのステージで共通した処理を行いたい場合があります。その際に使える便利な方法を紹介します。
マルチステージビルドで複数の環境を定義
FROM
命令はイメージだけではなく前のステージに対しても使うことが可能です。
公式ドキュメントの一番下で説明されていて案外見落とされがちですがとても便利。
また、AS
を使うことでステージに名前をつけてビルドステージを指定することもできます。
この記述方法を使うと以下のようにDockerfileが書けます。
# syntax=docker/dockerfile:1
# 共通処理をまとめた基底ステージ
FROM ubuntu:20.04 as base
ENV PATH $PATH:/usr/local/go/bin
RUN apt-get update && \
apt-get install -y \
ca-certificates \
wget \
vim
RUN wget https://dl.google.com/go/go1.17.linux-amd64.tar.gz && \
tar -C /usr/local -xzf go1.17.linux-amd64.tar.gz
RUN mkdir /root/myapp
# 開発環境用devステージ
FROM base as dev
WORKDIR /root/myapp
RUN go install golang.org/x/tools/cmd/goimports@latest
RUN go install golang.org/x/lint/golint@latest
# アプリケーションのbuilderステージ
FROM base as builder
WORKDIR /root/myapp
COPY main.go ./
RUN go build main.go
# 本番環境用prodステージ
FROM alpine:latest as prod
WORKDIR /root/myapp
# builderステージから成果物だけ持ってくる
COPY --from=builder /root/myapp/main ./
CMD ["./main"]
ビルドするステージを指定する場合は以下のように --target
オプションを使います。
$ docker build -t myapp/prod:latest --target prod . # base + dev + builder + prodステージをビルド
...
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp/prod latest bb6fe8644492 5 seconds ago 7.35MB // <- prodステージのイメージ
<none> <none> d934f861ea4e 6 seconds ago 720MB // <- builderステージのイメージ
<none> <none> 37f815320424 7 seconds ago 771MB // <- devステージのイメージ
alpine latest c059bfaa849c 2 weeks ago 5.59MB
ubuntu 20.04 ba6acccedd29 7 weeks ago 72.8MB
イメージのリストからもわかる通り、マルチステージビルドであるステージをターゲットにビルドを行うと
それ以前の全てのステージがビルドされてしまいます。prodステージのビルドはdevステージに依存していないので、ビルドをスキップしたいです。
そういう場合は、BuildKitを使ってビルドをすることで必要なステージだけビルドすることが可能です。
$ DOCKER_BUILDKIT=1 docker build -t myapp/prod:latest --target prod .
...
=> [internal] load metadata for docker.io/library/alpine:latest 3.0s
=> [internal] load metadata for docker.io/library/ubuntu:20.04 2.9s
...
=> [base 1/3] FROM docker.io/library/ubuntu:20.04@sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322 11.8s
=> => resolve
...
=> [prod 1/3] FROM docker.io/library/alpine:latest@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300 1.3s
=> => resolve
...
=> [base 2/3] RUN apt-get update && apt-get install -y ca-certificates wget vim 43.0s
=> [base 3/3] RUN wget https://dl.google.com/go/go1.17.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.17.linux-amd64.tar.gz 47.2s
=> [builder 1/3] COPY main.go ./ 0.0s
=> [builder 2/3] RUN go build main.go 0.5s
=> [prod 2/3] COPY --from=builder /main ./
=> exporting to image
...
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp/prod latest f845490f2588 2 minutes ago 7.35MB
myapp/prodイメージだけ作成できました。
docker-composeでステージを切り替える
マルチステージビルドで記述したDockerfileをdocker-compose.ymlで切り替える場合は以下のように書くことができます。
version: "3.3"
services:
myapp_dev:
tty: true
build:
context: .
dockerfile: Dockerfile
target: dev
volumes:
- type: bind
source: .
target: /root/myapp
myapp_prod:
build:
context: .
dockerfile: Dockerfile
target: prod
BuildKitを使ってビルドする場合は先頭にCOMPOSE_DOCKER_CLI_BUILD=1
をつけます。
$ COMPOSE_DOCKER_CLI_BUILD=1 docker-compose build myapp_dev
$ docker-compose run myapp_dev /bin/bash
Creating qiita_myapp_dev_run ... done
root@1dbaa3d9bcd7:~/myapp# go build main.go && ./main
Hello World
無事実行できました。
参考文献