Dockerfileを調整して、rustを使ったイメージのビルドを高速にする。
起きていたこと
Docker上でRustアプリを動かそうとするとビルドにすごく時間がかかっていた。15分程度はかかることもあった。
原因と対策
出力を見るに、依存関係のコンパイルに長いこと時間がかかっているためだとわかった。
Dockerfileの仕組みの把握
Dockerfileの調整について調べると、コードなどをどのくらい頻繁に書き換えるかでRUNを分割するとイメージが再利用されやすくなるとのこと:

Optimize cache usage in builds
An overview on how to optimize cache utilization in Docker builds.
依存関係よりも自前のコードのほうが書き換える頻度は高いので、依存関係のビルドと、自前コードのビルドを異なるRUNで分ければキャッシュが効くようになる。
cargo-chefの利用
これを可能にしてくれるクレートがあるので、そのクレート、cargo-chefをDockerfileで導入して使うことにした:
GitHub - LukeMathWalker/cargo-chef: A cargo-subcommand to speed up Rust Docker builds using Docker layer caching.
A cargo-subcommand to speed up Rust Docker builds using Docker layer caching. - LukeMathWalker/cargo-chef
書き換えたDockerfileはほぼcargo-chefのREADMEにあるものと同様なので、省略。
イメージの共通化
違うバイナリがほしいだけでDockerfileを分けると、2重にビルドが走る。これを防ぐため1つのDockerfileですべてのバイナリをビルドし、イメージにASで名前を付与しつつ、必要なバイナリをCOPYする。
# cargo-chefを利用した依存関係のビルドを先に行う
# バイナリは一括でビルドして、後続で必要なバイナリのみ取り出す
# ...
# バイナリA用のイメージ
FROM alpine:latest AS runtime-a
COPY --from=builder /app/target/release/binary_a /usr/local/bin/app
ENTRYPOINT ["/usr/local/bin/app"]
# バイナリB用のイメージ
FROM alpine:latest AS runtime-b
COPY --from=builder /app/target/release/binary_b /usr/local/bin/app
ENTRYPOINT ["/usr/local/bin/app"]
docker composeであれば、targetを指定して、どのイメージを利用するか指定する。
services:
service-a:
build:
context: .
dockerfile: Dockerfile
target: runtime-a # runtime-a ステージまでをビルド
image: my-app-a
service-b:
build:
context: .
dockerfile: Dockerfile
target: runtime-b # runtime-b ステージまでをビルド
image: my-app-b
これで、重複処理を大幅にカットできるはず。
結果
自前コードの更新だけなら、大雑把に言って毎回のビルドに、1000秒以上かかっていたところ2回目以降のビルドは、10分の1に短縮できた。正直もっと短縮できると思ってたので意外。
とりあえず満足。ストレージが危うくなるようなことがあればイメージは削除しよう。
以上です。

