📘 Anatomie et usage des images de conteneurs

Pour comprendre comment créer des conteneurs, il utile de comprendre les bases du fonctionnement et de l’usage des images de conteneurs, que ce soit comment elles sont constituées, où elles sont stockées, et comment faire référence à une image stockée quelque part.

1. Ingrédients d’une image de conteneur

Une image de conteneur prend la forme d’un ensemble de données comprenant :

  • Une arborescence de répertoires et de fichiers, que l’on peut généralement diviser en :

    • Les fichiers nécessaires au fonctionnement du système d’exploitation restreint (notamment l’ensemble des utilitaires requis)

    • Le logiciel conteneurisé, et tous les fichiers nécessaires à son fonctionnement

  • Des métadonnées au sujet de l’image : origine, date de création, version, environnement d’accueil requis, format de stockage, etc.

    • En particulier, une métadonnée très importante est le point d’entrée de l’image, qui prend la forme d’une commande qui sera exécutée lors du démarrage d’un conteneur créé à partir de l’image. C’est ce point d’entrée qui indique comment le logiciel conteneurisé sera lancé (à savoir, quel exécutable lancer et avec avec quels arguments).

Stockage "en couches" pour économiser l’espace

Une image doit souvent contenir l’essentiel d’un système d’exploitation (certes restreint), ce qui représente une taille parfois importante. Pour palier à cela les images de conteneur sont toujours stockées avec un format dit en couches. Ce format permet de définir une image comme l’extension d’une image pré-existante, et ainsi d’éviter de dupliquer les fichiers de cette dernière.

Ainsi, il est courant de pré-définir des images dites "de base" avec des systèmes d’exploitation restreints prêt à l’emploi (par exemple une image Alpine ou Ubuntu), et de bâtir ensuite des images finales comme des extensions de ces images de base. Cela permet, dans un environnement donné, de dédupliquer les données communes à deux images différentes, dès lors qu’elles partagent une même base.

Une caractéristique clé d’une image de conteneur est l’immutabilité, c’est à dire qu’il n’est pas possible de modifier une image de conteneur existante. Cela permet de garantir qu’un déploiement réalisé avec une image de conteneur sera similaire d’un contexte à un autre.

Il naturellement tout de même possible de créer une nouvelle image de conteneur à partir d’une copie d’une image de conteneur existante.

Enfin, au sein d’un stockage donné, une image de conteneur est toujours qualifiée à l’aide d’un nom et une étiquette. Nous détaillons ces notions dans la Section 3.

2. Déroulement d’une création de conteneur

Avant de plonger dans les détails, prenons de la hauteur et voyons comment se déroule de manière générale la création d’un conteneur à partir d’une image de conteneur.

Dans ce qui suit ci-dessous, nous allons évoquer différentes notions qui serons traitées plus tard dans ce chapitre et dans le cours − c’est normal !
run container.drawio
Figure 1. Étapes principales lors de la création d’un nouveau conteneur depuis une image de conteneur
  1. Si l’image de conteneur requis provient d’un registre distant, et s’il n’est pas encore présent dans le stockage local d’images de l’environnement d’accueil, alors un téléchargement est fait du distant vers le local.

  2. Un nouveau conteneur est instancié en effectuant une simple copie à l’écriture de l’image de conteneur (voir encart "Copie à l’écriture" si dessous).

  3. Le nouveau conteneur est configuré pour lui donner le comportement souhaité dans le contexte de son exécution. Cette configuration inclut notamment les éléments suivants :

    • On peut confier au conteneur un ensemble de variables d’environnement. Pour rappel, une variable d’environnement est une paire CLÉ=valeur qui, une fois définie, est accessible depuis tout logiciel s’exécutant dans un environnement, y compris si cet environnement est un conteneur. Il est courant d’associer des variables d’environnement à un conteneur de manière à paramétrer le logiciel qu’il contient.

    • On indique l’ensemble des publications de ports souhaitées (sera vu dans les chapitres sur la mise en réseau des conteneurs).

    • On indique l’ensemble des montages souhaités (sera vu dans le chapitre sur la gestion des données des conteneurs).

  4. Le point d’entrée du conteneur (soit celui spécifié dans l’image de conteneur, soit celui choisi à l’instantiation du conteneur) est exécuté. Tant que cette exécution n’est pas terminée, alors le conteneur est considéré comme étant en exécution.

