RustのさまざまなDockerfileのビルド時間を比較してみた

# はじめに

Rustで書かれたアプリケーションをコンテナイメージとしてビルドする際にDockerfileの記述方法にいくつか書き方があったので、それぞれについてイメージのビルド時間の比較してみました。

# 背景

というのも、Rustはアプリケーションのビルド時間が長いためDockerfileの書き方によって開発効率を悪くしてしまう場合があります。

例えば、Rustで作ったアプリケーションをKubernetesで動かす場合を想定します。ローカルでのアプリケーション開発に[skaffold](https://github.com/GoogleContainerTools/skaffold)を利用するとします。
skaffoldはコードの変更を検知し、イメージの再ビルドを行い、Kubernetes上にアプリケーションを再デプロイします。
このとき、Rust特有のビルド時間が長い問題にぶち当たります。ビルド時間がながいことによって、コードの変更に対してイメージの再ビルドが追いつかないのです。

上記は一例ですが、どうせならビルドの速いDockerfileを書きたいよねっということで、いくつか比較してみました。

# 方法

今回は、コードの変更によって生じるイメージのビルド時間を測定し、それらを比較します。ビルドイメージサイズに関しては特に気にしてません。またcargo workspaceでの開発も想定していません。

`serde`と`rocket`を使ったアプリを想定します。`Cargo.toml`、`main.rs`は下記のようにしています。

```toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rocket = { git = "https://github.com/SergioBenitez/Rocket" }
```

```rust
#[macro_use] extern crate rocket;

#[get("/")]
fn hello() -> &'static str {
    "Hello, before build!"
}

#[launch]
fn rocket() -> rocket::Rocket {
    rocket::ignite().mount("/", routes![hello])
}
```

一度イメージのビルドを実行した後、コードを変更後イメージの再ビルドを行います。イメージとしては、下記のようなスクリプトになります。変更前と変更後のコードは上記の`hello()`関数の返り値が異なります。

```shell
rm -rf src && cp -r testsrc/before src
docker build -q -f Dockerfile.base -t rust-docker-base .
rm -rf src && cp -r testsrc/after src
time docker build -q -f Dockerfile.base -t rust-docker-base .
```

[Github](https://github.com/mkazutaka/rust-dockerfile-comparison)上に各Dockerfileとスクリプトを置いています。

# 対象

### 1. base

最も基本的なDockerfileです。

```dockerfile
FROM rust:1.48.0

WORKDIR /app

COPY . .
RUN cargo build --release

ENTRYPOINT ["/app/target/release/app"]
```

### 2. echo

[Fast + Small Docker Image Builds for Rust Apps](https://shaneutt.com/blog/rust-fast-small-docker-image-builds/)で紹介されているイメージのビルド方法です。
アプリケーションのビルドキャッシュをつくるために一度テンポラリなmain.rsを作成してます。

```dockerfile
# https://shaneutt.com/blog/rust-fast-small-docker-image-builds/
FROM rust:1.48.0

WORKDIR /app
COPY Cargo.toml Cargo.toml
RUN mkdir src/
RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs
RUN cargo build --release
RUN rm -f target/release/deps/app*

COPY . .
RUN cargo build --release

ENTRYPOINT ["/app/target/release/app"]
```

### 3. cargo-chef

[cargo-chef](https://github.com/LukeMathWalker/cargo-chef)を使ったイメージのビルド方法です。

```dockerfile
# https://github.com/LukeMathWalker/cargo-chef
FROM rust as planner
WORKDIR app
# We only pay the installation cost once,
# it will be cached from the second build onwards
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare  --recipe-path recipe.json

FROM rust as cacher
WORKDIR app
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

FROM rust as builder
WORKDIR app
COPY . .
# Copy over the cached dependencies
COPY --from=cacher /app/target target
COPY --from=cacher $CARGO_HOME $CARGO_HOME
RUN cargo build --release

ENTRYPOINT ["/app/target/release/app"]
```

### 4. BuildKit + base

対象1のDockerfileに加えて、[BuildKit](https://docs.docker.com/develop/develop-images/build_enhancements/)を使用します。

```dockerfile
FROM rust:1.48.0

WORKDIR /app

COPY . .

RUN --mount=type=cache,target=/usr/local/cargo/registry \
	--mount=type=cache,target=/app/target \
	cargo build --release

ENTRYPOINT ["/app/target/release/app"]
```

### 5. BuildKit + sccache

BuildKitを使用していますが、あくまでキャッシュするのはsccacheで保存しているものに限定しています。

```dockerfile
FROM rust:1.47.0

RUN cargo install sccache

ENV HOME=/app
ENV SCCACHE_CACHE_SIZE="1G"
ENV SCCACHE_DIR=$HOME/.cache/sccache
ENV RUSTC_WRAPPER="/usr/local/cargo/bin/sccache"

WORKDIR $HOME

COPY . .

RUN --mount=type=cache,target=/app/.cache/sccache cargo build --release

ENTRYPOINT ["/app/target/release/app"]
```

# 結果

以下のPCで計測しました。念の為、ビルド実行する前に`docker system prune -f -a`を実行しました。

```
macOS Catalina v10.15.7
MacBook Pro (16-inch, 2019)
Processor 2.4GHz 8-Core Intel Core i9
Memory 64GB 2667 MHz DDR4
```

結果は下記のようなものになりました。

```
Time of Dockerfile.base
       87.27 real         2.86 user         0.86 sys
Time of Dockerfile.echo
       21.49 real         2.86 user         0.85 sys
Time of Dockerfile.cargochef
       18.40 real         2.86 user         0.84 sys
Time of Dockerfile.buildkit-base
       14.09 real         0.12 user         0.06 sys
Time of Dockerfile.buildkit-sccache
       34.50 real         0.16 user         0.08 sys
```

順番を入れ替えてもだいたい同じ結果になりました。

```
Time of Dockerfile.buildkit-base
       15.63 real         0.15 user         0.08 sys
Time of Dockerfile.cargochef
       18.81 real         3.05 user         0.92 sys
Time of Dockerfile.echo
       22.82 real         3.00 user         0.92 sys
Time of Dockerfile.buildkit-sccache
       35.12 real         0.16 user         0.09 sys
Time of Dockerfile.base
       90.60 real         2.93 user         0.89 sys
```


# 感想

今回の実験だと、BuildKitを使うと速いですね。プライベートだと、`echo`のDockerfileを使っていたのでちょっと置き換えて本当に速いか試してみたいと思います(置き換えれるか)。
[emk/rust-musl-builder](https://github.com/emk/rust-musl-builder)を使った場合どうなるのかとかもで試したいですね。

他にもこういうのもあるよっていうのがありましたら、ぜひコメントかGithubのIssueかPRください！
今回のコード類は、[mkazutaka/rust-dockerfile-comparison](https://github.com/mkazutaka/rust-dockerfile-comparison)にあります。

### ちなみに

私が使用しているRocketフレームワークを使っている最終的なアプリケーションのDockerfileです。
skaffoldのv1.17.2はBuildkit動かないので([skaffold Issue #5178](https://github.com/GoogleContainerTools/skaffold/issues/5178))、bleeding edge buildをつかってください。
musl遅いよって記事([Why does musl make my Rust code so slow?](https://andygrove.io/2020/05/why-musl-extremely-slow/))を見てからベースイメージにdebianを使うようにしています

```dockerfile
FROM debian:buster-slim as runner

RUN apt update; apt install -y libssl1.1

FROM rust:1.48.0 as builder

WORKDIR /usr/src

RUN rustup target add x86_64-unknown-linux-musl

COPY Cargo.toml Cargo.lock ./
COPY src ./src

RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/usr/src/target \
    cargo install --path .

FROM runner

COPY --from=builder /usr/local/cargo/bin/myapp .
COPY Rocket.toml .

USER 1000

CMD ["./myapp"]
```

# 参考

- [Cache Rust dependencies with Docker build - Stack Overflow](https://stackoverflow.com/questions/58473606/cache-rust-dependencies-with-docker-build)
- [cargo build --dependencies-only · Issue #2644 · rust-lang/cargo](https://github.com/rust-lang/cargo/issues/2644)
- [Rust - Fast + Small Docker Image Builds](https://shaneutt.com/blog/rust-fast-small-docker-image-builds/)
- [LukeMathWalker/cargo-chef: A cargo-subcommand to speed up Rust Docker builds using Docker layer caching.](https://github.com/LukeMathWalker/cargo-chef)
- [benmarten/sccache-docker-test](https://github.com/benmarten/sccache-docker-test)
- [Build images with BuildKit | Docker Documentation](https://docs.docker.com/develop/develop-images/build_enhancements/)
- [How to Package Rust Applications Into Minimal Docker Containers · alexbrand's blog](https://alexbrand.dev/post/how-to-package-rust-applications-into-minimal-docker-containers/)