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.
  1. 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 Dockerfile ADD. 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 ! 😂