Dans la pratique courante, étant donné que recréer un conteneur à partir d’une image de conteneur est très peu coûteux, on considère généralement un conteneur comme quelque chose de jetable que l’on n’hésite pas à détruire et recréer à loisir. Par exemple, pour mettre à jour un logiciel exécuté au sein d’un conteneur, on préférera toujours supprimer le conteneur et le recréer à l’aide d’une image de conteneur plus récente, plutôt que d’aller réinstaller le logiciel au sein du conteneur existant.

Une conséquence importante de cela est qu'il faut toujours utiliser un montage pour stocker les données importantes d’un conteneur au sein de l’environnement d’accueil (sera vu dans le chapitre sur la gestion des données des conteneurs), et ne éviter de stocker de telles données directement dans le conteneur lui-même, qui pourra être supprimé à tout moment.

Copie à l’écriture

Lors de l’étape (2) ci-dessus, ce n’est pas une véritable copie brute de données qui est effectuée, mais une copie à l’écriture (en anglais, copy-on-write). L’idée est la suivante :

  • Tant que les fichiers à l’intérieur du conteneur ne diffèrent pas de ceux de l’image de conteneur, alors le conteneur utilisera directement les données de l’image de conteneur, sans avoir besoin de les copier.

  • Si un fichier s’apprête à être modifié au sein du conteneur, alors une copie est effectuée à la volée de ce fichier provenant de l’image de conteneur, vers un espace dédié aux données d’exécution du conteneur.

  • Si un fichier s’apprête à être créé au sein du conteneur, alors il est simplement directement créé dans l’espace dédié aux données d’exécution du conteneur.

Ainsi, à tout instant, l’espace de stockage occupé par un conteneur n’est égal qu’à la différence entre les données du conteneur et celles de l’image de conteneur utilisé pour sa création.

C’est pour cette raison que créer un nouveau conteneur est généralement une tâche très rapide et quasi-instantanée, car à l’instant de la création il n’y a aucune donnée à copier.

3. Référencer une image

Pour qu’elle soit bien accessible, une image de conteneur peut être qualifiée au sein de l’emplacement où elle se trouve (stockage local ou registre de conteneur). Ce qualificatif est toujours composé de deux parties :

  • Un nom, qui décrit l’image, et qui généralement informe sur le logiciel contenu au sein de l’image. Il est fréquent dans certains registres de conteneurs que ce nom soit écrit sous la forme d’un chemin découpé à l’aide de de barres obliques (/). On peut par exemple avoir un nom sous la forme <groupe>/<nom au sein du groupe> dans un registre de conteneur qui permet de faire des groupes d’images.

  • Une étiquette, qui donne des informations utiles au sujet de l’image : version du logiciel qui s’y trouve, type d’environnement inclus dans l’image (par exemple l’image de base utilisée), information sur la taille de l’image (légère ou non), à quoi l’image est destinée (par exemple débogage, recette ou production) etc.

En pratique, on écrit toujours la qualificatif d’une image avec la syntaxe suivante :

<nom de l'image>:<étiquette>
Il n’existe pas de règles strictes pour bien nommer et étiqueter une image, et les pratiques sont très variées d’un projet à un autre. Il est cependant communément admis que bien étiqueter une image est très utile pour bien les distinguer entre elles !

Il faut être vigilant sur quelques aspects :

  • Au sein d’un même emplacement donné (stockage local ou registre de conteneur) :

    • Un qualificatif <nom de l’image>:<étiquette> correspond toujours à une seule image de conteneur. Ainsi, si on attribue à une image un qualificatif déjà utilisé par une autre image, le qualificatif est automatiquement retiré de cette dernière.

    • Une même image peut être connue sous plusieurs plusieurs qualificatifs différents (le plus souvent souvent avec le même nom mais avec des étiquettes différentes).

  • Un qualificatif est toujours spécifique à un emplacement donné (stockage local ou registre de conteneur). Ainsi, une même image stockée simultanément dans différents emplacements peut posséder un qualificatif différent par emplacement.

