Par Jordan A. - Expert DevOps
Durant la DockerCon 2021 qui a eu lieu le 27 mai 2021, nous avons pu assister à un certain nombre de conférences très intéressantes. L’une d’entre elle a retenu notre attention, car elle présente des concepts de base pour la rédaction de fichiers Dockerfile et ainsi créer des conteneurs efficaces et efficients.
Cette conférence a été réalisée par Aaron Kalin, Technical Evangelist chez Datadog : « Lessons Learned With Dockerfiles and Docker Builds » et propose 7 leçons à retenir, que je vais vous détailler et reprendre à l’aide d’exemples concrets.
Lesson 1 : Attention à l’image de base que vous utilisez
Les images alpines ont beaucoup de succès ces dernières années, du fait de leur faible taille et du nombre restreint de vulnérabilités. Il s’agit donc d’une base idéale pour construire votre propre image Docker ?
Oui mais… À force d’utilisation, les images alpines ne font plus du tout l’unanimité auprès des développeurs. Une des premières problématiques, concerne l’utilisation de musl plutôt que glibc (alors que les distributions les plus populaires utilisent plutôt glibc). Cela signifie que les éléments qui seront compilés sur les distributions alpines, peuvent ne pas être utilisables sur Ubuntu (et vice versa).
De plus, quid des paquets qui ne sont pas encore disponibles sur Alpine alors qu’ils le sont sur d’autres distributions, et indispensables pour traiter les dépendances de votre code ?
Aaron Kalin nous invite à utiliser plutôt les images dans des versions « slim » qui sont de tailles réduites, parfois assez proches des tailles des alpines comme ici :
$ docker image ls | grep python
python 3.9.1-slim-buster 8c84baace4b3 3 months ago 114MB
python 3.7.4-alpine3.9 32a1b98d0495 19 months ago 98.5MB
Lesson 2 : Enchaînez vos commandes RUN
Le principe d’enchaîner vos commandes RUN pour l’installation des dépendances permet d’avoir qu’une seule couche de créée (car pour chaque commande dans le fichier Dockerfile, une nouvelle couche est créée) pour vos dépendances.
Aaron Kalin conseille également d’organiser les noms de paquets à installer par ordre alphabétique avec un seul paquet par ligne (plus facile à maintenir et à réorganiser).
Par exemple, prenons un fichier Dockerfile avec les paquets à installer sur une seule ligne :
FROM ubuntu:bionic
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
nginx \
python
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
On obtient l’history suivant :
$ docker history docker_1_layer:latest
IMAGE CREATED CREATED BY SIZE COMMENT
6892a0a503de 18 seconds ago /bin/sh -c #(nop) CMD
["nginx" "-g" "daemon… 0B
374bdfdad2b2 18 seconds ago /bin/sh -c #(nop) EXPOSE
80 0B
3f7201caacaa 20 seconds ago /bin/sh -c apt-get update
&& apt-get install… 189MB
81bcf752ac3d 8 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 8 days ago /bin/sh -c mkdir -p
/run/systemd && echo 'do… 7B
<missing> 8 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 8 days ago /bin/sh -c set -xe &&
echo '#!/bin/sh' > /… 745B
<missing> 8 days ago /bin/sh -c #(nop) ADD file:e05689b5b0d51a231… 63.1MB
Maintenant, effectuons la même expérience avec les éléments dispatchés ligne par ligne dans son fichier Dockerfile :
FROM ubuntu:bionic
RUN apt-get update && apt-get install -y --no-install-recommends curl
RUN apt-get install -y git
RUN apt-get install -y nginx
RUN apt-get install -y python3
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
On obtient alors l’history suivant :
IMAGE CREATED CREATED BY SIZE COMMENT
0d25db122b31 16 seconds ago /bin/sh -c #(nop) CMD
["nginx" "-g" "daemon… 0B
3cf4fb051b11 17 seconds ago /bin/sh -c #(nop) EXPOSE
80 0B
f736c0e7e9e6 18 seconds ago /bin/sh -c apt-get install
-y python3 29.4MB
c6c35fc73cad 28 seconds ago /bin/sh -c apt-get install
-y nginx 53.3MB
53e8b93b739a 39 seconds ago /bin/sh -c apt-get install
-y git 83.4MB
57e76bf1ae81 52 seconds ago /bin/sh -c apt-get update
&& apt-get install… 48.9MB
81bcf752ac3d 8 days ago /bin/sh -c #(nop) CMD
["/bin/bash"] 0B
<missing> 8 days ago /bin/sh -c mkdir -p
/run/systemd && echo 'do… 7B
<missing> 8 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 8 days ago /bin/sh -c set -xe &&
echo '#!/bin/sh' > /… 745B
<missing> 8 days ago /bin/sh -c #(nop) ADD file:e05689b5b0d51a231… 63.1MB
On obtient deux images qui ont des tailles différentes, et un niveau de complexité plus important pour la deuxième :
$ docker image ls | grep docker
docker_4_layers latest 0d25db122b31
About a minute ago 278MB
docker_1_layer latest 6892a0a503de
4 minutes ago 252MB
$
Lesson 3 : Clean après l’installation de paquets
Reprenons notre exemple suivant :
FROM ubuntu:bionic
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
nginx \
python
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Ici, après avoir installé les paquets grâce à apt, nous n’avons procédé à aucun clear. Cependant, pour réduire encore la taille de l’image, et donc son délai de build et de chargement, on peut ajouter les commandes suivantes :
rm -rf /var/lib/apt/lists/* && apt clean
Cela nous donnerait donc :
FROM ubuntu:bionic
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
nginx \
python \
&& rm -rf /var/lib/apt/lists/* \
&& apt clean
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
On peut ainsi comparer la taille de l’image sans avoir effectué le clean : docker_1_layer, et de l’image ayant effectuée le clean : docker_1_layer_clean :
$ docker image ls | grep docker
docker_1_layer_clean latest 494fb62a6e8c
16 seconds ago 216MB
docker_4_layers latest 0d25db122b31
7 minutes ago 278MB
docker_1_layer latest 6892a0a503de
10 minutes ago 252MB
On voit donc que l’image où le clean a été effectué, est de taille réduite par rapport à l’image où le clean n’a pas été fait. On a donc encore réussi à réduire la taille de notre image.
Lesson 4 : Lancer l’installation des dépendances applicatives séparément à la fin du Dockerfile
En effet, comme ces dépendances sont amenées à changer de temps en temps avec l’évolution de votre code, il convient de les faire figurer en priorité dans les parties les plus basses du Dockerfile. Ainsi, on évite de reconstruire toutes les couches suivantes en cas de modifications.
On n’oublie pas non plus de spécifier à l’outil de ne pas conserver de données en cache (un peu comme pour apt).
Voici un exemple pour l’installation de librairies Python :
RUN pip install --no-cache-dir -r requirements.txt
Lesson 5 : Ne pas oublier d’utiliser le .dockerignore
Aaron Kalin nous rappelle à très juste titre, d’utiliser le fichier .dockerignore de manière intelligente. En effet, il permet d’exclure des répertoires et des fichiers de toutes copies qui pourraient être effectuées à l’intérieur de l’image Docker.
Parmi les fichiers et répertoires que l’on oublie souvent de ne pas inclure figurent en pôle position : .git
Quel dommage qu’il soit téléchargé à l’intérieur de votre image docker ?!
D’autres fichiers que l’on a tendance à oublier correspondent à tous les fichiers Dockerfile dans notre répertoire de travail.
Voici donc à quoi ressemblerait notre fichier .dockerignore :
.git
Dockerfile*
Les « lessons » 6 et 7 seront traitées dans le prochain article. Elles concernent l’utilisation de la construction des images par la fonctionnalité de multi-stage et l’utilisation des labels.