Docker et les layers
Explications très succincte des layers
Docker fonctionne grâce à Overlayfs* et des layers. Une image est composée de layers, assemblés au lancement d’un container pour former le filesystem final.
* Overlayfs dans la plupart des cas. Docker peut également utiliser d’autre engines, mais overlayfs est celui par défaut et le plus répandu.
Lors de la création d’une image, nous créons ces layers, et nous y ajoutons des métadonnées. Chaque instruction* d’un Dockerfile ajoute un Layer, ou une métadonnées à l’image finale.
Prenons le Dockerfile suivant comme exemple :
Cette image possède au moins 4 layers :
- au moins 1 via
python:alpine
, image de base utilisée ici (en réalité probablement plus) - 1 layer par instruction
RUN
Les instructions ENV et LABEL ne génèrent pas de layers, mais uniquement des métadonnées
On peut apercevoir ces layers avec les commandes docker push/pull qui indiquent quels layers sont pushed/pulled
Pour réellement voir les layers, on utilisera la commande docker inspect <id/name> | jq '.[0]'.RootFS.Layers
Plutôt que d’écrire 3 RUN
différents ici, regroupons les en un pour économiser les layers :
Pourquoi est-ce important ?
Plusieurs raisons :
- Plus on a de layers, moins notre image sera efficace (plus lente à push/pull, léger overhead au runtime, …)
- L’image peut s’en retrouver plus lourde :
- L’image peut leaker des secrets :
- Permet d’économiser de l’espace disque si les images partagent des layers (voir juste en dessous)
- Permet de build des images plus rapidement et efficacement (voir juste en dessous)
Ordonnons les layers
Pourquoi ordonner les layers ?
Ordonner ses layers a 2 énormes impacts pour tout le monde :
-
Si 2 images différentes ont toutes les deux besoins de python 3.12 et d’avoir git, elles peuvent partager des layers. Tous les layers partagés ne dupliquent pas l’espace disque pris. Ainsi, si deux images pour deux applications A et B, basées sur python 3.12 et git, sont bien construite, l’espace disque sera de
sizeof(python 3.12 + git) + sizeof(A) + sizeof(B)
.Si elles sont mal construites, l’espace disque total sera au mieux de
sizeof(python 3.12) + 2*sizeof(git) + sizeof(A) + sizeof(B)
, et au pire2*sizeof(python 3.12) + 2*sizeof(git) + sizeof(A) + sizeof(B)
. L’espace disques est donc significativement impacté, le temps de push/pull également, voire le startup time (si on doit pull l’image avant). -
Un développeur qui travaille sur une application avec son image va vouloir rebuild l’image fréquemment, pour tester. Il est fort probable que les dépendances de l’image évoluent rarement, que les dépendances de l’application évoluent peu fréquemment et que l’application évoluent fréquemment.
Si pour un changement minime de code, par exemple ajouter un commentaire, il faut re-télécharger toutes les dépendances, le temps de build en devient catastrophiquement long et les serveurs des associations et volontaires qui hébergent les dépendances se font contacter inutilement. En résulte un temps de build trop long, un développeur frustré, et de la bande passante gâchée.
Si au contraire les instructions du Dockerfile sont bien ordonnées, nous n’avons pas ce problème grâce à la réutilisation du cache de build.
Comment faire ?
La rule of thumb pour écrire son Dockerfile est de chercher à minimiser les layers. Mais il faut également garder en tête le cache, et l’utiliser avec habileté.
Dans ce billet, on va prendre pour exemple la construction d’une image docker pour une application en Python, image qui nécessite git
et des dépendances pythons listées dans un pyproject.toml
.
Le contenu de l’application app.py
et du pyproject.toml
n’ont que peu d’importance ici.
Voici un Dockerfile naïf :
Un seul run qui regroupe donc plusieurs layers en un, pas de layer de cache supprimé par la suite qui traîne dans l’image finale, sela semble intelligent en apparence.
Mais si on ajoute un commentaire basique dans le code de l’application, absolument toutes les dépendances python et git seront réinstallées, et on perd environ mille ans.
Une meilleure manière de faire :
Il y a ici plus de layers. Mais le changement le plus fréquent, celui du code source, n’entraîne que la re-copie du code et non pas l’install des dépendances, grâce au cache du builder Docker. L’opération prend très peu de temps.
L’opération moins fréquente de changement des dépendances python ne provoque pas la réinstallation de git, uniquement le COPY pyproject.toml
(et la suite).
Et seul le changement le plus rare, celui des dépendances d’image/d’OS (git par exemple), entraîne le rebuild complet.
Si l’on possède 2 applications A et B qui dépendent toutes les deux de python:3.12-alpine
et de git
, si le début des deux Dockerfile est identique, les deux images résultantes vont partager leurs premiers layers. Tout bénef !
Multi stage build
Si l’on est amateur de multi stage build, et que l’on cherche à avoir un comportement similaire, il faut changer légèrement les choses.
Voici un exemple :
L’exemple est plus complexe, mais permet d’utiliser efficacement le cache et les layers.
Leave a Comment
Your email address will not be published. Required fields are marked *