Il est par exemple possible de créer une image dans un registre local et de lui donner un qualificatif, puis d’envoyer cette image dans un registre de conteneurs distant en lui donnant une autre qualificatif.
L’étiquette latest

Dans les qualificatifs d’images, il existe une étiquette particulière qui s’intitule latest, et qui est traitée de manière spéciale par les outils de conteneurisation :

  • Lorsqu’on construit une image, si on lui donne un nom sans étiquette, alors l’étiquette latest sera attribuée par défaut à cette image.

  • Lorsqu’on fait référence à une image, si on ne met pas d’étiquette à cette référence, alors par défaut ce sera l’étiquette latest qui sera cherchée et utilisée.

Ainsi, étant donné un nom d’image, il est d’usage courant qu’il existe toujours une étiquette latest qui pointe vers la version d’image qui sera utilisée par défaut.

On notera d’ailleurs que, puisqu’une même image peut posséder plusieurs étiquettes, il est tout à fait possible qu’une même image possède une première étiquette informative − avec par exemple la version du logiciel qu’elle embarque − et possède également l’étiquette latest, ce qui indique aux utilisateurs de l’image que cette version est celle utilisée par défaut.

Par exemple, à l’heure de ces lignes, on trouve parmi les images officielles Python une même image avec les étiquettes 3.11.5, 3.11, 3 et latest. Cela signifie que si on utilise le nom de cette image, mais sans étiquette, alors on obtiendra l’image contenant Python version 3.11.5.

Malgré le sens derrière le mot anglais latest, il n’existe aucune règle stricte quant à l’usage de l’étiquette latest. Dans certains cas cette étiquette fait bien référence à la dernière version possible d’une image, alors que dans d’autres cas cette étiquette fait référence à la version à privilégier d’une image, cette version n’étant pas forcément la dernière.

À partir d’un qualificatif, on peut écrire une référence d’image sous la forme d’un chemin d’image, qui prend la forme suivante :

<emplacement>/<qualificatif>

où :

  • <emplacement> désigne l’endroit où récupérer l’image, qui peut être soit le stockage local, que l’on désigne toujours par localhost, soit un registre de conteneurs, désigné par son nom d’hôte.

  • <qualificatif> désigne le qualificatif tel que défini plus haut.

Exemple 1. Référence vers une image nginx par chemin d’image

Prenons le chemin d’image suivant :

docker.io/nginx:1.25

Nous avons ici :

  • docker.io comme emplacement, donc le Docker Hub, comme registre de conteneur.

  • nginx:1.25 comme qualificatif d’image, avec :

    • nginx comme nom d’image, ce qui sur le Docker Hub correspond à l’image officielle pour le serveur web nginx. On peut voir des informations sur cette image en se rendant sur sa page de description sur le catalogue du Docker Hub.

    • 1.25 comme étiquette, qui fait ici fait référence à la version de nginx embarquée dans l’image.

Exemple 2. Référence vers une image Simplestblog par chemin d’image

Prenons la référence d’image suivante, que l’on écrit pour désigner une image Simplestblog que nous avons au préalable publiée :

docker-registry.univ-nantes.fr/gl/developpement_exploitation/resources/simplestblog:latest

Ici on a :

  • docker-registry.univ-nantes.fr comme emplacement, donc le registre de conteneurs l’instance Gitlab de Nantes Université.

  • gl/developpement_exploitation/resources/simplestblog:latest comme qualificatif d’image, avec :

    • gl/developpement_exploitation/resources/simplestblog comme nom d’image, ce qui ici correspond à l’emplacement du projet Simplestblog au sein de l’instance Gitlab.

    • latest comme étiquette, donc vraisemblablement la version la plus récente de l’image.

Une fois une image distante récupérée sur un stockage local, l’image (désormais locale) mémorise tout de même son emplacement d’origine, ce qui permet à l’image de posséder, même localement, le même chemin que l’image distante.

4. Registres de conteneurs

Il est possible de créer un conteneur à partir d’une image de conteneur située sur un registre de conteneurs, qui est un stockage d’images distant accessible par le réseau. On peut voir un registre de conteneurs comme une sorte de "magasin d’applications" qui ne contient que des images de conteneurs. Un registre de conteneurs peut être privé, par exemple au sein d’une entreprise, ou encore peut être public et mettre à disposition des images à tous directement sur Internet. Lorsqu’on utilise une image provenant d’un registre de conteneurs, cette image est toujours automatiquement copiée au sein du stockage local d’images, avant d’être ensuite utilisée pour créer un nouveau conteneur.

