molecular coordinates

/var/log/coord_e.log

Dockerイメージ縮小オタク

おはようございます!!!@coord_eです、よろしくどうぞ。

動機

dockerイメージが小さくなると→嬉しい!*1

一般的なテク

multi-stage build

docs.docker.com

ビルド環境とか最終的なイメージには必要ないんで...ビルドした後、必要なものだけ最終的なイメージに引っ張ってきます。

upx

upx.github.io

バイナリ詰めるマン。なんでかバイナリサイズが1/10とかになる*2。普通に怖い

スム〜ズに本題に入るためにまずシングルバイナリを否定します。シングルバイナリを否定してもいいですか?シングルバイナリがかわいそうですが...

シングルバイナリ作るのしんどくないですか?しんどいですね。みんながみんなGoを書いているわけではないので...

実は、シングルバイナリを作らず極小dockerイメージを作る方法があるんです!*3

実行に必要なものをリストアップしよう

シェル芸をな*4

大体の場合*5*6*7*8、実行に必要なものは実行ファイルそれ自体と動的リンクされたライブラリ、そして動的リンカ(プログラムインタプリタ)です*9。前者はldd(1)で、後者はreadelf(1)でそれぞれ取得します。

{ \
      echo "/path/to/your/executable"; \
      readelf -l "/path/to/your/executable" \
        | grep "program interpreter" \
        | sed -e 's/^.*: \(.*\)\]$/\1/'; \
      ldd "/path/to/your/executable" \
        | awk -F'=>' '{print $2}' \
        | sed -e 's/(.*)//' -e '/^\s*$/d' \
        | awk '{$1=$1};1'; \
}

おそらくリンクが混ざっているので、次のコマンドにパイプして*10リンク元とリンク先を両方ともリストに加えてやります。

xargs -I{} bash -c "echo {}; readlink -f {};"

これで実行に必要なファイルのリストができました!あとはこれを次のコマンドにパイプして、全ての必要なファイルを一つのディレクトリに詰めます。

xargs -I{} cp -r --parents {} /bundle

いいですね。では†次のステージ†へ...

FROM scratch
COPY --from=0 /bundle/ /.

必要なものは全部 /bundle/ に入ってるので、ベースイメージはscratchです。 COPY でさっき/bundleにコピーしたファイル達を / に展開して...終了!

実例

hlintというHaskellのLintツールのイッミジを作ってみます。Haskell製のツールで、シングルバイナリを作るのがちょっとめんどくさい*11、なので今回の食材にぴったり。...では、出来上がったDockerfileがこちらです!

FROM haskell:8

RUN cabal new-update

# install
ARG INSTALL_DIR=/usr/bin
RUN cabal new-install hlint-2.2.11 --installdir "${INSTALL_DIR}" --install-method copy

# prepare for compression
WORKDIR /tmp
ADD https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz upx.tar.xz
RUN tar --strip-components=1 -xf upx.tar.xz && mv upx /usr/bin/

# compress executable
RUN cp "${INSTALL_DIR}/hlint" /tmp/hlint_copy
RUN upx -q -9 --brute "${INSTALL_DIR}/hlint"

# collect runtime dependencies
RUN { \
      echo "${INSTALL_DIR}/hlint"; \
      echo "$(which git)"; \
      readelf -l /tmp/hlint_copy \
        | grep "program interpreter" \
        | sed -e 's/^.*: \(.*\)\]$/\1/'; \
      { ldd /tmp/hlint_copy; ldd $(which git); } \
        | awk -F'=>' '{print $2}' \
        | sed -e 's/(.*)//' -e '/^\s*$/d' \
        | awk '{$1=$1};1'; \
    } | xargs -I{} bash -c "echo {}; readlink -f {};" \
      | xargs -I{} cp -r --parents {} /bundle

# copy
FROM scratch
COPY --from=0 /bundle/ /.

WORKDIR /work
CMD ["${INSTALL_DIR}/hlint"]

hlintには--gitオプションがあって、git管理対象のファイルのみにlintを行うといったことができます。そこそこ便利なので、--gitオプションが使えるように上のDockerfileではgitを同梱しています。このように必要なものをザクザク足していくことも、できるんですね(小並感)。でも、イメージサイズは?はい、こちらになります!

$ docker image ls
...
coorde/hlint    2.2.11   662fad71d2d6    4 weeks ago    13.6MB
...

小さめで嬉しいですね。今回作ったcoorde/hlintはボクがバイト先で作っているソフトウェアのCI上でブンブン働いています。

まとめ

参考文献

*1:よかったですね

*2:やりすぎると常に255を返すハリボテになることがあるので、オプションは-9ぐらいで止めておくといい

*3:いかがでしたか?

*4:Dockerfileはシェル芸が正当化できるので好きです

*5:要出典

*6:なぜ?

*7:誰が?

*8:いつ?

*9:他に必要なものがあったらこのリストに追加していけばいいです

*10:xargs→bash -cコンボ嫌いなんですけどもっといい方法知りませんか?

*11:hlintのリリース、普通に動的リンクされたバイナリで配布されててそういうのもあるんだってなった