Docker イメージ 最小化計画

小さいサイズのDockerイメージを作成するときのベースイメージとしてalpineは有名だが、最近Googleが管理する**distroless**というイメージの存在を知ったので、既存のイメージをdistrolessに乗せ替えた時の作業記録。

## TL;DR

### 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"]
```

### distrolessベース(完成系)
```dockerfile
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"]
```

## 導入
[とある動機](https://qiita.com/ChanTsune/items/64d322ddbd3f27a24570)から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
```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近くある。

```s
REPOSITORY ...       SIZE
slice      ...       70.1MB
```

## 雑にベースイメージだけ変えてみる

まずは、雑にベースイメージのみを変更してみた。
```dockerfile
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"]
```
```shell
$ docker build -t slice .
$ docker run -i slice --version
exec /slice: no such file or directory
```
が、動かない。というかそんなものはないとさえ言われた。。。

しっかりコピーしているのでないわけは無い。。。おそらく、必要な物が色々と足りないのだろう。

## もう少し大きいイメージに変えてみる
distroless/staticよりはもう少し色々入っているdistroless/baseに変えてみた。
```dockerfile
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"]
```
```shell
$ 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イメージに変えてみた。
```dockerfile
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"]
```
```sh
$ docker build -t slice .
$ docker run -i slice --version
slice 0.2.1
```
動いた！

サイズも、最初のことを思えば1/3程度まで小さくなった。
```s
REPOSITORY ...       SIZE
slice      ...       23.6MB
```
が、
が、である。
もっと小さいのがあるならそっちが使いたい！

先ほど動かなかった原因は、共有ライブラリが足りなかったからであることは分かっている、であればビルド時に静的にリンクしてしまえば良いのではなかろうか？

## 静的リンクしたバイナリを作る
Rustで静的リンクをしてやるにはmusl版を利用すれば良いのでそれ用のステップを追加

```dockerfile
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` で実行環境のアーキテクチャを取得している。
実際には次のように展開されている。
```sh
"$(uname -m)"-unknown-linux-musl
# x86_64
x86_64-unknown-linux-musl
# aarch64
aarch64-unknown-linux-musl
```

## いざbase
流石に先ほど足りないと言われた共有ライブラリを要求しなくなった訳だから動くはずである。

```dockerfile
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"]
```
```shell
$ docker build -t slice .
$ docker run -i slice --version
slice 0.2.1
```
よし

```s
REPOSITORY ...       SIZE
slice      ...       21.4MB
```

あれ？思ったより小さくならない？？

## 改めてstatic
こちらはエラーメッセージから動かなかった理由が明確では無いので、少しチャレンジングだが、いざ！

```dockerfile
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"]
```

```shell
$ docker build -t slice .
$ docker run -i slice --version
slice 0.2.1
```
おお！！！！動いた！！！！

```s
$ docker images
REPOSITORY ...       SIZE
slice      ...       3.39MB
```
おおお！！！こっちはサイズもかなり小さい！！！
## 結果

70.1MB -> 3.39MB まで約95%の容量削減に成功！

めでたしめでたし

## 比較(おまけ)
元々がdebianベースだったのでalpineベースのイメージも作ってサイズを確認してみた。
```dockerfile
省略...

FROM alpine

COPY --from=builder /slice /

ENTRYPOINT ["/slice"]
```

```s
$ 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`のサイズはそれの半分以下というのは驚きだ。

###### 参考

https://zenn.dev/tkithrta/articles/7ca7ab166bbac8

https://github.com/GoogleContainerTools/distroless

https://zenn.dev/yoshii0110/articles/21ddb58c6f6bfa

https://vlike-vlife.netlify.app/posts/golang_and_rust_statically_linked