Bien qu’on parle de registre de conteneurs, un registre contient en fait des images de conteneurs !
La très grande majorité des images de conteneurs disponibles en ligne sont dédiées à des environnements d’accueil de type Linux. Une raison est que, aujourd’hui, Linux est utilisé massivement comme environnement d’accueil pour faire fonctionner des serveurs. Or, la conteneurisation vise en grande partie à faciliter la de déploiement de logiciels sur des serveurs. Par conséquent, il est aujourd’hui encore rare d’utiliser la conteneurisation pour des logiciels qui s’exécutent dans un environnement Windows, bien moins utilisé comme environnement serveur.

Concernant les registres de conteneurs publics, Docker Hub est de très loin le registre de conteneurs public le plus important au monde à l’heure actuelle. C’est un registre hébergé par l’entreprise américaine Docker, Inc. qui développe le moteur de conteneurisation Docker. C’est historiquement le premier véritable registre de conteneurs public, et par conséquent c’est aujourd’hui de très loin le plus visible, le plus complet, et le plus utilisé. Tout le monde peut gratuitement utiliser des images hébergées publiquement sur le registre, en revanche il est préférable de passer par une offre payante pour stocker ses propres images sur le registre.

Bien que le site web du Docker Hub soit visitable à l’adresse https://hub.docker.com/, le registre de conteneurs lui-même est disponible avec le nom d’hôte docker.io. C’est ce nom d’hôte qu’il faut utiliser lorsqu’on encode une référence à une image située sur le Docker Hub !
À quoi ressemble le Docker Hub, et une page de description d’image ?

Le Docker Hub est un très vaste "magasin" d’images dans lequel est est vite arrivé de se perdre. Pour comprendre un petit peu mieux cet espace, voici ce que l’on voit si on se rend sur la page web qui présente les images officielles Python, qui partagent toutes le nom python :

dockerhub example annotated.drawio
  1. Tout en haut se trouve la barre de recherche qui permet de trouver toutes sortes d’images hébergées sur le Docker Hub. Attention, on trouve vraiment de tout ! Cela inclut à la fois des images dites officielles publiées directement par les développeurs de logiciels − qui sont des images de confiance et de qualité − et des images de toutes sortes créées par des petits projets et développeurs. Pour rappel, n’importe quel développeur peut se créer un compte gratuitement et héberger ses propres images, pour le meilleur et pour le pire…

  2. Ensuite se trouve la bannière de l’image, avec un logo, un titre, et diverses informations. On peut remarquer des mentions intéressantes qui permettent ici d’attester de la confiance qu’on peut avoir en les images de cette page :

    • La mention Docker Official Image signifie que c’est ce sont des images dont la qualité a été validée par l’équipe du Docker Hub,

    • La mention 1B+ signifie que ces images ont été téléchargées plus d’un milliard de fois,

    • La mention 9.0K le nombre d’utilisateurs à avoir mis une étoile à ces images.

  3. Sur la droite on peut voir un exemple de commande Docker permettant de télécharger une image. Deux points de vigilance :

    • La commande utilise l’outil docker, tandis que nous utiliserons l’outil podman

    • La commande utilise un chemin d’image abrégé qui ne spécifie ni le registre ni l’étiquette. Dans un environnement avec Docker Hub comme registre par défaut , ce chemin équivaut donc à docker.io/python:latest.

  4. Le plus gros de la page contient la documentation des images. Cette partie peut grandement varier d’une page à une autre, mais contient souvent une partie plus ou moins complexe comme celle entourée en pointillés (*). On y trouve une longue liste de toutes les images disponibles sur le registre avec le nom python. Chaque item de la liste correspond à une seule image, et pour chaque image on peut lire la liste des étiquettes associées. Au sein de chaque étiquette, des mots-clés sont utilisés pour informer sur comment l’image a été construite, comme slim pour indiquer que c’est une image légère avec peu de logiciels installés, alpine pour indiquer que l’image de base est Alpine Linux, ou encore bookworm qui correspond au nom de la version de Debian utilisée comme image de base.

