Go で書いたアプリを distroless 使って動かす方法
Motivation
- Go で書いたアプリを Docker で動かしたい場面は多々ある
- Docker イメージのサイズは小さければ小さいほど良い(諸説あり)
- 軽量 Docker イメージといえば alpine を使用したものが一般的だったが、昨今は distroless の方が良いらしい
- そのへんの理解を整理しつつ、 Go で書いたアプリを distroless で動かす所まで試したメモ
Organize my understanding
alpine ってなんだっけ?
- Alpine Linux をベースとした Docker イメージ
- Alpine Linux 自体が軽量な Linux ディストリビューションであり、 Docker イメージとして使用する場合も容量が小さいのが嬉しいポイントだった
なんで alpine は使わないほうがいいんだっけ?
- libc には一部互換性の不足がある1
- Native モジュールをバンドルしているアプリケーションで問題が発生する可能性がある
- python:3-slim, python:3-alpine3.6 イメージでベンチマークを動かした際に alpine の方がパフォーマンスが低い結果が出ている2
- Go なら、そもそも libc つかってなくない?
- net パッケージを使っている場合、デフォルトだと動的リンクで依存するようになっている4
- これは cgo パッケージが使われるようになったため
- cgo パッケージは Go から C のコードを呼び出せるようにするもの
- これは cgo パッケージが使われるようになったため
- ビルド時にオプションを指定することで無効化することはできるものの、おそらくパフォーマンスは低下する
- https://golang.org/src/net/net.go あたりのコメントを読んだ感じ、 cgo を使える場合には名前解決周りのパフォーマンスが向上しているようだ
- よって cgo を無効化する場合には、これらの恩恵は無くなる
- net パッケージを使っている場合、デフォルトだと動的リンクで依存するようになっている4
- 他ベースイメージも軽量化が進んでおり、イメージサイズの差がなくなってきた
Distroless は何がいいの?
- Distroless は Google がメンテナンスしている軽量 Docker イメージ
- Google が公式にメンテナンスしており、もちろん軽量
- Debian ベース
- 各プログラミング言語向けのイメージも提供している
- Docker イメージは grc.io でホストされている
- Google が公式にメンテナンスしており、もちろん軽量
- Go で作成したアプリを動かすなら distroless/base が良さそう
FROM golang:1.12 as build-env
WORKDIR /go/src/app
ADD . /go/src/app
RUN go get -d -v ./...
RUN go build -o /go/bin/app
FROM gcr.io/distroless/base
COPY --from=build-env /go/bin/app /
CMD ["/app"]
まぁマルチステージビルドを使っていて、 golang 公式イメージでバイナリをビルドして、ビルドしたバイナリを distroless/base にコピーして動かすという感じ
distroless/base には何が入っているの?
- README に書いてある
- distroless/static イメージがベースとなっている
- これは libc を必要としない static にコンパイルされた Go アプリケーション等で使える
- 以下のものが含まれている
- ca-certificates
- root ユーザー用の
/etc/passwd
エントリ /tmp
ディレクトリ- tzdata
- distroless/base
- libc/cgo を必要とする Go アプリなど、 distroless/static では不十分なほとんどのアプリケーションに使う
- distroless/static に含まれるものに加えて、以下のパッケージが含まれている
- glibc
- libssl
- openssl
ということで、基本は distroless/base を使えば良い
How to Run Go App with distroless
set up project
mkdir my-go-app && cd my-go-app
go mod init example.com/my-go-app
touch main.go
implementation go app
ここではシンプルに UUIDv4 を返すアプリとする
main.go
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
uuidv4, err := uuid.NewRandom()
if err != nil {
fmt.Println("err")
return
}
fmt.Println(uuidv4)
}
Docker 無しで普通に動かしてみる
$ go run main.go
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.2.0
81224ca2-780c-4dad-acb4-9acb366a7371
make Dockerfile
touch Dockerfile
Dockerfile
FROM golang:1.15.8-buster as gobuilder
WORKDIR /go/src/app
COPY go.mod go.sum ./
RUN go mod download
COPY main.go ./
RUN go build -ldflags="-w -s" -o /go/bin/app
FROM gcr.io/distroless/base
COPY --from=gobuilder /go/bin/app /
CMD ["/app"]
- distroless が debian ベースなので、念の為 golang:xxx-buster を使ってビルドしている
go mod download
Go Modules で管理している依存関係をモジュールキャッシュにダウンロードする5
go build -ldflags=“-w -s”
-ldflags
: go tool link の起動時に渡される引数6"-w -s"
: go tool link のオプション7- go tool link は main パッケージの Go アーカイブまたはオブジェクトを、依存関係とともに読み込んで、実行可能なバイナリに結合するツール
-w
: DWARF のシンボルテーブルを省略する- DWARF (ドワーフ) は、デバッグ用のデータフォーマットのこと
-s
: シンボルテーブルとデバッグ情報を省略する
自分の理解では、 Go アプリを Linux 向けにビルドすると ELF 形式のバイナリが生成され、ビルドの際に上記オプションを指定することで、生成されるバイナリのサイズを小さくできるものと理解している
docker build & run
docker build my-go-app .
docker run my-go-app
実行例
$ docker build -t my-go-app .
Sending build context to Docker daemon 5.12kB
Step 1/10 : FROM golang:1.15.8-buster as gobuilder
1.15.8-buster: Pulling from library/golang
0ecb575e629c: Pull complete
7467d1831b69: Pull complete
feab2c490a3c: Pull complete
f15a0f46f8c3: Pull complete
2ea92ed63b96: Pull complete
223c5fc7af76: Pull complete
764ae8cdbfbc: Pull complete
Digest: sha256:56e443b088657df2d9ff891b043aead11aedd94f8413959a93364af22564d6d7
Status: Downloaded newer image for golang:1.15.8-buster
---> 7185d074e387
Step 2/10 : WORKDIR /go/src/app
---> Running in 36e0f7e7225f
Removing intermediate container 36e0f7e7225f
---> 4590930d6d64
Step 3/10 : COPY go.mod go.sum ./
---> afbb960dc817
Step 4/10 : RUN go mod download
---> Running in 4a14cb2323a0
Removing intermediate container 4a14cb2323a0
---> 2a4a4045965a
Step 5/10 : COPY main.go ./
---> c6c18189789f
Step 6/10 : RUN go generate
---> Running in aa34e2d9aa29
Removing intermediate container aa34e2d9aa29
---> 881d69b323fa
Step 7/10 : RUN go build -ldflags="-w -s" -o /go/bin/app
---> Running in afc76bbeebc7
Removing intermediate container afc76bbeebc7
---> 9e98cf748f86
Step 8/10 : FROM gcr.io/distroless/base
---> a4cf6da932ac
Step 9/10 : COPY --from=gobuilder /go/bin/app /
---> e506aa225384
Step 10/10 : CMD ["/app"]
---> Running in a0de36104140
Removing intermediate container a0de36104140
---> 9f0084a99b9e
Successfully built 9f0084a99b9e
Successfully tagged my-go-app:latest
$ docker images my-go-app
REPOSITORY TAG IMAGE ID CREATED SIZE
my-go-app latest 9f0084a99b9e 2 minutes ago 20.9MB
$ docker run my-go-app
a8c80ca7-08c0-4b62-af48-afe39d11e130
試した環境
$ uname -a
Linux ip-172-31-255-5.ap-northeast-1.compute.internal 4.14.231-173.361.amzn2.x86_64 #1 SMP Mon Apr 26 20:57:08 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2"
ID="amzn"
ID_LIKE="centos rhel fedora"
VERSION_ID="2"
PRETTY_NAME="Amazon Linux 2"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
HOME_URL="https://amazonlinux.com/"
$ docker --version
Docker version 19.03.13-ce, build 4484c46
$ go version
go version go1.15.8 linux/amd64
- 軽量Dockerイメージに安易にAlpineを使うのはやめたほうがいいという話 - inductor’s blog [return]
- performance - Why is the Alpine Docker image over 50% slower than the Ubuntu image? - Super User [return]
- Comparison of C/POSIX standard library implementations for Linux [return]
- golangで書いたアプリケーションのstatic link化 - okzkメモ [return]
- Go Modules Reference - The Go Programming Language [return]
- go - The Go Programming Language [return]
- link - The Go Programming Language [return]