LoginSignup
20
21

More than 5 years have passed since last update.

FROM scratchから始める軽量Docker image for Go

Last updated at Posted at 2017-12-10

はじめに

最近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

上記は,ベースイメージにそれぞれscratchalpine:3.6golang: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証明書が必要なケースなども合わせて紹介されているので,そのあたりで詰まっている方はぜひそちらを読んでみてください!

20
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
21