Ensuite si on se rend dans l’onglet Tags on arrive face à la liste de toutes les étiquettes :

dockerhub example tags annotated.drawio

Cette liste n’est pas forcément ordonnée ni catégorisée, contrairement à la liste proposée dans la documentation de l’image montrée juste précédemment. On peut s’intéresser à quelques informations utiles au sein de chaque élément de la liste :

  1. L’intitulé de l’étiquette.

  2. Un préfixe de l’identifiant de l’image. Attention, sauf exception, cet identifiant n’est valable que sur le Docker Hub. Ainsi, une fois dans un stockage local, l’image pourra avoir identifiant différent.

  3. La commande pour récupérer la version de l’image correspondant à l’étiquette. Si on regarde le qualificatif python:3.9, là encore on peut remarquer que l’outil docker est directement proposé, et que le nom d’hôte du registre n’est pas explicite. Dans notre cas on préférerait plutôt écrire podman pull docker.io/python:3.9

  4. La taille de l’image.

On peut voir que plusieurs images différentes sont associées ici à l’étiquette 3.9, qui se distinguent par la colonne OS/Arch. Cela indique que ce registre de conteneurs est capable de fournir des images pour différentes sortes d’environnement d’accueil. Par exemple, linux/amd64 correspond à un environnement d’accueil avec Linux comme système d’exploitation et AMD64 type de microprocesseur, tandis que linux/arm/v5 correspond à un environnement avec microprocesseur de type ARM (utilisé par exemple dans la plupart des téléphones).

Dans ce cours toutes les manipulations seront effectuées avec des images linux/amd64. C’est le moteur de conteneurisation qui indique automatiquement au registre quel type d’environnement il possède, afin de récupérer toujours une image compatible (si elle existe).

D’autres informations sont présentes, mais seront laissées de côté pour ce cours.

On trouve également des registres de conteneurs importants adossés à deux parmi les principales solutions de forges logicielles commerciales :

  • La plateforme GitHub fournit un registre de conteneurs gratuit qui permet de stocker des images de conteneurs directement au sein d’un projet de développement hébergé sur Github. Le nom d’hôte du regist de conteneurs est ghcr.io.

  • La plateforme Gitlab fournit elle-aussi un registre de conteneurs gratuit qui permet de stocker des images de conteneurs directement au sein d’un projet de développement hébergé sur Gitlab. Le nom d’hôte du registre de conteneurs est registry.gitlab.com.

Enfin, dans le cadre de ce cours, nous allons faire usage d’un registre de conteneurs "privé" adossé à l’instance Gitlab de Nantes Université. Le nom d’hôte de ce registre de conteneurs est docker-registry.univ-nantes.fr.

Vous êtes peut-être confus sur l’existence d’un Gitlab à l’adresse https://gitlab.com/ et d’un Gitlab "Nantes Université" à l’adresse https://gitlab.univ-nantes.fr/ ? Pas de panique, quelques explications.

Gitlab Inc. est le nom d’une entreprise qui possède deux activités principales :

  • D’une part Gitlab Inc. développe un logiciel appelé Gitlab, à savoir un logiciel qui permet d’héberger une forge logicielle complète pour créer et gérer des projets de développement logiciel, avec notamment gestion de tickets, de versionnage de code, ou de stockage d’artefacts pour des projets. Parmi les artefacts que l’on peut stocker pour un projet, on peut stocker les images de conteneurs du projet, ce qui permet à une instance Gitlab d’agir en tant que registre de conteneurs. Gitlab est un logiciel libre qui peut être facilement téléchargé et installé par quiconque ayant besoin de sa propre forge logicielle. Le code source du logiciel Gitlab est disponible ici.

  • D’autre part, Gitlab Inc. propose un service commercial également appelé Gitlab, et disponible à l’adresse https://gitlab.com. Ce service est une installation particulière du logiciel Gitlab hébergé directement par les serveurs de l’entreprise Gitlab Inc., et qui représente une des principales activités commerciales de l’entreprise Gitlab Inc., qui propose des offres payantes pour héberger des projets de développement de grande taille. C’est un service comparable à celui offert par la plateforme commerciale Github, avec pour différence que cette dernière ne fonctionne pas à l’aide d’un logiciel libre (le code source de Github est propriétaire).

