构建最精简的 Rust Docker 镜像

2022-06-30 16:52:53 浏览数 (1)

本文摘选自 Sylvain Kerkour(Bloom.sh 站点的创建者和《黑帽 Rust(Black Hat Rust)》一书作者)的文章 How to create small Docker images for Rust。

构建最精简的 Docker 映像,以用来部署 Rust,将会带来很多益处:不仅有利于安全(减少攻击面),而且还可以缩短部署时间、降低成本(减少带宽和存储),并降低依赖项冲突的风险。

Rust 代码

我们的“应用”相当简单:将构建一个简单的命令行实用程序,用来调用 https://api.myip.com,并打印响应结果。

进行 HTTPS 调用很有趣,因为它需要一个库来与 TLS(通常使用 openssl)交互。但是,为了构建尽可能精简的 Docker 映像,我们需要对我们的程序做静态链接,而 openssl 的静态链接并不是那么容易实现。所以,本文我们将避免使用 openssl,而改用 Rust 生态库 rustls

让我们暂时忽略 Jemalloc

代码语言:javascript复制
cargo new myip

Cargo.toml

代码语言:javascript复制
[package]
name = "myip"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1", features = ["derive"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] }


[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator]
version = "0.3"

main.rs

代码语言:javascript复制
use serde::Deserialize;
use std::error::Error;

// Use Jemalloc only for musl-64 bits platforms
#[cfg(all(target_env = "musl", target_pointer_width = "64"))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

#[derive(Deserialize, Debug)]
struct ApiRes {
    ip: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    let res = reqwest::blocking::get("https://api.myip.com")?.json::<ApiRes>()?;

    println!("{}", res.ip);

    Ok(())
}

我们执行 cargo run,看看是否正常运行:

代码语言:javascript复制
cargo run
     Running `target/debug/myip`
127.0.0.1

使用空镜像 scratch

大小:15.9 MB

为了将 docker 空镜像 scratch 作为基础镜像,我们必须静态地将程序链接到 musl libc,因为 glibcscratch 中不可用。链接 musl libc,可以通过增加编译目标 x86_64-unknown-linux-musl 来实现。

这样做有一个问题,musl 的内存分配器没有进行速度优化,可能会降低应用程序的性能,尤其是在处理高吞吐量的应用程序时。

这就是为什么我们要使用 jemalloc,一个为高并发应用程序设计的内存分配器。

请注意,在使用 jemalloc 时可能会产生错误,因此请注意查看日志 ;)

作为一个数据节点,我已经使用它为数百万个 HTTP 请求提供了服务,没有任何问题。

Dockerfile.scratch

代码语言:javascript复制
####################################################################################################
## Builder
####################################################################################################
FROM rust:latest AS builder

RUN rustup target add x86_64-unknown-linux-musl
RUN apt update && apt install -y musl-tools musl-dev
RUN update-ca-certificates

# Create appuser
ENV USER=myip
ENV UID=10001

RUN adduser 
    --disabled-password 
    --gecos "" 
    --home "/nonexistent" 
    --shell "/sbin/nologin" 
    --no-create-home 
    --uid "${UID}" 
    "${USER}"


WORKDIR /myip

COPY ./ .

RUN cargo build --target x86_64-unknown-linux-musl --release

####################################################################################################
## Final image
####################################################################################################
FROM scratch

# Import from builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

WORKDIR /myip

# Copy our build
COPY --from=builder /myip/target/x86_64-unknown-linux-musl/release/myip ./

# Use an unprivileged user.
USER myip:myip

CMD ["/myip/myip"]

让我们构建,以及运行镜像:

代码语言:javascript复制
docker build -t myip:scratch -f Dockerfile.scratch .
# 省略构建时输出
# ……

docker run -ti --rm myip:scratch
127.0.0.1

使用基础镜像 alpine

大小:21.6MB

Alpine Linux 是以安全为理念的轻量级 Linux 发行版,基于 musl libcbusybox

如果使用 scratch 空镜像不满足需求,并且需要包管理器来安装依赖项,如 chromium 或者 ssh,那么应当使用 alpine 基础镜像。

