go getで取得したcliツールのバイナリを持った軽量なDockerイメージをつくる

諸事情で欲しかった。

動機

  • gooseというGoで書かれたcliのDBマイグレーションツールを使っているのだが、こいつのバイナリだけを持っているDockerイメージが欲しい
  • GoにもAlpine Linuxベースの軽量なイメージがある。しかしそれでも240MBくらいある
  • ベースイメージはGoでなくていいので、gooseのバイナリをシュッと実行ディレクトリに配置した軽いイメージが欲しい

といったところ。ちなみにAlpine Linuxはもともと組込系用途で利用されていたディストリビューションで、最近ではDockerイメージのベースディストリビューションとして広く利用されてます。

index | Alpine Linux

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点満点ではないが)をある程度確保できたと思ってる。