Rust et Docker: Optimiser la taille de l'image
Pour mon API de récupération de flux RSS, je veux faire une image docker, parce que je suis grave à la mode comme mec, et qu'il faut avouer que c'est quand même super pratique pour déployer des truc sans se faire chier, d'autant plus avec docker compose
Il vient cependant deux soucis avec docker en général, et rust en particulier. Le premier est la taille des images qui peuvent facilement, si on fait pas gaffe, taper le giga, voir plus, pour une pauvre API. Le deuxième étant les temps de compilation en rust qui sont quand même pas bien rapide, ce qui est déjà relou à la base, mais encore plus quand on a que quelques centaines de minutes gratos sur la CI de Gitlab ou de Github.
On va commencer par réduire la taille de l'image. Déjà voyons la taille du binaire release
produite, par cargo, avant toutes optimisations:
cargo build --release
# One eternity later
ls -aslh target/release | grep rss-aggregator
-rwxr-xr-x 2 eric eric 22M 16 mai 22:17 rss-aggregator
On peut déjà optimiser ça en ajoutant quelques menus options dans le Cargo.toml
sur notre profile de release. Ca se fait au prix d'un temps de compilation plus long, mais bon, on est plus à 20 minutes près!
[profile.release]
strip = true
opt-level = "s"
lto = true
codegen-units = 1
cargo build --release
# Two eternity later
ls -aslh target/release | grep rss-aggregator
-rwxr-xr-x 2 eric eric 9,3M 16 mai 22:19 rss-aggregator
Maintenant, on attaque le gros du sujet, l'image docker. Une approche naïve du truc voudrait qu'on prenne l'image officiel de rust, on compile avec, et yolo comme disent les vieux
FROM rust:latest AS builder
# On crée un user sans privilèges, ça mange pas de pain
ENV USER=rss-aggregator
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
# On copie ce dont on a besoin
WORKDIR app
COPY Cargo.toml Cargo.toml
COPY Cargo.lock Cargo.lock
COPY .env .env
COPY configuration.yaml configuration.yaml
COPY entity/ entity/
COPY migrations/ migrations/
COPY src/ src/
# On compile tout ça
RUN cargo build --release
# On dit d'utiliser notre user sans privilèges
USER rss-aggregator:rss-aggregator
EXPOSE 8080
ENTRYPOINT ["target/release/rss-aggregator"]
Résultat?
REPOSITORY TAG IMAGE ID CREATED SIZE
rss-aggregator latest c5ba02ced00b 12 seconds ago 2.59GB
2.59 GB pour un binaire qui fait à la base 9.3 Mo. Bah non, on va pas faire comme ça.
Vu que rust n'a pas besoin d'un interpréteur ou d'une machine virtuelle comme python ou java, on peut juste compiler le binaire quelque part, et récupérer uniquement le résultat de cette compilation dans une image un peu plus light. Voyons ça
FROM rust:latest AS builder
ARG DEBIAN_FRONTEND=noninteractive
# On crée un user sans privilèges, ça mange toujours pas de pain
ENV USER=rss-aggregator
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR app
COPY Cargo.toml Cargo.toml
COPY Cargo.lock Cargo.lock
COPY .env .env
COPY configuration.yaml configuration.yaml
COPY entity/ entity/
COPY migrations/ migrations/
COPY src/ src/
# On compile comme d'habitude
RUN cargo build --release
# Ici on utilise une nouvelle image qui va servir de runtime
# Buster-slim est une image debian presque légère; ~70 Mo
FROM debian:buster-slim as runtime
ARG DEBIAN_FRONTEND=noninteractive
# On copie le binaire de l'image servant de builder et le user
COPY --from=builder /app/target/release/rss-aggregator /usr/local/bin
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# On copie les resources statics
COPY static/ static/
USER rss-aggregator:rss-aggregator
EXPOSE 8080
ENTRYPOINT ["rss-aggregator"]
Et maintenant
REPOSITORY TAG IMAGE ID CREATED SIZE
rss-aggregator latest 4f07dfc12c5b 27 seconds ago 78.9MB
Ah ben oui, c'est beaucoup mieux! Mais on peut mieux faire grâce à Alpine. Cette distribution est ultra légère mais se base sur musl en lieux et place de glibc, ce qui peut poser des soucis. Et l'image docker fait... 5.57 Mo. Il faut cependant dire à cargo
qu'on cible musl
, mais ça se fait super bien.
FROM rust:latest AS builder
ARG DEBIAN_FRONTEND=noninteractive
# On ajoute ce qu'il faut pour compiler pour musl
RUN rustup target add x86_64-unknown-linux-musl
RUN apt update && apt install -y musl-tools musl-dev
RUN update-ca-certificates
ENV USER=rss-aggregator
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR app
COPY Cargo.toml Cargo.toml
COPY Cargo.lock Cargo.lock
COPY .env .env
COPY configuration.yaml configuration.yaml
COPY entity/ entity/
COPY migrations/ migrations/
COPY src/ src/
# On spécifie qu'on veut compiler pour musl
RUN cargo build --target x86_64-unknown-linux-musl --release
### Notre image de runtime
FROM alpine
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Ne pas oublier le x86_64-unknown-linux-musl dans la target
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/rss-aggregator /usr/local/bin
COPY static/ static/
USER rss-aggregator:rss-aggregator
EXPOSE 8080
ENTRYPOINT ["rss-aggregator"]
Ce qui nous donne une image de...
REPOSITORY TAG IMAGE ID CREATED SIZE
rss-aggregator latest fdc57ae50e68 45 seconds ago 15.3MB
15.3 MB! C'est devenu raisonnable!
Il faut garder à l'esprit par contre que certaine lib que vous utilisez dans votre code peuvent ne pas gérer musl
. Ce fut mon cas avec reqwest qui par défaut veut un openssl glibc
. J'ai pu corriger ça en spécifiant la feature native-tls-vendored
pour ce paquet, même si j'avoue pas trop comprendre ce que ça fait.
Au final, on se retrouve avec une image complète plus petite que le binaire pré-optimisations, je trouve ça plutôt pas mal!
Il reste le problème du temps de génération des images docker. Mais ça, ça fera l'objet d'un autre post. Principalement quand j'aurais trouvé une solution acceptable.