はじめに
Go 1.12 がリリースされましたね!
TLS 1.3 の対応や Go Modules の改良がされたようです。
Go 1.12 is released - The Go Blog
今回は Go Modules が含まれている Go 1.12 を使用して、開発環境と本番環境の Dockerfile の作成を考えてみます。
開発環境は docker-compose、本番環境は Kubernetes などのオーケストレーションツールにデプロイすることを想定した Dockerfile の作成を考えます。
アプリケーションは、Go Modules を利用してライブラリ管理し、外部に HTTPS 通信するアプリケーションを想定して Dockerfile を作成しています。
Listen ポートは 8080 ポートです。
環境
- macOS High Sierra Version 10.13.6
- Docker for Mac 2.0.0.3 (Engine: 18.09.2)
ディレクトリ構成
以下のようなルートディレクトリに main.go
、go.mod
、go.sum
が存在するようなアプリケーションを想定しています。
事前に go mod init
コマンドと go build
または、 go mod download
コマンドを実行したプロジェクトを用意してください。
├── Dockerfile // 開発環境の Dockerfile
├── Dockerfile.prod // 本番環境の Dockerfile
├── docker-compose.yml // 開発環境の docker-compose
├── go.mod
├── go.sum
└── main.go
開発環境
開発環境はソースコードが随時変更されるため、fresh
というライブラリを用います。
ソースコード自体は、docker-compose で指定するルートを docker volume に割り当て、ホットリロードを実現します。
Dockerfile
開発環境の Dockerfile の全体は以下のとおりです。
FROM golang:1.12.0-alpine3.9
WORKDIR /go/src/app
ENV GO111MODULE=on
RUN apk add --no-cache \
alpine-sdk \
git \
&& go get github.com/pilu/fresh
EXPOSE 8080
CMD ["fresh"]
Dockerfile の詳細
開発環境の Dockerfile の詳細を追ってみます。
FROM golang:1.12.0-alpine3.9
イメージは、執筆時点(2019/02)の最新の Alpine Linux の Go イメージを指定しています。
WORKDIR /go/src/app
WORKDIR
でアプリケーションを実行するディレクトリを指定しています。
ENV GO111MODULE=on
Go 1.12 では、Go Modules を使用するために必要な環境変数 GO111MODULE
を on
にしています。
RUN apk add --no-cache \
alpine-sdk \
git \
&& go get github.com/pilu/fresh
パッケージとして一通りビルドに必要なものが入っている alpine-sdk
と Go Modules でライブラリ取得時に内部的に git
を利用しているので git
をインストールしています。
また、Go 開発のホットリロードで利用する fresh
をインストールしています。
EXPOSE 8080
8080 ポートで Listen するアプリケーションなので、EXPOSE
命令で 8080 ポートを明示しています。
CMD ["fresh"]
最後に fresh
で起動することで、ホットリロードで起動する Dockerfile の完成です。
docker-compose
開発環境の docker-compose ファイルは以下のとおりです。
version: '3'
services:
app:
build: .
volumes:
- ./:/go/src/app
ports:
- "8080:8080"
docker-compose の詳細
開発環境の docker-compose の詳細を追ってみます。
build: .
build
でビルドする Dockerfile のディレクトリを指定しています。
volumes:
- ./:/go/src/app
volumes
で Dockerfile でも指定した WORKDIR
のディレクトリに対してマウントしています。こうすることでコンテナ内から開発マシンのディレクトリが見えるようになり、fresh
のビルド対象になります。
ports:
- "8080:8080"
ports
は、8080 ポートで Listen するアプリケーションなので、開発マシンとコンテナのポートを割り当てています。
開発環境の実行
以下の docker-compose コマンドで起動し、localhost:8080
にアクセスすることで開発が始められます。
$ docker-compose up -d
本番環境
本番環境は可能な限り小さいイメージを作るために、マルチステージビルドを用います。
Dockerfile
本番環境の Dockerfile の全体は以下のとおりです。
FROM golang:1.12.0 as builder
WORKDIR /go/src/app
ENV GO111MODULE=on
RUN groupadd -g 10001 myapp \
&& useradd -u 10001 -g myapp myapp
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/app
FROM scratch
COPY --from=builder /go/bin/app /go/bin/app
COPY --from=builder /etc/group /etc/group
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
EXPOSE 8080
USER myapp
ENTRYPOINT ["/go/bin/app"]
Dockerfile の詳細
本番環境の Dockerfile の詳細を追ってみます。
FROM golang:1.12.0 as builder
イメージは Alpine Linux ではなくデフォルトのイメージに変更し、マルチステージビルドを用いるために as builder
というステージ名を指定しています。
デフォルトのイメージに変更したのは、Go Modules のインストールに必要な git
や Go ライブラリのインストールや Go のビルドに必要なパッケージが含まれているためです。
マルチステージビルドについては、ステージ名に任意の名前が付けられます。
こうすることで、次のステージでビルドした成果物をコピーできます。
RUN groupadd -g 10001 myapp \
&& useradd -u 10001 -g myapp myapp
ここでは、ユーザーグループとユーザーを作成しています。
これは、次のステージで利用するアプリケーションの実行ユーザーを作成するために実行しています。
また、次のステージで利用する scratch
イメージは shell が実行できないため、ビルドステージでユーザーを作成しています。
COPY go.mod go.sum ./
RUN go mod download
COPY . .
開発環境では volume にマウントしていましたが、本番環境ではアプリケーションをそのままコンテナ内にコピーしています。
そして、COPY
命令で全てのファイルをコピーせず、事前に go.mod
と go.sum
ファイルをコピーしています。
これは、コピー後に go mod download
を実行してライブラリをインストールすることで、次回以降ライブラリの追加、 go.mod
と go.sum
に変更がなくソースコードの変更のみであればソースコード全体をコピーする COPY . .
の行から実行され、それより前のステップはキャッシュが活用されスキップされます。
こうすることでキャッシュが有効活用されるので、インストールのステップをスキップし、ビルド速度が向上します。
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/app
クロスコンパイル用の環境変数を指定し、-ldflags="-w -s"
でバイナリを削減しています。
FROM scratch
前述のとおり scratch
イメージは、shell も入っていない最小のイメージです。
小さなイメージを作るには良いですが、attach も出来ないので取り回しが悪い点は否めません。
状況によって、Alpine Linux のイメージを使うと良いと思います。
COPY --from=builder /go/bin/app /go/bin/app
前のステージでビルドしたアプリケーションを次のステージにコピーしています。
--from=[NAME]
をコピーするファイルの前に宣言することでコピー元のステージが指定できます。
COPY --from=builder /etc/group /etc/group
COPY --from=builder /etc/passwd /etc/passwd
ここでは、ビルドステージからユーザーグループとユーザーのファイルをコピーしています。
このファイルをコピーすることで、ユーザーグループとユーザーを追加したことになるので、実行ユーザーを変更することが可能になります。
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ここでは、HTTPS 通信をするために証明書をコピーしています。
HTTPS 通信をしないのであれば不要です。
USER myapp
作成したユーザーを myapp
ユーザーに変更しています。
デフォルトでは root ユーザーで実行されるため、非 root ユーザーで実行するためにユーザーを変更しています。
ENTRYPOINT ["/go/bin/app"]
最後に ENTRYPOINT
でアプリケーションを実行しています。
ENTRYPOINT
にコマンドを指定することで、引数を用いるアプリケーションであれば実行時に引数がそのまま指定できます。
本番環境の実行
本来であれば CI でビルドし、CD でコンテナオーケストレーション環境にデプロイしますが、確認のために開発マシンで起動します。
Dockerfile をビルドします。
$ docker build -t myapp -f Dockerfile.prod .
8080 ポートでビルドしたコンテナを起動します。
$ docker run --name myapp -p 8080:8080 -it -d myapp
docker top コマンドで実行ユーザーを確認してみます。
$ docker top myapp
PID USER TIME COMMAND
13606 10001 0:00 /go/bin/app
USER
が 10001
で実行されていることが分かります。
番外編
本番環境の例では、ビルドステージにデフォルトのイメージを使用して作成しました。
ただし、デフォルトのイメージを採用することによるデメリットも存在します。
以下に示すのは、デフォルトの Go と Alpine Linux の Docker イメージを比較したものです。
golang 1.12.0-alpine3.9 d4953956cf1e 17 hours ago 347MB
golang 1.12.0 c4f8e4c91496 17 hours ago 772MB
見てわかるとおり 2 つのイメージには、400MB 程度のイメージサイズの差があります。
これはつまり、Docker イメージを pull するときのダウンロード速度に影響します。
同じベースイメージを使っていれば再利用されますが、Go のバージョンアップをするときにベースイメージを変更する必要があるため、その時点では全体のビルド速度が低下します。
ビルドステージに Alpine Linux のイメージを使用した Dockerfile の全体は以下のとおりです。
FROM golang:1.12.0-alpine3.9 as builder
WORKDIR /go/src/app
ENV GO111MODULE=on
RUN addgroup -g 10001 -S myapp \
&& adduser -u 10001 -G myapp -S myapp
RUN apk add --no-cache \
alpine-sdk \
git
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/app
FROM scratch
COPY --from=builder /go/bin/app /go/bin/app
COPY --from=builder /etc/group /etc/group
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
EXPOSE 8080
USER myapp
ENTRYPOINT ["/go/bin/app"]
変更点としては、イメージをデフォルトから Alpine Linux のイメージに変更しています。
FROM golang:1.12.0-alpine3.9 as builder
また、Debian 系と Alpine Linux とでディストリビューションが異なるため、ユーザーグループとユーザー追加のコマンドが異なります。
RUN addgroup -g 10001 -S myapp \
&& adduser -u 10001 -G myapp -S myapp
そして開発環境同様、alpine-sdk
と git
をインストールし、ファイルを全てコピーするように変更しています。
RUN apk add --no-cache \
alpine-sdk \
git
COPY . .
Alpine Linux を採用するメリットは、イメージサイズの小ささによるダウンロードの速さです。
毎度イメージのダウンロードからパッケージのインストールまで行うのであれば、デフォルトのイメージに比べイメージが小さい分速く終わります。
しかしながら番外編の Dockerfile にもデメリットがあり、本番環境の例で採用しなかったのは以下の理由からです。
Go Modules のインストールには、git
に依存しているため git
のインストールが必要です。
また、Go ライブラリのインストールや Go のビルドには gcc
などのパッケージを必要とする場合があるため、alpine-sdk
をインストールしています。
Alpine Linux のイメージにはそれらがデフォルトでインストールされていないため、事前にインストールする必要があります。
しかしそれらをインストールしてしまうと Docker のキャッシュがリセットされ、ソースコードを変更する度にパッケージのインストールが再度実行されてしまいます。
本番環境の例で紹介したデフォルトのイメージであれば git
や必要なパッケージがインストールされているので、Docker のキャッシュを有効活用してソースコードのコピーより前のステップをスキップできます。
したがって、毎度のソースコードの変更が主なビルドを考えると Docker のキャッシュが有効なデフォルトのイメージが良いと思います。
Go のライブラリを使用しないなど、パッケージをインストールしないのであれば、Alpine Linux を選択するメリットがあると思います。
さいごに
ここまで、Go 1.12 の開発環境と本番環境の Dockerfile の最適化を考えました。
次には Go 1.13 のリリースが控えていますが、Go Modules 自体は Go 1.13 からデフォルトで有効になるとのことなので、GO111MODULE
の環境変数が不要になる程度で概ね使えるのではないでしょうか。
また、Docker 18.09 で正式に追加された BuildKit
を試していないので、BuildKit
を使うことでより最適化できるかもしれません。
参考
Go v1.11 + Docker + fresh でホットリロード開発環境を作って愉快なGo言語生活
Create the smallest and secured golang docker image based on scratch
Non-privileged containers based on the scratch image
Use Multi-Stage Builds to Inject CA Certs