はじめに
最近Goが流行ってますね. 自分は普段はScala派なのですが,やっぱりバイナリを作れるので小さいアプリを書くときはGoを使っています.(最近はScala NativeがあるのでScalaでもバイナリは作れなくもないですが...)
今回はGoでビルドしたバイナリを実行するDocker Imageを軽量に作るにはどうすればよいの?
という部分について紹介します.
ちなみに今回の記事は最終的には下記の記事を参考に,というかほぼそのまま試したものです.
興味のある方はこちらを読んでみてください!
あとさっき確認したらこちらの方が似た目的の記事を書かれていて,こちらの方が玄人向けで質も高いのでおすすめです.
本記事は上記の記事でいうところの
今度はGoでやってみます。ただし、Pure Goで最小というのはすでに方法があって、scratchという何も含まれないイメージを元に、静的リンクしたバイナリを配置するという方法です。
に当たる部分を解説したものになっています.あらかじめご承知置きを...
環境
- OSX 10.13.1
- Go 1.9.2 darwin/amd64
- Docker 17.09.0-ce
$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GORACE=""
GOROOT="/usr/local/Cellar/go/1.9.2/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.9.2/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/9j/m283k_6d00z12bhtwvr3b42d66h90f/T/go-build309557041=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
TL;DR
とりあえず方法さえわかればいい,という方も多いと思うのでまずは結論から.
基本的にやることは以下の2つです.
1. ベースイメージは scratch
にする.
FROM scratch
2. GoのバイナリはLinux向けに静的ビルドをする.
$ GOOS=linux CGO_ENABLED=0 go build main.go
これだけです. 簡単ですね!
方法
ここからは自分がハマったところから順を追って説明していきます.
ソースコード (Go)
かなり簡略化していますが,自分のプロジェクトでハマったところが1箇所だけあったので,それを示すために以下のようなコードにしています.
package main
import (
"time"
"net/http"
)
const layout = "2006-01-02 15:04:05"
func main() {
http.HandleFunc("/time", func(writer http.ResponseWriter, request *http.Request) {
l := request.URL.Query().Get("tz")
location, err := time.LoadLocation(l)
if err != nil {
panic(err)
}
writer.Write([]byte(time.Now().In(location).Format(layout)))
})
http.ListenAndServe(":8080", nil)
}
/time
を叩くと現在時刻を返してくれるHTTPサーバです.
Query Parameterでtzを指定するとそのタイムゾーンの時刻を返してくれます.
ビルド
初回は何も考えずに一旦ビルドしてみます. (うまく動かない例なのでご注意を)
$ go build -o clock main.go
Dockerfile
FROM scratch
COPY clock /clock
ENTRYPOINT ["/clock"]
ビルドしてできたバイナリをルートにCOPYしてENTRYPOINTで呼び出しているだけです.
軽量化するために必要なのはベースイメージをFROM scratch
にすることだけなんですねー
DockerHubにも書かれている通り,FROM scratch
はレイヤーが何もない空の状態をベースにしますよという意味です. よって基本的にはイメージのサイズはバイナリのサイズそのままになります.
念の為イメージのサイズを比べておきましょう.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
clock latest 95634a53e1c4 Less than a second ago 6.11MB
clock-alpine latest db323e49ed1c 12 seconds ago 10.1MB
clock-golang-alpine latest fc2bb6247afd 44 seconds ago 275MB
上記は,ベースイメージにそれぞれscratch,alpine:3.6,golang:1.9.2-alpine3.6を使ったものです.
今回のコードではバイナリのサイズが6.11MB
だったので, FROM scratch
のものはバイナリのサイズそのものになっていることが確認できます.
alpineも4MB
程度しかオーバーヘッドがなく評判通り軽いですね!
Go込みのイメージは271MB
と圧倒的に大きくなります.ただ気をつけて欲しいのが,Goはビルド時に必要なだけで,実行時には必要ないということです. つまり,実行するだけならこの約269MB
は無駄なんですねー.
いざ実行
ではいよいよ実行してみます.
$ docker run clock
standard_init_linux.go:185: exec user process caused "exec format error"
ダメでした^ ^;
これは単純にバイナリがOSX向けビルドになってしまっているからです.
OSX上にGoの開発環境をセットアップした場合,基本的にデフォルトではOSX向けのバイナリをビルドするような設定になっています.
逆に言うとlinux上でビルドしている場合は最初からlinux向けに設定されていることが多いので上記のようなエラーは出ていないかもしれません.
Linux向けビルドに直して再実行
このあたりを参考に GOOS=linux
を設定する必要があります.
$ GOOS=linux go build -o clock main.go
これでビルドしたバイナリを使ってイメージをビルドし,再実行してみます.
$ docker run clock
panic: open /usr/local/Cellar/go/1.9.2/libexec/lib/time/zoneinfo.zip: no such file or directory
ダメですか (T . T)
一部のライブラリは依存しているファイルがあるようで,これも一緒にCOPYしてやらないといけないようです. おそらく他のライブラリにもこういう依存があるものがあると思うので注意が必要です.
このあたりのコメントを見ればわかるのですが,今回のtime.LoadLocation
のケースだと$GOROOT/lib/time/zoneinfo.zip
を読みに行くようになっているようなので,Dockerfileを以下のように修正すれば解決しました.
FROM scratch
COPY clock /clock
ENV GOROOT /usr/local/go
ADD https://github.com/golang/go/raw/master/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip
ENTRYPOINT ["/clock"]
GOROOT
の設定とzoneinfo.zip
を追加しています.
このようなADDの使い方は,こちらにもある通り,実はDocker的にはあまり推奨されていないのですが,今回は見逃してください.
3度目の正直
最初に貼ったブログポストだと, CGO周りでLibraryがDynamic Linkされてしまってうまく動かんよと書いてあったのですが,自分の環境だとこれであっさり動きました.
$ docker run -p 8080:8080 clock
$ curl "localhost:8080/time?tz=Asia/Tokyo"
2017-12-11 02:17:05
実行したときに no such file or directory
が出てしまう場合は次のようにビルドしてstatic linkされるようにすると良いそうです.
$ GOOS=linux CGO_ENABLED=0 go build -a -o clock main.go
go envで見る限りだと,自分の環境もデフォルトはCGO_ENABLED=1
なのになぜ動いたのか...
最後に
今回はGoでビルドしたバイナリをscratchに乗っける例を紹介しました.
おそらく使ってるライブラリによっては,他にもハマりポイントがあるとは思うのですが一例として参考になれば幸いです!
最初に貼ったブログポストだとSSL証明書が必要なケースなども合わせて紹介されているので,そのあたりで詰まっている方はぜひそちらを読んでみてください!