Et pour ce qui est l’instance Gitlab de Nantes Université, accessible à l’adresse https://gitlab.univ-nantes.fr/, c’est simplement une installation du logiciel Gitlab qui a été effectuée par le service informatique de l’université, à destination des personnels et étudiants.

5. Quelles images utiliser comme base ?

Lorsqu’on souhaite déployer un logiciel qui n’est pas déjà disponible sous la forme d’une image, il est important de prendre le temps de réfléchir à l’image de base à utiliser pour un conteneur. Cette image de base va non seulement définir la première couche du mini-environnement dans lequel notre logiciel va s’exécuter, mais va également nous fournir des outils qui peuvent servir à construire notre logiciel, et également des outils utiles pour maintenir et déboguer notre conteneur.

Il est par exemple intéressant de se poser les questions suivants :

  • Ai-je besoin de construire le logiciel ?

  • Est-ce que mon logiciel a besoin d’un environnement Linux complet, avec tous les logiciels système habituels (ligne de commande, outils d’interface réseau, etc) ?

  • Comment installer efficacement les dépendances de mon logiciel ? Faut-il mieux obtenir les dépendances en amont et les dépose dans le conteneur lors de sa création ? Ou bien est-ce qu’on préférera utiliser un installeur de paquets existant (comme apt, apk ou dnf) fourni au préalable par le système d’exploitation de l’image ?

  • Quels outils seraient utiles dans le conteneur s’il est nécessaire de déboguer le logiciel qui s’y trouve, ou bien d’effectuer des tâches de maintenance sur ce logiciel ?

  • Comment faire pour limiter la taille de mon conteneur ?

Parmi les très nombreuses images de base disponibles sur un registre de conteneurs public comme le Docker Hub, on trouvera au moins deux catégories d’images qui sont très souvent utilisées comme images de base :

  • Les images basées sur des distributions Linux fournissent chacune un environnement minimal avec tous les logiciels et bibliothèques installés par défaut au sein d’une distribution Linux. Ainsi ces images contiennent toujours un système de fichiers Linux classique (avec bin, home, usr, etc.), ainsi que les outils utilitaires UNIX habituels (grep, ls, ps, etc.). Une image de ce type fournit également un installeur de paquets qui permet de facilement télécharger de nouveaux logiciels fournis par la distribution Linux considérée. On trouve de telles images pour la plupart des distributions Linux classiques, comme Debian, Ubuntu, ou Fedora. Une image de base très populaire est celle qui repose sur la distribution Alpine, car cette dernière est conçue pour occuper un espace disque minimal (ce qui permet de réduire la taille de l’image finale construite), et conçue pour une meilleure sécurité.

  • Les images dédiées à des technologies particulières fournissent chacune un environnement déjà prêt pour faire fonctionner un logiciel développé avec une technologie particulière. Ces images sont généralement elle-mêmes basées sur les images décrites dans le point précédent, et ajoutent les exécuteurs et bibliothèques adaptés à un contexte particulier. On trouve de telles images pour différents langages et technologies comme R, Python (avec l’outil pip), Node.js (avec l’outil npm), ou encore Java (avec différentes images comme Eclipse Temurin pour l’exécution de programmes Java avec la plateforme d’exécution Temurin, Gradle pour la compilation de programmes Java avec l’outil Gradle, ou encore Maven pour la compilation de programmes Java avec l’outil Maven).

On retiendra qu’il est souvent préférable d’utiliser la seconde catégorie d’images car elle "prémâche" efficacement le travail de préparation, en fournissant directement les compilateurs et/ou exécuteurs nécessaires pour nos logiciels.

Même si les images de base dédiées à des technologies sont très pratiques, elles peuvent être inadaptées lors de déploiements pour des raisons de sécurité. En effet, il est recommandé d’éviter de déployer des conteneurs qui contiennent des outils de construction de logiciels, comme des compilateurs ou des gestionnaires de paquets − pour plus d’informations se référer aux recommandations de l’ANSSI concernant les conteneurs. Nous reparlerons de ces aspects en fin de semestre lorsque nous traiterons de comment construire une image de conteneur.