【翻譯】爲Rust應用快速地構建體積小的鏡像

原文地址html

這篇文章我會介紹如何快速爲Rust應用體積小的Docker鏡像。我會從建立一個小的測試應用開始,而後不斷構建迭代Dockerfilelinux

環境要求

首先確保你已經安裝了下面的應用:git

起步: 建立demo應用

使用rustup進行設置,確保使用最新穩定版的Rust。github

rustup default stable
rustup update 
複製代碼

建立一個myapp的新項目docker

cargo new myapp
cd myapp/ 
複製代碼

起步: 初始化Dockerfile

如下是咱們用於docker構建的起點,在當前目錄中建立一個名爲Dockerfile的文件:shell

FROM rust:latest

WORKDIR /usr/src/myapp COPY . . RUN cargo build --release RUN cargo install --path . CMD ["/usr/local/cargo/bin/myapp"] 複製代碼

一樣建立一個.dockerignore的文件寫入如下內容:緩存

target/
Dockerfile 
複製代碼

嘗試構建並運行應用:bash

docker build -t myapp .
docker run --rm -it myapp 
複製代碼

若是一切都能工做的話,你能夠在控制檯看到Hello, world!app

咱們初次構建的問題

當我寫這篇文章的時候, Rust的包管理器cargo有一個issue是它尚未一個dependencies-only的選項,用來單獨構建依賴工具

cargo缺乏這樣單獨構建依賴的選項使得咱們在每次改動src下面的內容時都會從新構建依賴項,但咱們只想在Cargo.toml或者Cargo.lock文件改變時才從新構建依賴項,比方說添加或者更新依賴。

另外一個問題是,雖然rust:latest Docker鏡像很是適合構建,但它的體積至關大,超過1.6GB。

改進構建流程避免src改動從新構建依賴項

爲了不這些問題而且開啓docker構建緩存讓構建變得更快,首先咱們開始改動Cargo.toml來添加一個依賴:

[package]
name = "myapp"
version = "0.1.0"
 [dependencies]
rand = "0.5.5" 
複製代碼

咱們添加rand包做爲項目依賴,它提供了方便地生成隨機數的工具。

如今若是運行:

docker build -t myapp .
複製代碼

它將構建rand依賴關係並將其添加到緩存,可是更改src/main.rs將使得生成的緩存無效:

cat src/main.rs
fn main() {
    println!("I've been updated!");
}
docker build -t myapp . 
複製代碼

請注意,這次構建必須再次重建rand依賴項。

在等待cargo的only-dependencies構建選項時,將任何代碼複製到構建環境以前,咱們能夠經過將Dockerfile更改成默認的src/main.rs來解決此問題:

FROM rust:latest

WORKDIR /usr/src/myapp 
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/myapp* 
COPY . . 
RUN cargo build --release 
RUN cargo install --path . 
CMD ["/usr/local/cargo/bin/myapp"] 
複製代碼

上面的Dockerfile中的如下幾行將致使Cargo構建時僅從新構建咱們的應用程序:

RUN rm -f target/release/deps/myapp*
複製代碼

若是咱們構建一次:

docker build -t myapp .
複製代碼

而後對src/main.rs作一點小小的改動:

cat src/main.rs
fn main() {
    println!("I've been updated yet again!");
}
複製代碼

咱們將會發現接下來docker只會在咱們的應用邏輯改變時從新構建,而依賴項目則被緩存起來用來快速構建。

減少鏡像體積

rust:latest鏡像具備構建項目所需的全部工具,但它的大小超過1.6GB。咱們可使用Alpine Linux(一種出色的小型Linux發行版)來改善鏡像大小。

Alpine團隊提供了一個只有幾兆字節大小的docker映像,而且仍然具備一些用於調試的shell功能,而且能夠用做Rust構建的小型基礎鏡像。

經過多階段docker構建,咱們可使用rust:latest來完成構建工做,只需將應用複製到基於alpine:latest的最終構建階段便可:

# ------------------------------------------------------------------------------
# Cargo Build Stage
# ------------------------------------------------------------------------------

FROM rust:latest as cargo-build

WORKDIR /usr/src/myapp 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/myapp* COPY . . RUN cargo build --release RUN cargo install --path . # ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------

FROM alpine:latest

COPY --from=cargo-build /usr/local/cargo/bin/myapp /usr/local/bin/myapp CMD ["myapp"] 複製代碼

如今若是你運行:

docker build -t myapp .
docker images |grep myapp 
複製代碼

你能夠看到這些東西:

myapp   latest    03a3838a37bc    7 seconds ago    8.54MB
複製代碼

下一步:跟進、修復並進一步完善咱們的構建

若是你嘗試使用docker run --rm -it myapp運行以上示例,則可能會遇到相似下面的錯誤:

standard_init_linux.go:187: exec user process caused "no such file or directory"
複製代碼

若是您熟悉ldd則能夠運行如下命令,以查看咱們缺乏的應用程序共享庫:

docker run --rm -it myapp ldd /usr/local/bin/myapp
複製代碼

在上面的例子中我演示瞭如何經過避免每次src/main.rs改動從新構建依賴提高構建速度,以及如何將鏡像大小從1.6GB+減小到幾兆字節,然而咱們的構建仍是不能生效,由於咱們須要針對MUSL Libc進行構建,這是一個輕量級、快速的標準庫,在alpine:latest中是默認庫。

除此以外,咱們還但願確保咱們的應用程序以容器內的非特權用戶身份運行,從而遵照最小特權原則

爲MUSL Libc構建

要針對MUSL libc進行構建,咱們須要安裝x86_64-unknown-linux-musl構建目標,以即可以將Cargo標記爲使用--target爲其構建。咱們還須要標記Rust使用musl-gcc連接器。

rust:latest鏡像預安裝rustup。 rustup容許咱們使用rustup target add $NAME安裝新的構建目標,所以咱們能夠像這樣修改Dockerfile:

# ------------------------------------------------------------------------------
# Cargo Build Stage
# ------------------------------------------------------------------------------

FROM rust:latest as cargo-build

RUN apt-get update 
RUN apt-get install musl-tools -y 
RUN rustup target add x86_64-unknown-linux-musl 
WORKDIR /usr/src/myapp 
COPY Cargo.toml Cargo.toml 
RUN mkdir src/ 
RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs 
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl 
RUN rm -f target/x86_64-unknown-linux-musl/release/deps/myapp* 
COPY . . 
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl 
# ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------

FROM alpine:latest

COPY --from=cargo-build /usr/src/myapp/target/x86_64-unknown-linux-musl/release/myapp /usr/local/bin/myapp 
CMD ["myapp"] 
複製代碼

請注意如下行,它顯示了咱們爲MUSL Libc構建應用程序的新方式:

RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-mus
複製代碼

從新構建應用程序並運行它:

docker build -t myapp .
docker run --rm -it myapp 
複製代碼

若是一切正常,你應該再次看到應用已被更新了!

以非特權用戶身份運行

爲了遵循最小特權原則,咱們建立一個名爲myapp的用戶,避免用戶以root用戶的身份運行應用。

將Final Stage docker build階段更改成如下內容:

# ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------

FROM alpine:latest

RUN addgroup -g 1000 myapp 
RUN adduser -D -s /bin/sh -u 1000 -G myapp myapp 
WORKDIR /home/myapp/bin/ 
COPY --from=cargo-build /usr/src/myapp/target/x86_64-unknown-linux-musl/release/myapp . 
RUN chown myapp:myapp myapp 
USER myapp

CMD ["./myapp"] 
複製代碼

更新src/main.rs爲:

use std::process::Command;

fn main() {
    let mut user = String::from_utf8(Command::new("whoami").output().unwrap().stdout().unwrap();
    user.pop();
    println!("I've once more been updated, and now I run as the user {}!", user)
} 
複製代碼

如今構建並運行應用:

docker build -t myapp .
docker run --rm -it myapp 
複製代碼

若是一切正常,你應該會看到應用已再次更新,如今應用以用戶myapp運行。

最後

如今咱們構建應用程序的完整Dockerfile以下所示:

# ------------------------------------------------------------------------------
# Cargo Build Stage
# ------------------------------------------------------------------------------

FROM rust:latest as cargo-build

RUN apt-get update 
RUN apt-get install musl-tools -y 
RUN rustup target add x86_64-unknown-linux-musl 
WORKDIR /usr/src/myapp 
COPY Cargo.toml Cargo.toml 
RUN mkdir src/ 
RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs 
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl 
RUN rm -f target/x86_64-unknown-linux-musl/release/deps/myapp* 
COPY . . 
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl 
# ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------

FROM alpine:latest

RUN addgroup -g 1000 myapp 
RUN adduser -D -s /bin/sh -u 1000 -G myapp myapp 
WORKDIR /home/myapp/bin/ 
COPY --from=cargo-build /usr/src/myapp/target/x86_64-unknown-linux-musl/release/myapp . 
RUN chown myapp:myapp myapp 
USER myapp

CMD ["./myapp"] 
複製代碼

從這裏觀看有關使用Skaffold在DC / OS上將Rust部署到Kubernetes的演示。利用該演示中的一些技術,你能夠將應用程序自動部署到Kubernetes,以使用Skaffold在本地minikube系統上進行測試。

Happy coding!

相關文章
相關標籤/搜索