Hori Blog

フリーランスでバックエンドエンジニアとして活動している Ryota Hori のブログです。
最近はテック系記事より雑記ブログ気味。

Docker in Docker で Go 製のバイナリを持った軽量な Docker イメージを作る

go get で取得した cli ツールのバイナリを持った軽量な Docker イメージをつくる - tehepero note(・ω<) を読んで、別解として Docker in Docker で作れないかなーと思ってやってみました。

やりたいこと

根本的な動機は元記事をご参照ください。

go get で取得した cli ツールのバイナリを持った軽量な Docker イメージをつくる - tehepero note(・ω<)

元記事では Alpine ベースの golang イメージで go get したバイナリを docker cp で取得し、実行用のイメージを作成するフローを CircleCI で実行しています。

この記事では CI を用いずに Docker in Docker なビルド用のイメージを用いることでコンテナ内で go get から docker build まで済ませてみようかと思います。

なお、 docker push せずに成果物のイメージを取り出したかったので、今回は Docker のデーモンを起動せずにホストの /var/run/docker.sock をコンテナと共有します。

実行用のイメージ

先に実行用イメージ作成の構成を説明したほうがわかりやすいので、ビルド用イメージの解説の前にぺろっと紹介。

ディレクトリ構成はこんな感じです。

directory-structure
Dockerfile
pre.sh*

pre.sh で下準備をして、 Dockerfiledocker build する想定です。

今回は goose のバイナリを入れたいので、下準備は go get です。

pre.sh は以下。

pre.sh
#/bin/bash

go get -v bitbucket.org/liamstask/goose/cmd/goose
cp $GOPATH/bin/goose ./goose

goose のバイナリがカレントディレクトリに配置されます。

Dockerfile はバイナリをコピーするだけ。

Dockerfile
FROM alpine:3.5

COPY ./goose /usr/local/bin/

ENTRYPOINT goose

この実行用のイメージのビルドを Alpine ベースの Docker コンテナ内で実行するのが目標です。

ビルド用のイメージ

Docker のイメージ作成は依存系の解決と実行用のアレコレ配置を別イメージにするのが好きなので、二段階でやります。

( CI 等、キャッシュがない環境で依存系のビルドが何度も走るとしんどいという理由)

Docker in Docker with golang のイメージ

下準備で Docker と golang の入った Docker イメージを用意します。

今回はバージョンにこだわらずに Docker のベースイメージに apk で取れる go を入れます。(記事作成時点では 1.7.3 でした)

もし最新バージョンの golang を入れたい場合は golang の公式イメージの Dockerfile を参考にするのがいいかと。

Dockerfile
FROM docker

RUN \
      apk --no-cache --update add \
      build-base \
      git \
      go \
      && \
      mkdir /go

ENV GOPATH /go
ENV PATH $PATH:$GOPATH/bin

こちらを

docker build -t horiryota/docker-golang .

として下準備。

ビルド用のイメージ

本題です。

ビルド用イメージ作成用のディレクトリはこんな感じです。

directory-structure-for-build-image
build.sh*
Dockerfile
usage.sh*

核となる build.sh

build.sh
#/bin/bash

srcDir='/srcDir'

if [ -z "$IMAGE_NAME" ]; then
  echo '$IMAGE_NAME unbound'
  usage.sh
  exit 1
fi

if [ ! -d "${srcDir}" ]; then
  echo 'srcDir not found'
  usage.sh
  exit 1
fi

if [ ! -r "${srcDir}/Dockerfile" ]; then
  echo 'Dockerfile not found'
  usage.sh
  exit 1
fi

if [ ! -r "${srcDir}/pre.sh" ]; then
  echo 'pre.sh not found'
  usage.sh
  exit 1
fi

cp -R /srcDir/* ./

./pre.sh && \
  docker build -t $IMAGE_NAME .

です。

つらつら書いている if 文は必要なものが存在するかのチェックです。

最終的にはビルド用イメージを doker run する際に /srcDirDockerfilepre.sh の入ったディレクトリをマウントすることで、 pre.sh を実行した後に docker build する構成です。

mount する /srcDir を荒らさないよう、ファイルは ./ にコピーしています。

usage.sh はイメージの使い方が出力されるだけのスクリプトなので割愛。

Dockerfile は以下です。

Dockerfile
FROM horiryota/docker-golang

WORKDIR /workdir

COPY usage.sh /usr/local/bin/
COPY build.sh /usr/local/bin/

ENTRYPOINT "build.sh"

build.sh を発火するだけです。

WORKDIR /workdir にしているのは / ディレクトリにある Dockerfile で docker build をしようとすると権限問題で失敗するためです。

ビルド用イメージのビルドをして準備完了です。

docker build -t horiryota/docker-golang-builder .

作成実行

実行コマンドはこんな感じ。

docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock -v $DOCKERFILE_DIR:/srcDir:ro -e IMAGE_NAME=$IMAGE_NAME horiryota/docker-golang-builder

今回は /var/run/docker.sock を mount したのでホストの docker にイメージが生成されます。

  • $DOCKERFILE_DIR 実行用イメージ作成用のファイルがあるディレクトリ。カレントディレクトリであれば $(pwd) でできるかなと
  • $IMAGE_NAME 作成したいイメージ名。 build.shdocker build -t $IMAGE_NAME で使うのでお好きに。

ちなみに /srcDir の mount で後ろについている :ro は read only の意味です。ホストのディレクトリを不用意に荒らさないで済むので付けておくことを推奨します。

ということで、カレントディレクトリで horiryota/docker-goose のイメージを作成するならこんな感じでしょうか。

docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/srcDir:ro -e IMAGE_NAME=horiryota/docker-goose horiryota/docker-golang-builder

生成イメージの確認

REPOSITORY              TAG     IMAGE ID      CREATED            SIZE
horiryota/docker-goose  latest  fa633023a994  About an hour ago  17.4 MB

諸々のバージョンが違うので同じにはなりませんでしたが、およそ元記事と同じくらいのイメージサイズになったことが確認できました。

今回のコード

使ったコードを公開しておきます。

以上

Docker のコンテナ内で docker build することで Alpine ベースの軽量イメージが作成できました。

Docker in Docker で準備用のスクリプトと Dockerfile を実行するのが主旨なので、 golang のバイナリ用以外にも汎用的にビルドに使えるようにできたんじゃないかなーと思っています。

ちなみに Docker ベースのイメージを使わなくともホストの /usr/bin/docker をコンテナに mount するという手もあるようですが荒業感が強すぎるので止めました( Docker for Mac だと /usr 以下を mount しようとすると怒られる模様)。

コンテナ内で Docker のデーモンを立ち上げれば /var/run/docker.sock を mount しなくても docker push までコンテナ内で完結できるはずなので、必要にかられたら試してみます。