Golangのシングルバイナリをいい感じでDocker Image化する

author potpro(ぼとぷろ)
2019/05/29

Golangのシングルバイナリをいい感じでDocker Image化する

Golangはシングルバイナリ(単体で動く実行ファイル)を出力してくれてありがたいのですが、それをコンテナ向けの軽量LinuxであるalpineやBusyBoxを使って動かそうとすると、ハマることが多い。

シングルバイナリじゃなかったのか、と思ってしまうのだが、どうやらデフォルトで一部のライブラリがダイナミックリンクになっているからである。

このあたりが参考になった。

golangでビルドしたバイナリをスタティックリンク (static link)にする

これ、割とググればいろいろ出てくるのですが結局のところイマイチ最適解が見つからない。

何故かっていうと、中身に入っているライブラリなどで割かしビルド条件を変える必要があるため。

また、コンテナのサイズも小さくしたいので、いろいろとオプションを調べて、自分はこんな感じで落ち着いた。

※ちなみに今回想定しているシングルバイナリは、Golangで書かれたWeb Rest APIアプリケーションです。

go build

go build -a -tags "netgo" -installsuffix netgo -ldflags="-s -w -extldflags \"-static\"" -o=/build/app main.go

問題はldflagsという引数で、これはgccが使うldの引数である。makeでのインストールなんかに慣れている人は結構なじみがあるみたいだが、自分はあまり知らなかった。 まあ簡単に言えばどのようにリンクするかの設定だ。

-extldflags "-static"で、ビルドしたバイナリをスタティックリンクにする。こうすることで外部ライブラリを使う挙動をしなくなる。

-sオプションを追加する。-sオプションはシンボルテーブルとデバッグ情報を省略することで容量を削減する。

もう一つ、-wオプションはDWARFのシンボルテーブルを省略。何ぞやと思ったけどこれもデバック的な情報らしい。

-a -tags netgo -installsuffix netgoは、Golangのnetパッケージを使用している場合必要になります。Webサーバソフトウェアはほぼ必須。

必要な理由として、この指定が無いとnetパッケージがダイナミックリンクになってしまうかららしく、実際抜くとAlpineなどでは動かなくなります。

元々tagsを使っていた場合は、複数指定できるみたいです。 -tags "netgo test"という感じ。

これで動く!と思いがちなのだが、これでも動かない事が多い。 いくらシングルバイナリと言っても、結局Linux上で動いているしLinuxシステムに依存する設定や必要なパッケージを別途入れる必要があったりする。

Alpine Linux

シングルバイナリの実行用としてAlpine Linuxを使用している。Alpineは軽量LinuxとしてDockerで人気のイメージであり、

A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size

と書いてあるように5MBしかない。自分はこのAlpine Linuxが好きで結構な頻度で使っている。

ただ、やっぱりUbuntuやCentOSと比べると入っているもの自体が何もなさ過ぎて、この環境でビルドなんかをするのはかなり難易度が上がったりする。

というより、bashが入ってなかったりするので普通に使うのも割と簡単ではない。

Alpine LinuxでのHHVMのコンパイルはマジで無理過ぎてあきらめた。

で、足りないものの最低限としてこの2つのパッケージを入れた。

FROM alpine:3.9.4
# Timezone = Tokyo
RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

tzdataはタイムゾーン情報。 特にタイムゾーンはちゃんとAsia/Tokyoにしておかないと後々変なバグを引き起こしそう。GMT+0のイギリスに住んでいる人は問題ないと思いますが。

ca-certificatesはCAの認証局情報。HTTPS通信でCA証明書を要求されるので必要。 alpineにはlibresslは入っているっぽいのでhttps行けると思いきやこれが無いと駄目になることがある。

Webサーバアプリケーションならこれで最低限かな?という感じでした。

Dockerfile

軽量なDocker Imageを作るには、Multi-stage buildを使い、ビルドしたものをAlpineなどの軽量Linuxで動かすのがベスト。

最終的なDockerfileはこんな感じ。当然規模にもよると思うがWeb APIサーバはこれで10MBで収まった。Alpineで5MB、Goバイナリで5MBという感じ。10MBでWeb APIサーバが動くというのは割と衝撃だ・・・ (Docker ImageなのでLinuxカーネルは別ですけど)

Go modules対応させるために少し変更。

FROM golang:1.12.9-alpine3.10 AS build-env

ENV GO111MODULE=on

RUN apk --no-cache add git make build-base

WORKDIR /go/src/app

COPY . .

RUN mkdir -p /build
RUN go build -a -tags "netgo" -installsuffix netgo -ldflags="-s -w -extldflags \"-static\"" -o=/build/app main.go

FROM alpine:3.10.2
# Timezone = Tokyo
RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

COPY --from=build-env /build/app /build/app
RUN chmod u+x /build/app

ENTRYPOINT ["/build/app"]

こんな感じ。しかし容量削減はLinuxの上のレイヤーを知らないといけないので勉強になる。 Alpineを使っているとUbuntuやCentOSの大体揃っていることへのありがたみがひしひしと伝わります。