Et bien justement ça tombe bien... Je ne suis pas connu pour ma patience et je n'aime pas attendre la construction d'une image. Il n'est pas toujours possible de masquer ce temps au cœur d'une pipeline CI/CD et j'ai souvent le besoin de build en direct mes images. Quelles techniques peuvent m'aider afin de gagner du temps dans la construction de mes images ? Existe t-il des bonnes pratiques qui me feront forcément gagner du temps ?
Buildkit
Afin de bien comprendre l'importance de la construction d'images dans l'environnement Docker. Voici l'information transmise par les équipes Docker à ce sujet :
Docker Build is one of the most used features of the Docker Engine - users ranging from developers, build teams, and release teams all use Docker Build.
De ce fait, Docker a travaillé sur un nouvel outil plus performant pour la création d'image. Vous pouvez maintenant créer vos images Docker à l'aide du backend Buildkit. Ce backend, plus intelligent que le builder legacy, est capable d'accélérer le processus de construction des images. Comment ?
En réalisation notamment une mise en cache plus intelligente, en parallélisant des étapes de construction et en acceptant également de nouvelles options dans les fichiers Dockerfile.
Sortie officiellement depuis la version 18.09, cet outil n'est plus vraiment une nouveauté. L'utilisez-vous pour autant ? Je vais essayer de vous montrer pourquoi cet outil va devenir votre outil favori pour la construction de vos images !
Plutôt que de décrire ici les fonctionnalités apportées par cette version, vous trouverez très facilement cette liste sur internet, j'ai décidé de me focaliser sur un cas très concret d'utilisation.
Je vais tout simplement améliorer le temps de traitement du build d'une image, qui est présente sur le Docker Hub : https://hub.docker.com/r/owasp/modsecurity . Plus précisement, je vais m'attaquer à l'image utilisant apache et modsecurity version 2.9.3.
Téléchargé plus de 12k+ fois, ce fichier peut être amélioré et nous allons réaliser cela ensemble !
Cas pratique
Cette image récupère le module ModSecurity sur le github du projet avant de le compiler. Il est ensuite intégré à l'image Docker httpd
afin de fournir un pare-feu applicatif.
Dans un premier temps, je vais cloner le dépôt et réaliser une première construction tout à fait classique. Cette première construction nous servira de référence :
git clone https://github.com/CRS-support/modsecurity-docker
cd modsecurity-docker
time docker build -f v2-apache/Dockerfile -t modsec:classic . --no-cache
🚩 Je vais pull au préalable la seule image utilisée dans ce build afin de ne pas tenir compte de ce délai :
docker pull httpd:2.4
J'utilise également la commande time
afin de mesurer le temps d'éxecution du build. Bien évidement et en toute transparence, ce temps de construction peut varier car l'image contient des dépendances réseaux. Je vais utiliser le temps de référence le plus faible obtenu lors d'une vingtaine de build successif 🚩
Successfully built a328bbb7d984
Successfully tagged modsec:classic
real 1m15,623s
user 0m0,073s
sys 0m0,066s
Vous allez facilement constater que plusieurs éléments retardent notre construction :
- Clone des dépôts
git
, - Deux compilations de modules,
- Utilisation de la commande
apt-get
avec une liste importante de paquets à installer.
- Utilisation de make dans vos Dockerfiles
Il est souvent nécessaire, voir simplement utile, de compiler soit même certains modules dans ses fichiers Dockerfile
. Dans notre cas, deux modules sont compilés. Je trouve que dans ce cas, ces compilations prennent trop de temps.
Vérifions le fichier et essayons de comprendre pourquoi :
RUN wget --quiet https://github.com/ssdeep-project/ssdeep/releases/download/release-2.14.1/ssdeep-2.14.1.tar.gz \
&& tar -xvzf ssdeep-2.14.1.tar.gz \
&& cd ssdeep-2.14.1 \
&& ./configure \
&& make \
&& make install \
&& make clean
RUN git clone https://github.com/SpiderLabs/ModSecurity --branch v2.9.3 --depth 1 \
&& cd ModSecurity \
&& ./autogen.sh \
&& ./configure \
&& make \
&& make install \
&& make clean
Cette utilisation d'un Dockerfile est très classique mais un élément pourrait nous faire gagner un temps considérable. Lors de la compilation des modules, la commande make
n'utilise pas tous les processeurs à disposition !
Vous pouvez simplement modifier cela avec un simple changement :
make -j$(nproc)
Réalisons un nouveau test de construction avec cet ajout :
Successfully built ca2b54ef51be
Successfully tagged modsec:classic-nproc
real 1m0,250s
user 0m0,112s
sys 0m0,028s
Avec ce simple ajout, nous venons de gagner ~15 secondes !
Mais allons plus loin en utilisant buildkit...
2. Utilisation de buildkit
Prenons un exemple simple de multi-staging :
FROM debian AS base
RUN apt-get update && apt-get install git
FROM base AS builder1
RUN git clone …
FROM base as builder2
RUN git clone …
Je réalise une image commune nommée base dans la quelle, nous allons installer git. Je vais ensuite utiliser cette image afin de cloner 2 dépôts mais en installant git une seule fois.
Grâce à buildkit, cet exemple prend encore plus d'intérêts, puisque les images builder1 et builder2 vont être construites en parallèle!
C'est tout l'intérêt de cette fonctionnalité, buildkit peut paralléliser certaines tâches.
Reprenons donc notre Dockerfile et essayons de voir les éléments qui vont être construits en simultané :
FROM httpd:2.4 as build
LABEL version="2.9.3"
....
FROM httpd:2.4
ENV ACCESSLOG=/var/log/apache2/access.log \
....
Ces deux étapes vont être construites en parrallèle. Ce qui va encore nous faire gagner du temps.
En repartant de mes précédentes modifications, je vais créer l'image avec buildkit :
DOCKER_BUILDKIT=1 docker build -f v2-apache/Dockerfile -t modsec:buildkit_nproc . --no-cache
[+] Building 54.4s (25/25) FINISHED
Nous venons encore de gagner du temps, et sans aucune modification !
Pour activer buildkit, rien de plus simple. Vous pouvez soit utiliser la variableDOCKER_BUILDKIT=1
ou l'activer par défaut dans votre fichier de configuration /etc/docker/daemon.json
:
{ "features": { "buildkit": true } }
Il est temps maintenant de revoir un peu notre fichier et d'y apporter quelques modifications afin d'optimiser l'utilisation de buildkit.
3. Modification du fichier Dockerfile
Je vais surtout m'attarder à la première partie du fichier, cette partie concerne la compilation des modules utilisés plus tard dans le fichier :
FROM httpd:2.4 as build
LABEL version="2.9.3"
RUN apt-get update \
&& apt-get install -y --no-install-recommends --no-install-suggests \
automake \
ca-certificates \
g++ \
git \
libcurl4-gnutls-dev \
libpcre++-dev \
libtool \
libxml2-dev \
libyajl-dev \
lua5.2-dev \
make \
pkgconf \
wget
RUN wget --quiet https://github.com/ssdeep-project/ssdeep/releases/download/release-2.14.1/ssdeep-2.14.1.tar.gz \
&& tar -xvzf ssdeep-2.14.1.tar.gz \
&& cd ssdeep-2.14.1 \
&& ./configure \
&& make -j$(nproc) \
&& make install \
&& make clean
RUN git clone https://github.com/SpiderLabs/ModSecurity --branch v2.9.3 --depth 1 \
&& cd ModSecurity \
&& ./autogen.sh \
&& ./configure \
&& make -j$(nproc) \
&& make install \
&& make clean
.....
Afin d'utiliser la fonction de parallélisation au maximum, je vais découper les deux compilations en deux étapes différentes :
FROM httpd:2.4 as base
LABEL version="2.9.3"
RUN apt-get update \
&& apt-get install -y --no-install-recommends --no-install-suggests \
automake \
....
wget
FROM base as builder-ssdeep
RUN wget --quiet https://github.com/ssdeep-project/ssdeep/releases/download/release-2.14.1/ssdeep-2.14.1.tar.gz \
&& tar -xvzf ssdeep-2.14.1.tar.gz \
&& cd ssdeep-2.14.1 \
&& ./configure \
&& make -j$(nproc) \
&& make install \
&& make clean
......
FROM base as builder-modsec
RUN git clone https://github.com/SpiderLabs/ModSecurity --branch v2.9.3 --depth 1 \
&& cd ModSecurity \
&& ./autogen.sh \
&& ./configure \
&& make -j$(nproc) \
&& make install \
&& make clean
Vérifions le temps de traitement :
[+] Building 42.8s (25/25) FINISHED
Mais est-il possible d'aller encore plus loin ?
4. Vers l'infini et ...
Oui c'est possible, je dirais même que tout est possible ...
Voici mon dernier essai avec quelques modifications supplémentaires :
[+] Building 37.9s (31/31) FINISHED
Quelles sont ces modifications ?
FROM httpd:2.4 as builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends --no-install-suggests \
automake \
g++ \
libcurl4-gnutls-dev \
libpcre++-dev \
libtool \
libxml2-dev \
libyajl-dev \
lua5.2-dev \
make \
openssl \
pkgconf
FROM builder as builder-ssdeep
ADD https://github.com/ssdeep-project/ssdeep/releases/download/release-2.14.1/ssdeep-2.14.1.tar.gz ./
RUN tar -xvzf ssdeep-2.14.1.tar.gz \
&& cd ssdeep-2.14.1 \
&& ./configure \
&& make -j$(nproc) \
&& make install \
&& make clean
FROM alpine as base-git
RUN apk --no-cache add git \
&& git clone https://github.com/SpiderLabs/ModSecurity --branch v2.9.3 --depth 1
FROM builder as builder-modsec
COPY --from=base-git /ModSecurity ./ModSecurity
RUN cd ModSecurity \
&& ./autogen.sh \
&& ./configure \
&& make -j$(nproc) \
&& make install \
&& make clean
En fait, il y a deux principales modifications.
- J'ai retiré l'utilisation de
wget
afin de privilégier la commande DockerfileADD
. Cela retire une installation à la commande :apt
- J'utilise une image alpine pour installer
git
et cloner le dépôt plus rapidement et de façon parallèle aux autres constructions.
Je n'ai pas mis le détail du fichier et vous trouverez prochainement le fork du projet dans mon dépôt github. J'ai également modifié la position de certaines commandes afin d'utiliser la fonctionnalité de buildkit ( construction parallèle ) au maximum ...
Encore plus loin ... ?
Nous avons pu voir ensemble comment diminuer le temps de construction d'une image Docker d'environ 75 secondes à 38 secondes... Nous sommes proche du gain de 50% ! 😍
Bien évidement ce gain temporel peut être encore plus important si votre image demande un temps de construction conséquent. Peut-on aller encore plus loin ? Oui, sans aucun doute. Mais à quel prix ? On peut voir qu'entre mes deux derniers builds, le gain n'est que de 4 secondes environ alors que les modifications sont relativement plus nombreuses.
L'effort n'est donc à mon sens, pas rentable. Sauf si bien évidement, votre environnement de travail vous pousse à grapiller la moindre seconde. Dans l'ensemble je dirais que si vous respectez les éléments suivants, vos builds devraient devenir plus rapide et même plus lisible sans pour autant nécessiter un temps conséquent dans l'écriture de vos Dockerfiles :
- Utilisez la commande
make
avec l'option-j
si possible - Multi-staging, Multi-staging, Multi-staging
- Profitez de votre multi-staging avec buildkit !
Et la taille des images dans tout ça ? Je sais que certains d'entre vous y seront sensibles :
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
modsec buildkit_nproc_multi_staging_3 82b3a5b807f9 19 minutes ago 194MB
modsec buildkit_nproc_multi_staging_2 26a5a5ad9932 21 minutes ago 197MB
modsec buildkit_nproc_multi_staging_last 7a1874098b5e 23 minutes ago 194MB
modsec buildkit_nproc_multi_staging cd000688c0f8 28 minutes ago 197MB
modsec buildkit_nproc 20399bb0cba9 About an hour ago 197MB
modsec classic-nproc ca2b54ef51be 2 hours ago 197MB
Et oui, elles ont toutes une taille similaire ! On dit merci qui ?*
En tout cas, il reste encore plein d'autres choses à voir avec Buildkit, j'aborderai d'autres instructions lors d'un prochain article:par exemple la mise en cache apt
ou l'utilisation de secrets
lors du build de votre image !
En tout cas n'hésitez pas à m'apporter des remarques ou des commentaires sur Twitter ! C'est toujours un plaisir d'avoir des retours ! 😇
*Oui, oui, Merci Buildkit ! 😂