Carpe Diem

備忘録

ScratchイメージでGoアプリの超軽量イメージをビルド

概要

Alpineベースのイメージが軽量イメージとして認知されていますが、Goの場合は単一バイナリで動くのでイメージ内にgolangが入っている必要はありません。
なので最も軽量と言われているScratch imageを利用することで、dockerイメージを軽量化することができます。

環境

  • golang 1.8.3
  • Docker 17.06.0-ce

ビルド手順

対象ファイル

以下のファイルを扱うことにします。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("https://google.com")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println(len(body))
}

コンパイル

通常のビルドをすると以下のようになります。

$ go build -o main .
$ file main
main: Mach-O 64-bit executable x86_64

このままでは動かないので、scratchイメージ用にクロスコンパイルします。

$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o main main.go

CGO_ENABLED=0-installsuffix cgoをつけることでcgoの依存を排除しています。
通常のビルドだとOSのライブラリをdynamic linkするため、ビルド環境と実行環境が異なると上手く動きません。
なので異なる実行環境でも動くようにstatic linkにしておく必要があります。
-aは依存パッケージをリビルドするためのオプションで、これによって依存パッケージのcgo依存を排除しています。

このワークアラウンドは以下のissueで話されてます。

github.com

また本番環境用であれば-ldflags="-s -w"をつけることでよりファイルサイズを小さくすることも可能です。

こうしてビルドしたものは以下のようなバイナリになります。

$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Dockerfile

FROM scratch
ADD ca-certificates.crt /etc/ssl/certs/
ADD main /
CMD ["/main"]

ca-certificates.crtは内部でTLSリクエストをするケースで必要です。今回だとhttps://google.comにリクエストしているため必要になります。
ca-certificates.crtUbuntuなど一般的なディストリビューションであれば入っているので、その辺から用意しておきます。

以下のようにgithubca-certificates.crt付きのscratchイメージを用意してる人もいますが、古かったり不正な証明書を含む可能性があるためプロジェクトではUbuntuなどのディストリビューションから自分で抽出することをオススメします。

github.com

docker build

$ docker build -t example-scratch .

imageサイズを確認してみましょう。

$ docker images 
REPOSITORY           TAG                 IMAGE ID            CREATED              SIZE
example-scratch      latest              55c6d909ff46        13 seconds ago       5.69MB
example-alpine       latest              dc16f2d945c3        3 seconds ago        268MB

alpineが268MBに対し、scratchは5.69MBと非常に軽量になったことがわかります。

動作検証

$ docker run -it example-scratch
11875

問題なく動きました。

もし証明書が無いと、今回の場合以下のようなエラーが出ます。

Get https://google.com: x509: failed to load system roots and no roots provided

Docker multi stageを利用してビルド

docker 17.05から、multi-stageビルドが利用できます。
1つのDockerfileで複数のFROMイメージ間のファイルをCOPY --fromで直接を参照できるようになってます。

FROM golang:alpine AS build-env
ADD . /workspace
WORKDIR /workspace
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o main main.go

FROM scratch
ADD ca-certificates.crt /etc/ssl/certs/
COPY --from=build-env /workspace/main /main
CMD ["/main"]

これによって

  • ビルド環境にgolangがなくても、dockerさえ動けば軽量イメージが作成できる
  • 既存のイメージもscratchにバイナリを渡すだけでいいので手軽に移行できる

といったことが可能になります。

まとめ

Docker imageを軽量化することでデプロイ速度を上げることができます。
今回の対応で運用面のコスト改善であったり、オートスケール時の起動速度改善ができると思います。

ソース