由于基础镜像 alpine 基于 musl libc,因此它的约束条件与空镜像 scratch 相同,我们需要使用编译目标 x86_64-unknown-linux-musl,以静态链接我们的 Rust 程序。

Dockerfile.alpine

代码语言:javascript复制
####################################################################################################
## Builder
####################################################################################################
FROM rust:latest AS builder

RUN rustup target add x86_64-unknown-linux-musl
RUN apt update && apt install -y musl-tools musl-dev
RUN update-ca-certificates

# Create appuser
ENV USER=myip
ENV UID=10001

RUN adduser 
    --disabled-password 
    --gecos "" 
    --home "/nonexistent" 
    --shell "/sbin/nologin" 
    --no-create-home 
    --uid "${UID}" 
    "${USER}"


WORKDIR /myip

COPY ./ .

RUN cargo build --target x86_64-unknown-linux-musl --release

####################################################################################################
## Final image
####################################################################################################
FROM alpine

# Import from builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

WORKDIR /myip

# Copy our build
COPY --from=builder /myip/target/x86_64-unknown-linux-musl/release/myip ./

# Use an unprivileged user.
USER myip:myip

CMD ["/myip/myip"]

让我们构建,以及运行镜像:

代码语言:javascript复制
docker build -t myip:alpine -f Dockerfile.alpine .
# 省略构建时输出
# ……

docker run -ti --rm myip:alpine
127.0.0.1

使用基础镜像 buster-slim

大小:79.4MB

最后一个例子,我们将使用基础镜像 debian:buster-slim 作为基本。由于 Debian 基于 glibc,我们不再需要使用编译目标 x86_64-unknown-linux-musl

Dockerfile.debian

代码语言:javascript复制
####################################################################################################
## Builder
####################################################################################################
FROM rust:latest AS builder

RUN update-ca-certificates

# Create appuser
ENV USER=myip
ENV UID=10001

RUN adduser 
    --disabled-password 
    --gecos "" 
    --home "/nonexistent" 
    --shell "/sbin/nologin" 
    --no-create-home 
    --uid "${UID}" 
    "${USER}"


WORKDIR /myip

COPY ./ .

# We no longer need to use the x86_64-unknown-linux-musl target
RUN cargo build --release

####################################################################################################
## Final image
####################################################################################################
FROM debian:buster-slim

# Import from builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

WORKDIR /myip

# Copy our build
COPY --from=builder /myip/target/release/myip ./

# Use an unprivileged user.
USER myip:myip

CMD ["/myip/myip"]

让我们构建,以及运行镜像:

代码语言:javascript复制
docker build -t myip:debian -f Dockerfile.debian .
# 省略构建时输出
# ……

docker run -ti --rm myip:debian
127.0.0.1

结论

代码语言:javascript复制
docker images
REPOSITORY    TAG           IMAGE ID       CREATED          SIZE
myip          scratch       795604e74501   9 minutes ago    15.9MB
myip          alpine        9a26400587a2   2 minutes ago    21.6MB
myip          debian        c388547b9486   12 seconds ago   79.4MB

虽然本文我们聚焦于 Docker,但是如果镜像对您来说仍然太大,并且您知道自己在做什么,那么请参阅这篇文章,还有一些技巧可以将 Rust 可执行文件的大小进一步精简。

例如,在 Cargo.toml 文件中:

代码语言:javascript复制
[profile.release]
lto = true
codegen-units = 1

然后,在执行 cargo build 命令后,在 Dockerfile 文件中增加:

代码语言:javascript复制
RUN strip -s /myip/target/release/myip

现在,大小如下:

代码语言:javascript复制
docker images
REPOSITORY    TAG           IMAGE ID       CREATED          SIZE
myip          scratch       de26b0460262   17 minutes ago   4.2MB
myip          alpine        4188ccc82662   6 minutes ago    9.81MB
myip          debian        0eefb58278a8   4 seconds ago    72.8MB

谢谢您的阅读!

0 人点赞