Cloud RunでRustのYewのブログを配信しているのですが、Docker imageが1GBを超えていたので大幅に削減してみました
Docker imageが1.3GB
このブログをNext.jsからRustに入れ替えてからしばらくの間時間を取ることができなかったので、特別な対応を取らずにリリースしていた結果、Docker imageが1.3GBと非常に大きいサイズに膨れ上がっておりました
Rustのオフィシャルイメージは大きいことが知られていますが、さすがにこの大きさだとDocker imageを保存するコストや実行されるまでのスピードに影響があると思い、削減するためのアクションを取り始めました
Rustに限らずバイナリを生成して実行することができる場合、Dockerで複数のステージを構築して、実行時はバイナリだけを含めた軽量のイメージを作成できるみたいなので段階的に分けていきます
実行ステージをdebianへ
とりあえずdebianを実行ステージで利用して、バイナリだけを含める実装を行います
※開発時のメモ書きなので不足箇所や不要箇所が含まれているかもしれません
FROM rust:1.70.0 as builder
RUN apt-get update && apt-get install -y make g++ binaryen
RUN rustup target add wasm32-unknown-unknown
RUN cargo install --locked trunk
RUN cargo build --release
RUN make ssr_build
# 実行 Stage
FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y libgcc1 libstdc++6 bash
EXPOSE 8080
COPY --from=builder /usr/ssr_server/dist/ /dist/
COPY --from=builder /tmp/target/release/simple_ssr_server /simple_ssr_server
ENTRYPOINT ["./simple_ssr_server"]
CMD ["--dir", "dist"]
こちらのDockerfileを実行すると46.3MBとなり、1.3GBと比較すると約3.5%まで削減できました
(46.3 / (1.3 * 1024) = 0.03478)
debianはlsコマンドや必要な機能群が含まれており、比較的大きなイメージではありますが、エラーが発生することもなく簡単に実装できるのかなり効果的でした
alpineでさらに小さく
さらにDocker imageを小さくするためにalpineに変更します
FROM rust:1.70.0-alpine as builder
RUN apk add --no-cache build-base npm binaryen
WORKDIR /usr
COPY . .
RUN rustup target add x86_64-unknown-linux-musl
RUN rustup target add wasm32-unknown-unknown
RUN cargo install --locked trunk
RUN cargo build --release --target x86_64-unknown-linux-musl
WORKDIR /usr/crates/ssr_server
RUN trunk build --release -d ./dist
RUN cp robots.txt ./dist/robots.txt
RUN cargo build --release --target x86_64-unknown-linux-musl --features=ssr --bin simple_ssr_server --
FROM alpine:latest
RUN apk add --no-cache libgcc libstdc++ ca-certificates
EXPOSE 8080
COPY --from=builder /usr/crates/ssr_server/dist/ /dist/
COPY --from=builder /usr/target/x86_64-unknown-linux-musl/release/simple_ssr_server /simple_ssr_server
ENTRYPOINT ["./simple_ssr_server"]
CMD ["--dir", "dist"]
alpineを実行ステージのimageに設定するとのDockerfileを実行すると、6.9MBまで削減でき、1.3GBと比較すると約0.5%まで小さくできたということになります
(6.9 / (1.3 * 1024) = 0.00518)
ちなみにですが、alpineに変更するタイミングでopenssl関連の動的リンク問題が発生して2週間ほどハマるという大失態を犯してしまいました
この記事の下部に記載しましたので、興味があればご覧ください
完成版Dockerfile、scratchで最小化
こちらが完成版のDockerfileです
scratchという最小のイメージでバイナリだけを設置したDockerfileです
FROM rust:1.70.0 as builder
RUN apt-get update && apt-get install -y binaryen musl-tools && rm -rf /var/lib/apt/lists/*
WORKDIR /usr
COPY . .
RUN rustup target add x86_64-unknown-linux-musl
RUN rustup target add wasm32-unknown-unknown
RUN cargo install --locked trunk
RUN cargo build --release --target x86_64-unknown-linux-musl
WORKDIR /usr/crates/ssr_server
RUN trunk build --release -d ./dist
RUN cp robots.txt ./dist/robots.txt
RUN cargo build --release --target x86_64-unknown-linux-musl --features=ssr --bin simple_ssr_server --
# Runtime Stage
FROM scratch
EXPOSE 8080
COPY --from=builder /usr/crates/ssr_server/dist/ /dist/
COPY --from=builder /usr/target/x86_64-unknown-linux-musl/release/simple_ssr_server /simple_ssr_server
ENTRYPOINT ["/simple_ssr_server"]
CMD ["--dir", "dist"]
scratchにバイナリを含めるだけなので2.6MBまで削減でき、1.3GBと比較すると約0.2%の大きさになりました
(2.6 / (1.3 * 1024) = 0.00195)
小さくすることは正義👍
動的リンク問題
問題の確認
実行ステージのイメージをalpineに変更したタイミングでDockerのbuildはできるけど実行するとアプリが落ちるという問題が発生しました
cargo run --release --target x86_64-unknown-linux-musl --features=ssr --bin simple_ssr_server -- --dir dist
// 実行時のエラー ↓
Compiling anymap2 v0.13.0
error: failed to run custom build command for `openssl-sys v0.9.93`
調べてみると動的リンクの問題が発生しているようです
動的リンクとはプログラムを実行する際に、外部のライブラリとリンクされる方式を指す用語のことです
なので、alpineに動的リンクされているライブラリを含めるか動的リンクを行わないという選択肢があります
※確か静的リンクにしてください、的なメッセージが出て静的にした気がします。間違っていたらすみません、、、、
動的リンクされているか確認
まずは本当に動的リンクされているかを確認するためにDockerfileの中にlddコマンドを追加して、依存ライブラリがあるのかどうか?を検証します
RUN ldd /usr/src/app/target/x86_64-unknown-linux-musl/release/simple_ssr_server
実行したところopenssl関連の動的リンクが発生していることを確認できました
/lib/ld-musl-x86_64.so.1 (0x7f00695ed000)
libssl.so.3 => /lib/libssl.so.3 (0x7f0068f50000)
libcrypto.so.3 => /lib/libcrypto.so.3 (0x7f0068b97000)
libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f00695ed000)
opensslがどこで利用されているかを確認
Rustには依存関係を表示してくれるtreeというコマンドがあるので実行みます
どうやらreqwestの中にopensslが含まれるようですね
cargo tree --target all
├── infrastructure v0.0.0
│ ├── reqwest v0.11.20
│ │ ├── hyper-tls v0.5.0
│ │ │ ├── bytes v1.5.0
│ │ │ ├── hyper v0.14.27 (*)
│ │ │ ├── native-tls v0.2.11
│ │ │ │ ├── lazy_static v1.4.0
│ │ │ │ ├── libc v0.2.148
│ │ │ │ ├── log v0.4.20
│ │ │ │ ├── openssl v0.10.57 ← ここに入ってる
│ │ │ │ │ ├── bitflags v2.4.0
│ │ │ │ │ ├── cfg-if v1.0.0
│ │ │ │ │ ├── foreign-types v0.3.2
│ │ │ │ │ │ └── foreign-types-shared v0.1.1
│ │ │ │ │ ├── libc v0.2.148
│ │ │ │ │ ├── once_cell v1.18.0
│ │ │ │ │ ├── openssl-macros v0.1.1 (proc-macro)
│ │ │ │ │ │ ├── proc-macro2 v1.0.67 (*)
│ │ │ │ │ │ ├── quote v1.0.33 (*)
│ │ │ │ │ │ └── syn v2.0.33 (*)
│ │ │ │ │ └── openssl-sys v0.9.93
│ │ │ │ │ └── libc v0.2.148
│ │ │ │ │ [build-dependencies]
│ │ │ │ │ ├── cc v1.0.83 (*)
│ │ │ │ │ ├── pkg-config v0.3.27
│ │ │ │ │ └── vcpkg v0.2.15
│ │ │ │ ├── openssl-probe v0.1.5
│ │ │ │ ├── openssl-sys v0.9.93 (*)
opensslの問題解消
調査してみると、reqwestのfeatureを変更することでopensslの問題を解決できるようなので、Cargo.tomlに記載されているreqwestを下記のコードのように修正します
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json"] }
修正完了後にtreeコマンドを実行するとopensslからrusttlsに切り替わっており、buildして実行してもエラーが起きませんでしたので、問題を解消することができました
cargo tree --target all
├── infrastructure v0.0.0
│ ├── reqwest v0.11.20
│ │ ├── hyper-rustls v0.24.1
│ │ │ ├── futures-util v0.3.28 (*)
│ │ │ ├── http v0.2.9 (*)
│ │ │ ├── hyper v0.14.27 (*)
│ │ │ ├── rustls v0.21.7
│ │ │ │ ├── log v0.4.20
│ │ │ │ ├── ring v0.16.20
│ │ │ │ │ ├── libc v0.2.148
│ │ │ │ │ ├── once_cell v1.18.0
│ │ │ │ │ ├── spin v0.5.2
│ │ │ │ │ ├── untrusted v0.7.1
│ │ │ │ │ ├── web-sys v0.3.64
│ │ │ │ │ │ ├── js-sys v0.3.64
│ │ │ │ │ │ │ └── wasm-bindgen v0.2.87
│ │ │ │ │ │ │ ├── cfg-if v1.0.0
│ │ │ │ │ │ │ └── wasm-bindgen-m
まとめ
- マルチステージビルドで大幅なDocker imageサイズ削減が可能
- opensslの問題は他のプロジェクトでも起こり得るので、覚えとくと良いかも
- SSR用のaxumなので、FEではなくBE寄りの内容だった
コメントを残す