コーヒーおかわり

データサイエンティストを目指す大学院生の日記。

目が点! すっごく小さいDockerイメージ! tweetコマンドをDockerを使って動くようにした

世の中のDockerイメージは大きすぎて、ダウンロードに時間がかかりすぎる!(某CM風)

Dockerイメージを作っていると悩まされるのがDockerイメージ(以下イメージ)のサイズです。イメージサイズが大きいとダウンロードやアップロードに時間がかかり、不便です。

そこで、テザリングでパソコンをインターネットに繋げているときにダウンロードしても、通信量・通信時間が気にならないような大きさのイメージを作りました。

https://hub.docker.com/r/kotaru/tweet/

その大きさはなんと、3MB!
"目が"点ですよ。メガだけに!

https://hub.docker.com/r/kotaru/tweet/tags/
3MBのイメージ!

はぁぁぁぁい!ずっと言いたくて言えなかったダジャレを言ったところで、このイメージを作った過程を説明します。なにをしたかをとっとと知りたい人はまとめを見ましょう。

tweet command written in Go

github.com

Go言語を覚えたてのころに作ったtweetコマンドですが、最近更新しました。Dockerを使って、Go言語の開発環境がなくても簡単に使えるようにしました。
なお、動かすためにはTwitterAPIキーが必要で、環境変数にセットする必要があります。詳しくは上記のリンクで。

tweetコマンドを作った背景

このコマンドを作った時は、まだ研究室にSlackが導入されていませんでした。サーバで長期実行プログラムが終わったときに、通知するいい方法がなかったので、このコマンドを作りました。
機能はシンプルで、「標準入力から受け取った文字列をそのままツイートする」だけです。案外便利なので今でも普通につぶやく時に自分は使ってます。後輩に変人だと言われてしまったが...w

(注意) 重要な情報はツイートしないようにしてくださいね

Docker multi stage build with scratch

さてさて、本題といきましょうか。
Docker 17.05からmulti stage buildという機能が追加されています。
詳細はリンク先を参照してください。

docs.docker.com

簡単に説明すると、アプリケーションをビルドするためのコンテナとアプリケーションを実行するコンテナを分離することができるという話です。
分割することで、ビルド時のみに必要なファイルをわざわざ消すシェルスクリプトを書かなくてすみ、綺麗で保守性の高いDockerfileをかけるようになります。 下記は私の作ったtweetコマンドをDocker上で動かすためのDockerfileです。

FROM golang:1.11.2-alpine3.8 as builder

RUN apk --update --no-cache add git ca-certificates
WORKDIR /go/src/github.com/kotaru23/tweet
COPY . .
RUN go get .
RUN CGO_ENABLED=0 GOOS=linux go build -o tweet


FROM scratch

COPY --from=builder /go/src/github.com/kotaru23/tweet/tweet /go/bin/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
CMD ["/go/bin/tweet"]

注目するべきは、3点あります。

  1. FROMが2回書かれている。そのうち1つには "as builder"と書かれている
  2. COPYの後に"--from=builder"と書かれている
  3. FROM scratch

説明

FROMが2回書かれていますが、これは決して間違いではありません。
1つ目のFROMから次のFROMまでの間では、tweetコマンドをソースコードからコンパイルしています。また、この後の工程のために"builder"という名前をコンテナにつけました。

FROM golang:1.11.2-alpine3.8 as builder

RUN apk --update --no-cache add git ca-certificates
WORKDIR /go/src/github.com/kotaru23/tweet
COPY . .
RUN go get .
RUN CGO_ENABLED=0 GOOS=linux go build -o tweet

2つ目のFROM以降は、アプリケーション実行用のイメージです。
1つ目のイメージを継承しているのではなく、全く別のイメージです。

FROM scratch

COPY --from=builder /go/src/github.com/kotaru23/tweet/tweet /go/bin/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
CMD ["/go/bin/tweet"]
COPY --from=builder "builderイメージのパス" "コピー先のディレクトリ"

とすることで、先ほど作成したbuilderイメージからファイルを持ってきています! Go言語で書かれたアプリケーションは、外部ライブラリ(C言語等)を使用していなければ、バイナリ1つをコピー&ペーストすれば実行できます。(バイナリは各OS, CPUのアーキテクチャごとにコンパイルする必要があります)
Go言語のライブラリ、ランタイムなどは全てバイナリの中に含まれているようです。 このtweetコマンドはGo言語で書かれていてバイナリとCA証明書があれば動くようになっているので、先のbuilderコンテナからバイナリとCA証明書を実行用コンテナに持ってきています。

FROM scratch

scratchとは、ほとんど何もないオフィシャルイメージです。/bin/shすらないようです。詳しくは下記リンク

https://hub.docker.com/_/scratch/

このイメージはファイルを他の場所からコピーしてきて、バイナリを実行するぐらいの用途にしか使えないのではないかと思います。JavaなどのVMを使用する言語で書かれたアプリケーションでは使えないかもしれません。しかし、外部依存のない純粋なGo言語で書かれたアプリケーションであれば、バイナリを持ってくるだけで実行できます。今回の例では通信をするためにCA証明書もビルド用のコンテナから持ってきていますが、それだけです。

まとめ

3MBのイメージを作るために必要な3つのこと

  • Go言語で書かれたアプリケーション(C言語の依存なし)
  • Docker multi stage build
  • scratchイメージ

あとがき

あ、"3"って数字使いすぎですかね。コンサルタントの真似ですw
それとBuildKitの話ができなかったのが残念です。

最後に言わせてほしい。

$ echo "純子ちゃん、やーらしか!" | \
docker run --rm -i \
    -e TWITTER_CONSUMER_KEY=$TWITTER_CONSUMER_KEY \
    -e TWITTER_CONSUMER_SECRET=$TWITTER_CONSUMER_SECRET \
    -e TWITTER_ACCESS_TOKEN_KEY=$TWITTER_ACCESS_TOKEN_KEY \
    -e TWITTER_ACCESS_TOKEN_SECRET=$TWITTER_ACCESS_TOKEN_SECRET \
    -e TWITTER_SCREEN_NAME=$TWITTER_SCREEN_NAME \
    kotaru/tweet