諸事情で欲しかった。
動機
- gooseというGoで書かれたcliのDBマイグレーションツールを使っているのだが、こいつのバイナリだけを持っているDockerイメージが欲しい
- GoにもAlpine Linuxベースの軽量なイメージがある。しかしそれでも240MBくらいある
- ベースイメージはGoでなくていいので、gooseのバイナリをシュッと実行ディレクトリに配置した軽いイメージが欲しい
といったところ。ちなみにAlpine Linuxはもともと組込系用途で利用されていたディストリビューションで、最近ではDockerイメージのベースディストリビューションとして広く利用されてます。
Alpine glibc問題
Alpine Linuxを駆使してDockerイメージのダイエットに腐心されてる人ならわかると思いますけど、GoのバイナリをビルドしてAlpineコンテナ上で実行させるにはglibc問題を突破しなくてはならない。glibc問題によって何が起こるかというと、go getで取得してきたバイナリをシュッとAlpineベースのイメージに放り込んでも以下のようなエラーで実行できないというもの。
standard_init_linux.go:178: exec user process caused "exec format error"
Alpine glibc問題についてはちょくちょくWeb上で散見されて、皆が通るハマりどころみたいな感じになってます。
glibc がリンクされた binary を alpine 上で動かしたい - Qiita
Alpine用のバイナリを錬成する
というわけで作ったのでどうやってるか説明。調理器具としてDockerとCircleCIを使う。
ビルド用のイメージを作る
まず、ビルド用のDockerfileを要してgo getしてgooseのバイナリを錬成するためだけのDockerイメージを作る。ベースイメージはgolangの公式イメージでAlpineベースのものを選べば手堅いだろうという考え。
Dockerfile.buildという名で用意してある。
FROM golang:1.7.4-alpine
MAINTAINER stormcat24 <stormcat24@stormcat.io>
RUN apk update && \
apk add --virtual build-dependencies build-base git && \
go get bitbucket.org/liamstask/goose/cmd/goose && \
apk del build-dependencies && \
rm -rf /var/cache/apk/*
リポジトリをアップデートし、ビルドに必要なものを取得し、bitbucketからgooseをgo getしてます。
次に、ベースイメージとして利用するgolang:1.7.4-alpineの全容を確認しておきます。
RUN set -ex \
&& apk add --no-cache --virtual .build-deps \
bash \
gcc \
musl-dev \
openssl \
go \
\
&& export GOROOT_BOOTSTRAP="$(go env GOROOT)" \
\
&& wget -q "$GOLANG_SRC_URL" -O golang.tar.gz \
&& echo "$GOLANG_SRC_SHA256 golang.tar.gz" | sha256sum -c - \
&& tar -C /usr/local -xzf golang.tar.gz \
&& rm golang.tar.gz \
&& cd /usr/local/go/src \
&& patch -p2 -i /no-pic.patch \
&& patch -p2 -i /17847.patch \
&& ./make.bash \
\
&& rm -rf /*.patch \
&& apk del .build-deps
着目して欲しいのはapk addでインストールしているmusl-dev。muslはCライブラリでAlpine Linuxではglibcではなくmuslを標準としてます。ちなみにAlpine Linux3.4ではapkからglibcをインストールすることも可能。 ちなみに前にCircleCI上でビルドしたGoのバイナリを動かすために、Alpineでゴリゴリっとglibcをインストールして実行するっていうのをやってた。今はそこまで苦労はしない。最終的にはAlpineでビルドしたバイナリを、別のAlpineのコンテナで動かすので今回はglibcの存在は気にせずビルドする。
錬成したバイナリをコンテナから取得する
ここからはCircleCIの出番。Alpineでビルドしたバイナリは、当然Dockerコンテナ内に閉じられており、これを取り出す作業を行う。
dependencies:
override:
- "docker build -f Dockerfile.build -t $CIRCLE_PROJECT_USERNAME/goose-build:latest ."
- "docker run $CIRCLE_PROJECT_USERNAME/goose-build:latest sleep 10":
background: true
- "docker cp `docker ps | grep goose-build | cut -f1 -d' '`:/go/bin/goose ."
- ls -l
手順を解説すると、
- バイナリビルド用のDockerイメージをビルドする
- 出来上がったイメージをrun。常駐するようなコンテナではないので、sleepで適当な秒数指定してwaitさせる
- sleepはバックグラウンドで実行させる
- sleep中に、docker cpを使ってコンテナからgooseのバイナリをホスト(ここではCircleCIのコンテナ)へコピーする
これでgooseのバイナリをコンテナから取り出せた。
実行用のイメージを作る
取り出したバイナリを実行用のイメージにシュッと放り込むことで最終成果物の完成。
これが本命のDockerfile
FROM gliderlabs/alpine:3.4
MAINTAINER stormcat24 <stormcat24@stormcat.io>
COPY goose /usr/local/bin
このDockerイメージをCircleCI上で生成し、DockerHubにPushしてる。
使ってみる
DockerHubに上がってるのでdocker runで以下のようにピロッと実行できる。
docker run stormcat24/goose:latest goose --help
goose is a database migration management system for Go projects.
Usage:
goose [options] <subcommand> [subcommand options]
Options:
Commands:
-env string
which DB environment to use (default "development")
-path string
folder containing db info (default "db")
-pgschema string
which postgres-schema to migrate (default = none)
up Migrate the DB to the most recent version available
down Roll back the version by 1
redo Re-run the latest migration
status dump the migration status for the current DB
create Create the scaffolding for a new migration
dbversion Print the current version of the database
イメージサイズ
ビルド用イメージと、実行用イメージとではこれくらいの差ができた。ビルド用はもちろんGoがまるまる入ってるので300MB近くなった。実行用イメージの方は約17MBに?
$ docker images | grep goose
stormcat24/goose latest 30aba3bf1d17 56 minutes ago 16.8 MB
stormcat24/goose-build latest 1c1103cadeb1 2 hours ago 299 MB
ビルド用のイメージから産業廃棄物を片っ端から削除する方法もあるが、かなりめんどくさいのでこれでいいと思う。
まとめ
ビルド用イメージ+CircleCI(別にCircleCIじゃなくてもいい)のあわせ技になるが、目的の軽量なイメージはこれで作れた。
本来はビルド、実行含めて1枚のDockerfileで完結することがポータビリティ的には良いわけだけど、ポータビリティとイメージの軽さはトレードオフな面があるので、ビルド用イメージをかますことで軽さとポータビリティ(COPYしてるので100点満点ではないが)をある程度確保できたと思ってる。