小さいサイズのDockerイメージを作成するときのベースイメージとしてalpineは有名だが、最近Googleが管理するdistrolessというイメージの存在を知ったので、既存のイメージをdistrolessに乗せ替えた時の作業記録。
TL;DR
debianベース(スタート地点)
FROM rust:latest as builder
WORKDIR /work
COPY . .
RUN cargo build --release
RUN strip /work/target/release/slice -o /slice
FROM debian:buster-slim
COPY --from=builder /slice /usr/local/bin/
ENTRYPOINT ["slice"]
distrolessベース(完成系)
FROM rust:latest as builder
RUN rustup target add "$(uname -m)"-unknown-linux-musl
WORKDIR /work
COPY . .
RUN cargo build --release --target "$(uname -m)"-unknown-linux-musl
RUN strip /work/target/"$(uname -m)"-unknown-linux-musl/release/slice -o /slice
FROM gcr.io/distroless/static
WORKDIR /
COPY --from=builder /slice /
USER nonroot
ENTRYPOINT ["/slice"]
導入
とある動機からRust製のコマンドラインツールを開発しており、簡単に使えるようにDockerfileも用意していた。
その際のベースイメージとしてdebianを採用していたが、ビルド後のイメージサイズが70MBほどあった。
それほど大したことをするわけでもないコマンドラインツールのために70MBもディスク容量を喰わせるのもいかがなものかとそんなことを考えていた折、alpineより小さいイメージの存在を知り、これは載せ替えるしかない!そんな思いで載せ替えを決行。
What is distroless
https://github.com/GoogleContainerTools/distroless
"Distroless" images contain only your application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution.
ということで、パッケージマネージャどころかshellさえ入っていない超軽量環境だそうだ。
スタート地点
ベースイメージにdebianを採用していた時のDockerfile
FROM rust:latest as builder
WORKDIR /work
COPY . .
RUN cargo build --release
RUN strip /work/target/release/slice -o /slice
FROM debian:buster-slim
COPY --from=builder /slice /usr/local/bin/
ENTRYPOINT ["slice"]
一応、小さくするための工夫として、マルチステージビルドを活用し実行時に不要なものを含まないようにし、さらにstripコマンドで余計なシンボル情報を削るようにはしているが、それでもビルド後のイメージサイズは次の通り70MB近くある。
REPOSITORY ... SIZE
slice ... 70.1MB
雑にベースイメージだけ変えてみる
まずは、雑にベースイメージのみを変更してみた。
FROM rust:latest as builder
WORKDIR /work
COPY . .
RUN cargo build --release
RUN strip /work/target/release/slice -o /slice
# ベースイメージを変更
FROM gcr.io/distroless/static
# 色々ないだろうからルートにコピーするように変更
COPY --from=builder /slice /
# 絶対パスに変更
ENTRYPOINT ["/slice"]
$ docker build -t slice .
$ docker run -i slice --version
exec /slice: no such file or directory
が、動かない。というかそんなものはないとさえ言われた。。。
しっかりコピーしているのでないわけは無い。。。おそらく、必要な物が色々と足りないのだろう。
もう少し大きいイメージに変えてみる
distroless/staticよりはもう少し色々入っているdistroless/baseに変えてみた。
FROM rust:latest as builder
WORKDIR /work
COPY . .
RUN cargo build --release
RUN strip /work/target/release/slice -o /slice
# static -> baseに変更
FROM gcr.io/distroless/base
COPY --from=builder /slice /
ENTRYPOINT ["/slice"]
$ docker build -t slice .
$ docker run -i slice --version
/slice: error while loading shared libraries: libgcc_s.so.1: cannot open shared object file: No such file or directory
今度は、そんなものは無いとまでは言われなかったが、libgcc_s.so.1が無いと言われた。
libcの入っているイメージに変える
今度は、先ほど足りないよ言われた共有ライブラリを含む、distroless/ccイメージに変えてみた。
FROM rust:latest as builder
WORKDIR /work
COPY . .
RUN cargo build --release
RUN strip /work/target/release/slice -o /slice
FROM gcr.io/distroless/cc
COPY --from=builder /slice /
ENTRYPOINT ["/slice"]
$ docker build -t slice .
$ docker run -i slice --version
slice 0.2.1
動いた!
サイズも、最初のことを思えば1/3程度まで小さくなった。
REPOSITORY ... SIZE
slice ... 23.6MB
が、
が、である。
もっと小さいのがあるならそっちが使いたい!
先ほど動かなかった原因は、共有ライブラリが足りなかったからであることは分かっている、であればビルド時に静的にリンクしてしまえば良いのではなかろうか?
静的リンクしたバイナリを作る
Rustで静的リンクをしてやるにはmusl版を利用すれば良いのでそれ用のステップを追加
FROM rust:latest as builder
# musl版を利用するためにツールチェインを追加
RUN rustup target add "$(uname -m)"-unknown-linux-musl
WORKDIR /work
COPY . .
# ビルド時のターゲットにmusl版を指定
RUN cargo build --release --target "$(uname -m)"-unknown-linux-musl
RUN strip /work/target/"$(uname -m)"-unknown-linux-musl/release/slice -o /slice
x86_64とaarch64のサポートをするために uname -m
で実行環境のアーキテクチャを取得している。
実際には次のように展開されている。
"$(uname -m)"-unknown-linux-musl
# x86_64
x86_64-unknown-linux-musl
# aarch64
aarch64-unknown-linux-musl
いざbase
流石に先ほど足りないと言われた共有ライブラリを要求しなくなった訳だから動くはずである。
FROM rust:latest as builder
RUN rustup target add "$(uname -m)"-unknown-linux-musl
WORKDIR /work
COPY . .
RUN cargo build --release --target "$(uname -m)"-unknown-linux-musl
RUN strip /work/target/"$(uname -m)"-unknown-linux-musl/release/slice -o /slice
FROM gcr.io/distroless/base
COPY --from=builder /slice /
ENTRYPOINT ["/slice"]
$ docker build -t slice .
$ docker run -i slice --version
slice 0.2.1
よし
REPOSITORY ... SIZE
slice ... 21.4MB
あれ?思ったより小さくならない??
改めてstatic
こちらはエラーメッセージから動かなかった理由が明確では無いので、少しチャレンジングだが、いざ!
FROM rust:latest as builder
RUN rustup target add "$(uname -m)"-unknown-linux-musl
WORKDIR /work
COPY . .
RUN cargo build --release --target "$(uname -m)"-unknown-linux-musl
RUN strip /work/target/"$(uname -m)"-unknown-linux-musl/release/slice -o /slice
FROM gcr.io/distroless/static
COPY --from=builder /slice /
ENTRYPOINT ["/slice"]
$ docker build -t slice .
$ docker run -i slice --version
slice 0.2.1
おお!!!!動いた!!!!
$ docker images
REPOSITORY ... SIZE
slice ... 3.39MB
おおお!!!こっちはサイズもかなり小さい!!!
結果
70.1MB -> 3.39MB まで約95%の容量削減に成功!
めでたしめでたし
比較(おまけ)
元々がdebianベースだったのでalpineベースのイメージも作ってサイズを確認してみた。
省略...
FROM alpine
COPY --from=builder /slice /
ENTRYPOINT ["/slice"]
$ docker images
REPOSITORY ... SIZE
slice ... 8.45MB
distroless/cc(23.6MB)
> distroless/base(21.4MB)
> alpine(8.45MB)
> distroless/static(3.39MB)
となった。
意外なことに、雑に使うならalpineの方が小さい
しかし、distroless/static
のサイズはそれの半分以下というのは驚きだ。
参考