Conteneurs

Pierre Ferrari

Cmglee_Container_City_2

source

Identification du module

Utiliser un service avec des conteneurs

Conteneurs

Un conteneur Linux est un terme générique pour une implémentation d’une forme de virtualisation au niveau du système d’exploitation. Il s’agit en effet d’une manière d’isoler un ou plusieurs processus qui tournent en leur créant un environnent qui sera différent, parfois à l’extrême, des autres processus tournant sur le même système d’exploitation.

Linux ne contient pas un système d’isolation unique, mais plusieurs mécanismes qui ont évolué au fil des années. Ces mécanismes sont utilisés ensemble pour générer cet effet d’isolation. Les mécanismes principaux du noyau Linux, sont les espaces de noms (namespace) et les groupes de contrôle (cgroups).

Les espaces de nom permettent au système de restreindre les ressources que voient les processus conteneurisés et garantissent qu’aucun d’entre eux ne peut interférer avec un autre.

Les groupes de contrôle quand à eux permettant de limiter la quantité des ressources utilisables par les processus conteneurisés.

Le « conteneur » peut ainsi définir par exemple; un système de fichiers qui sera différent, un nom d’hôte différent, limiter l’accès du processus à la mémoire, lui désigner une partie d’un nombre restreint des processeurs présents sur la machine.

À la différence de la virtualisation classique, une seule copie du système d’exploitation tourne sur la machine, les conteneurs utilisent ainsi moins de ressources en termes de mémoire.

virtualization-vs-containers

Lorsque vous exécutez de nombreux processus et applications différents sur une seule machine, il est important que chaque processus soit isolé, principalement pour la sécurité. Lorsque vous isolé un processus vous créez un conteneur. On peut donc donner la définition suivante:

Un conteneur c’est un ou plusieurs processus qui sont isolés au sein d’un même système d’exploitation

On voit la différence avec une machine virtuelle, dans l’image ci-dessus. À gauche, une machine virtuelle contient aussi l’application (APP) mais l’application fonctionne dans un système d’exploitation virtualisé (GUEST OS). Ces systèmes d’exploitations voient des composants matériels tels que des cartes réseaux, des cartes sons et tout les autres matériels virtuelles. Ils n’ont pas connaissances des vrais matériels. Toutes ces machines virtuelles fonctionnent sous les ordres d’un hyperviseur (HYPERVISOR) qui est gérer par un système d’exploitation de base, celui de l’hôte (HOST OPERATING SYSTEM).

À droite, le conteneur correspond à un des carrés pointillés. On y trouve une ou plusieurs applications (APP) avec leur fichiers nécessaires au fonctionnement de l’application (SUPPORTING FILES RUNTIME) isolé du reste du système (pointillés) et partageant le même système d’exploitation (HOST OPERATING SYSTEM). L’application, si elle doit accéder au matériel, le fait directement sur le vrai matériel. Pas de virtualisation dans ce cas.

Pourquoi isolé ?

On peut se demander pourquoi isolé un processus ? Il y a deux aspects qui entre en compte dans l’isolation.

Ressources

En isolant le processus, on évite de compromettre toute l’application. Imaginez votre navigateur ayant 3 onglets ouverts sur différents site web. Si un onglet plante, on ne veut pas que toute l’application plante. On souhaite simplement pouvoir fermer l’onglet défectueux. C’est le même principe avec un hébergement web. Si vous hébergez 150 sites web et qu’un site web plante, vous ne voulez pas que les 149 autres sites web soient inaccessible.

Dans ces exemples, on va faire de l’isolation de ressources par exemple en allouant 10% du temps processeur d’un seul cœur à un processus. Si ce dernier plante, il ne plante que 10% d’un cœur. Le reste du matériel est toujours disponible pour les autres processus.

De même, imaginons le programme suivant dans lequel le programmeur a oublié d’incrémenter la variable i. Ce faisant, il a créer une boucle sans fin qui va instancier des objets. Si le programme n’a pas de limite, il ira jusqu’à utiliser toute la mémoire RAM au détriment des autres processus qui finiront par planter.

public static void Main(string[] args)
{
    int i = 0;
    List<MonObjet> lst = new List<MonObjet>();
    while(i < 10)
    {
        lst.Add(new MonObjet());
    }
}

En utilisant les cgroups on peut écrire la valeur 2097152 dans le fichier memory.limit_in_bytes du processus appConteneur.

echo $(( 2048 * 1024 )) | sudo tee /sys/fs/cgroup/memory/appConteneur/memory.limit_in_bytes #2 MB RAM

et ainsi limiter la taille maximal de mémoire disponible pour le processus appConteneur à 2MB. Si le processus tente d’obtenir plus de mémoire, le système d’exploitation refusera et seul le processus appConteneur en subira les conséquences.

Sécurité

Un autre aspect de l’isolation concerne la sécurité. En effet, si plusieurs processus partagent les mêmes ressources, et qu’un processus contient une faille de sécurité, en passant par le processus vulnérable, il sera possible d’obtenir des informations sensibles des autres processus.

Imaginons deux programmes utilisant les mêmes points de montage (les mêmes partitions de disques). L’application vulnérable n’est pas intéressante, c’est un simple jeu de morpion en ligne. Par contre, l’autre application est une site de vente en ligne contenant une base de données de clients avec leur numéros de carte de crédit. En passant par le jeu de morpion, l’attaquant aura accès aux données du site de vente en ligne puisque les deux applications partagent les mêmes points de montage et donc les mêmes emplacements de stockage.

Cette fois, en utilisant les namespace, on pourra isolé les ressources matériels des deux applications. Chaque application ne verra que ses points de montage. Si une application est vulnérable, elle n’exposera à l’attaquant que ses données personnelles.

Démonstration

Pour cette démonstration nous allons utiliser une machine Linux avec la distribution Ubuntu installée. Nous utiliserons également une autre distribution nommée Alpine.

alpine

Notes

Il faut bien faire la distinction entre Système d’exploitation ou OS ou encore Kernel et Distribution.

Le système d’exploitation se nomme Linux. Son travail est de rendre possible l’utilisation du matériel physique présent dans la machine. Il se matérialise par deux fichiers qui se trouvent dans le répertoire /boot et qui se nomment vmlinuz-5.X.Y-ZZ-generic et initrd.img-5.X.Y-ZZ-generic.

La distribution, c’est l’ensemble des autres fichiers et répertoires présent dans une installation. Son but est de fournir les binaires nécessaires à l’utilisation du matériel.

Ainsi, on peut dire que Linux rend utilisable une carte réseau et que le binaire ip fournit par la distribution dans le répertoire /usr/bin/ en permet la configuration.

État des lieux

Nous sommes actuellement dans le répertoire personnel de l’utilisateur ubuntu. Il s’agit du répertoire /home/ubuntu. Dans ce répertoire on trouve un dossier nommé alpine. Ce dossier contient toute la distribution Alpine.

ubuntu@m347:~$ ls alpine/
bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

On peut déjà remarquer plusieurs choses:

Isolation

Isolons maintenant le répertoire alpine et faisons en sorte que la racine du disque / dans notre système isolé devienne /home/ubuntu/alpine. Pour faire ce travail, nous devrons dialoguer avec Linux grâce à l’outil unshare. Cet outil va nous permettre de demander à Linux de changer d’espace de nom et ainsi isolé notre dossier. On peut travailler sur plusieurs espaces de nom en fonction de ce que l’on souhaite faire. Vous trouverez de plus amples informations au sujet des espaces de nom sur internet et notamment sur ce site linuxembedded.fr.

Sans entrer trop dans le détail, nous allons demander au noyau de nous isoler de tout:

Une fois que nous serons isolé, nous changerons la racine actuelle / par notre répertoire /home/ubuntu/alpine.

Ça donne ceci :

ubuntu@m347:~$ cd alpine/
ubuntu@m347:~/alpine$ sudo unshare --mount --uts --ipc --net --pid --fork  --mount-proc --root /home/ubuntu/alpine sh
/# ls
bin    dev    etc    home   lib    media  mnt    opt    proc   root   run    sbin   srv    sys    tmp    usr    var

Comme on peut le voir, la racine actuelle est maintenant celle du répertoire alpine. Il est impossible d’aller voir au-dessus. En réalité, un utilisateur que l’on placerait maintenant dans ce shell n’aurait même pas conscience de l’existence de la distribution Ubuntu de base. Il en est de même pour le programmes que nous pouvons lancer. Si on reprend les programmes précédent ls et ip, nous pouvons faire un premier constat qui est que les seuls version de programme utilisable sont ceux de la distribution Alpine puisque nous n’avons plus accès aux autres.

/# ls /home/ubuntu/
ls: /home/ubuntu: No such file or directory

Si on regarde la version actuelle de notre distribution on obtient

/# cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.21.3
PRETTY_NAME="Alpine Linux v3.21"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"

Alors qu’en dehors de notre isolation on obtient

ubuntu@m347:~$ cat /etc/os-release 
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Pour ce qui est des interfaces réseaux, dans notre système isolé on obtient uniquement l’interface de loopback qui est nécessaire au fonctionnement d’une grande majorité de programme mais on peut constater que ce n’est pas la même qu’en dehors puisque actuellement cette interface est DOWN

/# ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

## On la met en `UP`
/# ip link set lo up
/# ip link 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Alors qu’en dehors nous avions une interface de loopback up et une interface réseau externe.

ubuntu@m347:~$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enx908d6e23017f: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 90:8d:6e:23:01:7f brd ff:ff:ff:ff:ff:ff

On peut faire les mêmes remarques concernant les processus. Dans la partie isolée on ne voit que deux processus, un bash dans lequel on est entrain d’écrire nos commandes et le processus ps qui liste les processus

/# ps
PID   USER     TIME  COMMAND
    1 root      0:00 -bash
    8 root      0:00 ps

Alors qu’en dehors il y a beaucoup d’autres processus…

ubuntu@m347:~$ ps fa
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:01 /sbin/init maybe-ubiquity
      2 ?        S      0:00 [kthreadd]
      3 ?        I<     0:00  \_ [rcu_gp]
      4 ?        I<     0:00  \_ [rcu_par_gp]

...

   1649 pts/0    S      0:00  |           \_ sudo unshare --mount --uts --ipc --net --pid --fork --mount-proc --root /home/debian/alpine sh
   1650 pts/0    S      0:00  |               \_ unshare --mount --uts --ipc --net --pid --fork --mount-proc --root /home/debian/alpine sh
   1651 pts/0    S+     0:00  |                   \_ sh

Il est intéressant de constater dans ces deux dernières commandes que le sh est le même mais une fois vu de la partie isolée (dans le conteneur) et donc avec comme PID le numéro 1 et une fois vu du système complet (en dehors du conteneur). Dans le système complet on voit que ce sh est un enfant de unshare. Dans le système complet ce sh n’est rien d’autre qu’un processus isolé.

Conclusion unshare

On peut constater que le principe de base de la conteneurisation c’est l’isolation de processus. On peut aussi constater que cette isolation est assez simple à mettre en place. Il suffit de demander au noyau Linux de créer un nouveau namespace et de créer un nouveau cgroup pour isoler les ressources. Une fois l’isolation mise en place, depuis le conteneur, on ne peut plus voir ce qui existe en dehors de celui-ci.

Les logiciels de conteneurisation

Toute la démonstration ci-dessus est assez compliquée et le système isolé n’est de loin pas parfais. Si on veut créer des conteneurs, on ne va pas procéder ainsi à moins d’être un spécialiste, et encore…

Pour faire ce travail de conteneurisation, on utilisera des logiciels qui sont spécialisés dans ce travail. Il en existe beaucoup, chacun avec ses avantages et ses inconvénients. Il faut cependant garder à l’esprit que tout ces logiciels font le même travail que la démonstration ci-dessus, c’est-à-dire qu’ils demandent au noyau Linux l’isolation d’un ou plusieurs processus.

Parmi les logiciels de conteneurisation on trouve des librairies de bas niveau qui s’occupe du dialogue avec le noyau. Leur travail consiste entre autre à gérer les espaces de nom namespace et les groupes de contrôle cgroups.

Au dessus de ces librairies, on trouve les logiciels intermédiaires qui vont se charger de plusieurs tâches comme la gestion des /images, la création de ces dernières, la gestion des configurations des conteneurs, la gestion de l’état du conteneur, la sauvegarde etc.

Et encore au-dessus, on trouve des outils de gestion de ces conteneurs. Ces outils vont se charger du dialogue entre l’utilisateur et les librairies situées en-dessous.

Notes

Il existe d’autres logiciels de conteneurisation qui se situe à mi-chemin entre des vrais conteneurs et des machines virtuelles. Parmi ceux-ci on trouve:

Toutes ces technologies utilisent des micro-machines virtuelles pour permettre un isolation plus forte entre les conteneurs et le système d’exploitation. Elles nécessitent des /images de base spécifique et subissent des pertes de performances inhérentes aux machines virtuelles même si celles-ci sont optimisées au maximum et très légères.

Timeline

Release

Pourquoi utiliser des conteneurs ?

Pour répondre à cette question nous allons prendre quelques cas de figure.

Cas 1: Développement

Imaginons deux développeurs PHP qui sont embarqués dans le même projet. Le premier possède une machine Apple et le second travail sur un poste Ubuntu. Le serveur de déploiement fonctionne sur une machine Linux. Il est fournit avec un serveur web apache en version 2.4.41 et une base de données mariadb 10.10. Il fonctionne avec PHP 7.4.26.

Apple

Le développeur Apple choisi de mettre en place l’environnement de travail en utilisant MAMP. MAMP est fourni avec les versions suivantes:

Ubuntu

Le développeur Ubuntu 22.04 installera les paquets officiels de la distribution, à savoir:

Apple Ubuntu Serveur final
PHP 8.1.13 PHP 8.1.2 PHP 7.4.26
mySQL 5.7.39 mySQL 8.0.28 MariaDB 10.10
Apache 2.5.54 Apache 2.5.52 Nginx 1.27.4

En résumé, on peut dire qu’on se trouve avec un beau mélange de versions de logiciels et de systèmes d’exploitations qui ont chacun leur particularités. Il y aura donc inévitablement des problèmes qui surviendront et qui seront indépendant de notre développement.

Saviez-vous qu’un retour à la ligne sur :

Ou encore qu’un é sur

Et encore que la racine d’un système de fichier sur :

Mais aussi que les chemins d’accès aux fichiers sur :

Avec de la conteneurisation

L’idée de la conteneurisation dans ce cas, c’est de pouvoir mettre plusieurs services à disposition. Ces services seront isolés du système d’exploitation et de ses autres services. Il sera donc possible de mettre en place un environnement de développement similaire à l’environnement de production.

Ainsi, chaque développeur qui participe au projet installera localement le même conteneur qui est l’équivalent du serveur final. Ils développeront tous sur les mêmes versions.

Cas 2: Production

On peut également imaginer qu’un développeur souhaite mettre à disposition son logiciel. Ce dernier à besoin de certaines librairies pour fonctionner. L’utilisateur qui souhaite utiliser ce logiciel ne souhaite pas installer toutes les librairies nécessaires. En effet, il n’a pas confiance en une des librairie et il ne veut pas l’installer dans son système. Comme il souhaite quand même utiliser le logiciel, il va installer le conteneur qui contient le logiciel et toutes les librairies nécessaires. La librairie qui ne lui inspire pas confiance ne sera pas installée dans son système. Elle sera isolée dans le conteneur.

Prenons l’exemple du logiciel funbox. Ce logiciel permet d’effectuer plusieurs animations dans son terminal. Pour pouvoir réaliser toutes ces animations, il a besoin d’une liste conséquente de logiciels annexes.

Liste des 43 logiciels nécessaires

Pas sûr que l’utilisateur soit d’accord d’installer tous ces logiciels sur son système. Il préfère installer le conteneur qui contient le logiciel et toutes les librairies nécessaires.

Cas 3: Firefox

Des conteneurs dans mon navigateur

Cas 4: Google

Cette information date un peu mais on comprend bien l’utilité des conteneurs pour des géants comme Google.

2 milliards de containers générés chaque semaine. (la page n’est plus disponible)

Révision

💬 Peut-on faire une différence entre une machine virtuelle et un conteneur ?

Oui, la VM fonctionne sous les ordres d’un hyperviseur avec du matériel simulé et son propre SE

💬 Quelle est la différence entre le système d’exploitation Linux et une distribution Linux (par exemple Alpine) ?

Linux est un SE, Alpine est une distribution basée sur Linux dont l’objectif est de fournir des logiciels permettant de travailler avec le SE Linux

💬 Est-il possible de faire fonctionner un conteneur avec un système d’exploitation différent de celui de la machine hôte ?

NON

💬 Qu’est-ce qu’un conteneur ?

C’est un processus isolé

💬 Comment peut-on allouer 10% d’un cœur de processeur à un conteneur ?

On ne peut pas démonter un processeur pour ne fournir qu’une partie de la puce mais on peut travailler sur le temps disponible. Si le processeur n’a rien d’autre à faire, on aura 100% du processeur à disposition mais s’il est occupé à faire d’autre chose, il nous restera que 10% de son temps pour nos travaux. Par exemple, sur 1 minute de temps le processeur travaillera que 6 secondes pour nous

💬 Il est écrit qu’on peut limiter la taille mémoire allouée à un conteneur à l’aide des cgroups en utilisant, par exemple, la directive memory.limit_in_bytes. Où trouve-t-on la documentation officielle de cette directive ?

kernel.org

💬 Qu’est-ce qu’un espace de nom ?

C’est la partie du SE qui nous permet d’isoler les resources matériels comme les points de montage (mount), les identifiants de processus etc.

💬 Qu’est-ce qu’un groupe de contrôle ?

C’est la partie du SE qui nous permet de limiter l’accès aux resources matériels tels que le temps processeur, la bande passante etc.

💬 Quelle différence peut-on faire en le programme runc et le programme docker ?

docker est le programme de haut niveau qui simplifie l’utilisation de runc. runc est le programme qui dialogue avec le SE pour isolé des processus

💬 Qu’est-ce qu’un daemon comme par exemple containerd ?

Un démon est un programme qui ne se voit pas et qui est en écoute d’instruction provenant de programmes de plus haut niveau. Une fois les instructions reçues, le démon exécute des tâches en relation avec les instructions

Objectif pratique

L’objectif de la pratique c’est d’être capable de mettre en place un conteneur avec un programme qui tourne dedans et ce, indépendamment de la technologie utilisée, docker, podman, LXD ou Incus.

Nous débuterons la mise en pratique des conteneurs avec le logiciel qui est le plus connu et qui se nomme docker.

Présentation du concepteur de Docker

Présentation de docker

Notes

Le logiciel de conteneurisation d’environnement qui se nomme Incus est développé par un ancien élève du CPLN qui est passé par la même formation que vous. Il se nomme Stéphane Graber. Il est le leader du projet Incus anciennement LXD. LXD c’est, par exemple, ce qui tourne sur les Chromebook par défaut pour faire fonctionner Linux et les Chromebook c’est quelques centaines de milliers d’étudiants aux états-uni.

Incus se veut être un système de virtualisation d’environnement. Attention ici au mot virtualisation car il s’agit bien de conteneur.

Docker était à la base un logiciel monolithique c’est-à-dire, un gros logiciel qui faisait tout. Avec le temps et pour des raisons de compatibilité avec d’autres logiciels, docker s’est décomposé en plusieurs morceaux comme on peut le voir sur l’image ci-dessous:

docker-stack

Vous travaillerez avec la couche de plus haut niveau qui est le docker CLI. La CLI fera des requêtes au daemon qui lui-même fera des requêtes à containerd. containerd à son tour passera par shim pour finalement accéder à runc. C’est runc qui est réellement en charge de créer les conteneurs.

Tout cela sera complètement transparent pour vous. Vous n’aurez pas à vous soucier de tout cela. Vous utiliserez uniquement la CLI de docker. Par contre, vous verrez par la suite que cette couche logiciel à posé (et pose toujours) des problèmes de sécurité.

Command-Line Interface

Le travail dans la CLI suppose la manipulation aisée de l’interface de commande. Il est donc important de connaître les commandes de base de la CLI telles que la manipulation de fichier, la navigation dans les répertoires, la gestion des droits etc.

Pour vous aidez dans cette tâche, j’ai mis ci-dessous un résumé des commandes de base utilisée dans un terminal. Vous pouvez également vous référer à la documentation officielle de Ubuntu ou de Debian.

Commandes de base

Source

COMMANDES PRINCIPALES

Touche/Commande Description
cd [répertoire] Change de répertoire ex: cd Documents
cd Répertoire personnel de l’utilisateur (home)
cd ~ Répertoire personnel de l’utilisateur (home)
cd / Racine du disque dur
cd - Répertoire précédent
ls Liste non détaillée des fichiers et dossiers du répertoire en cours
ls -l Liste détaillée des fichiers et dossiers du répertoire en cours
ls -a Liste incluant les fichiers cachés
ls -lh Liste détaillée avec l’unité pour la taille des fichiers
ls -R Liste le contenu de la totalité du répertoire en cours incluant les sous-dossiers et de manière récursive
sudo [commande] Lance la commande avec les privilèges de sécurité du superuser (Super User DO)
top Affiche les processus actifs. Touche q pour quitter
nano [fichier] Ouvre le fichier avec l’éditeur de texte nano
clear Efface tout l’écran
reset Réinitialise le terminal

COMMANDES CHAINEES

Touche/Commande Description
[commande-a]; [commande-b] Lance la commande A puis la commande B peu importe le succès ou non de la commande A
[commande-a] && [commande-b] Lance la commande B si la commande A a réussi
[commande-a] || [commande-b] Lance la commande B si la commande A a échoué
[commande-a] & Lance la commande A en arrière plan

COMMANDES EN FLUX DE REDIRECTION (PIPE)

Touche/Commande Description
[commande-a] | [commande-b] Lance la commande A qui envoie son résultat à la commande B. Par exemple : ls | grep C affiche la liste des fichiers et dossiers qui contiennent la lettre C

HISTORIQUE DE COMMANDE

Touche/Commande Description
history N Affiche l’historique des N commandes tapées précédemment
Ctrl + R Recherche interactivement dans l’historique des commandes
![valeur] Exécute la dernière commande tapée qui commence par ‘valeur’
![valeur]:p Affiche à l’écran la dernière commande tapée qui commence par ‘valeur’
!! Exécute la dernière commande tapée
!!:p Affiche à l’écran la dernière commande tapée

GESTION DE FICHIERS

Touche/Commande Description
touch [fichier] Crée un nouveau fichier
pwd Affiche le chemin complet du répertoire en cours
. Répertoire en cours, par exemple ls .
.. Répertoire parent c’est à dire qui contient le répertoire en cours, par exemple ls ..
ls -l .. Liste détaillée du répertoire parent
cd ../../ Monte de 2 niveaux
cat Concatène à l’écran
rm [fichier] Supprime un fichier, par exemple rm data.tmp
rm -i [fichier] Supprime un fichier avec demande de confirmation
rm -r [rép] Supprime le répertoire et son contenu
rm -f [fichier] Force la suppression du fichier sans demande de confirmation
cp [fichier] [nouveauFichier] Copie fichier vers nouveauFichier
cp [fichier] [répertoire] Copie fichier dans répertoire
mv [fichier] [nouveauFichier] Déplace/Renomme fichier vers nouveauFichier par exemple mv fichier1.ad /tmp

GESTION DES REPERTOIRES

Touche/Commande Description
mkdir [rép] Crée un nouveau répertoire
mkdir -p [rép]/[rép] Crée un répertoire et un sous-répertoire dans la foulée
rmdir [rép] Supprime le répertoire (uniquement si le répertoire est vide)
rm -R [rép] Supprime le répertoire et son contenu
less [fichier] Affiche le contenu du fichier par morceau
[commande] > [fichier] Envoie le résultat de la commande vers le fichier. Attention le contenu du fichier est écrasé
[commande] >> [fichier] Ajoute le résultat de la commande au contenu existant du fichier
[commande] < [fichier] Indique à la commande de lire le contenu du fichier

RECHERCHE

Touche/Commande Description
find [rép] -name [expression] Recherche les fichiers dont le nom est conforme à l’expression dans le répertoire spécifié, par exemple find /Utilisateurs -name "fichier.txt"
grep [expression] [fichier] Recherche toutes les lignes contenant l’expression, par exemple grep "Tom" fichier.txt
grep -r [expression] [rép] Recherche récursivement dans tous les fichiers du répertoire spécifié toutes les lignes qui contiennent l’expression
grep -v [expression] [fichier] Recherche toutes les lignes qui ne contiennent PAS l’expression
grep -i [expression] [fichier] Recherche toutes les lignes qui contiennent l’expression sans tenir compte de la casse (majuscules/minuscules)

AIDE

Touche/Commande Description
[commande] -h Affiche l’aide pour la commande
[commande] --help Affiche l’aide pour la commande
info [commande] Affiche l’aide pour la commande
man [commande] Affiche le manuel d’utilisation de la commande
whatis [commande] Décris ce que fait la commande en 1 seule ligne
apropos [expression] Recherche les commandes dont la description contient l’expression

Installation de docker

L’installation de docker peut se faire de différente manière en fonction de l’environnement dans lequel vous vous trouvez. Dans ce cours, nous utiliserons une machine virtuelle Ubuntu LTS. L’installation se fera alors ainsi.

Une partie des étapes de la documentation officielle est inutile
Pour se connecter en ssh en forçant l’utilisation d’un mot de passe ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password ubuntu@A.B.C.D
ubuntu@m347:~$ sudo apt update
[sudo] password for ubuntu: 
ubuntu@m347:~$ sudo apt upgrade
ubuntu@m347:~$ sudo apt autoremove
ubuntu@m347:~$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
ubuntu@m347:~$ echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
ubuntu@m347:~$ sudo apt update
ubuntu@m347:~$ sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  containerd.io docker-buildx-plugin docker-ce docker-ce-cli docker-ce-rootless-extras docker-compose-plugin docker-scan-plugin
  libltdl7 libslirp0 pigz slirp4netns

...
installation_docker.cast

Parmis les paquets installés, on retrouve des logiciels qu’on a déjà vu précédemment tel que docker-ce-cli qui est la CLI de docker ainsi que containerd.io qui est un logiciel qui permet de gérer les conteneurs.

Une fois cette étape réalisée nous aurons à disposition un ensemble de commandes accessibles via la commande de base docker

voir la commande docker
ubuntu@m347:~$ docker

Usage:  docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Options:
      --config string      Location of client config files (default "/home/ubuntu/snap/docker/2746/.docker")
  -c, --context string     Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket(s) to connect to
  -l, --log-level string   Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
      --tls                Use TLS; implied by --tlsverify
      --tlscacert string   Trust certs signed only by this CA (default "/home/ubuntu/snap/docker/2746/.docker/ca.pem")
      --tlscert string     Path to TLS certificate file (default "/home/ubuntu/snap/docker/2746/.docker/cert.pem")
      --tlskey string      Path to TLS key file (default "/home/ubuntu/snap/docker/2746/.docker/key.pem")
      --tlsverify          Use TLS and verify the remote
  -v, --version            Print version information and quit

Management Commands:
  builder     Manage builds
  buildx*     Docker Buildx (Docker Inc., v0.8.2)
  compose*    Docker Compose (Docker Inc., v2.5.0)
  config      Manage Docker configs
  container   Manage containers
  context     Manage contexts
  image       Manage /images
  manifest    Manage Docker image manifests and manifest lists
  network     Manage networks
  node        Manage Swarm nodes
  plugin      Manage plugins
  secret      Manage Docker secrets
  service     Manage services
  stack       Manage Docker stacks
  swarm       Manage Swarm
  system      Manage Docker
  trust       Manage trust on Docker /images
  volume      Manage volumes

Commands:
  attach      Attach local standard input, output, and error streams to a running container
  build       Build an image from a Dockerfile
  commit      Create a new image from a container's changes
  cp          Copy files/folders between a container and the local filesystem
  create      Create a new container
  diff        Inspect changes to files or directories on a container's filesystem
  events      Get real time events from the server
  exec        Run a command in a running container
  export      Export a container''s filesystem as a tar archive
  history     Show the history of an image
  /images      List /images
  import      Import the contents from a tarball to create a filesystem image
  info        Display system-wide information
  inspect     Return low-level information on Docker objects
  kill        Kill one or more running containers
  load        Load an image from a tar archive or STDIN
  login       Log in to a Docker registry
  logout      Log out from a Docker registry
  logs        Fetch the logs of a container
  pause       Pause all processes within one or more containers
  port        List port mappings or a specific mapping for the container
  ps          List containers
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rename      Rename a container
  restart     Restart one or more containers
  rm          Remove one or more containers
  rmi         Remove one or more /images
  run         Run a command in a new container
  save        Save one or more /images to a tar archive (streamed to STDOUT by default)
  search      Search the Docker Hub for /images
  start       Start one or more stopped containers
  stats       Display a live stream of container(s) resource usage statistics
  stop        Stop one or more running containers
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
  top         Display the running processes of a container
  unpause     Unpause all processes within one or more containers
  update      Update configuration of one or more containers
  version     Show the Docker version information
  wait        Block until one or more containers stop, then print their exit codes

Run 'docker COMMAND --help' for more information on a command.

To get more help with docker, check out our guides at https://docs.docker.com/go/guides/

docker info la configuration

On peut connaître la configuration de docker avec la commande docker info. Cette commande nous donne des informations sur la version de docker, le nombre de conteneurs, d’images, de volumes, de système de fichier etc.

voir la commande docker info
ubuntu@m347:~$ docker info
Client:
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.10.2
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.16.0
    Path:     /usr/libexec/docker/cli-plugins/docker-compose
  scan: Docker Scan (Docker Inc.)
    Version:  v0.23.0
    Path:     /usr/libexec/docker/cli-plugins/docker-scan

Server:
 Containers: 3
  Running: 0
  Paused: 0
  Stopped: 3
 /images: 1
 Server Version: 23.0.1
 Storage Driver: vfs
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 2456e983eb9e37e47538f59ea18f2043c9a73640
 runc version: v1.1.4-0-g5fd4c4d
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 5.19.0-35-generic
 Operating System: Ubuntu 22.04.2 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 16
 Total Memory: 27.1GiB
 Name: m347
 ID: 6703d90e-dcfe-4d89-b583-9ff341b4d919
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false

docker run hello-world

C’est une tradition des cours informatiques, nous allons commencer par un Hello World. La commande pour démarrer un conteneur docker est docker run <nom de l'image>. L’image que nous allons utiliser se nomme hello-world. Elle est fournie par docker en tant qu’image de test.

ubuntu@m347:~$ docker run hello-world
docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/create": dial unix /var/run/docker.sock: connect: permission denied.
See 'docker run --help'.

…et on tombe sur un problème 😟. Nous n’avons pas les droits suffisants pour utiliser le socket docker. On peut le confirmer en lançant la même commande mais en tant que root.

ubuntu@m347:~$ sudo docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share /images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
On comprend dès à présent que les conteneurs docker sont des conteneurs qui fonctionne avec les droits root et que c’est un fonctionnement qui est dangereux 😱!

Dans la documentation officielle de docker, il nous propose de nous passer de la commande sudo en ajoutant notre compte dans le groupe docker ainsi :

Running Docker as normal user

By default, Docker is only accessible with root privileges (sudo). If you want to use docker as a regular user, you need to add your user to the docker group.

sudo usermod -a -G docker $USER
## logout and login again 

Warning: if you add your user to the docker group, it will have similar power as the root user. For details on how this impacts security in your system, see Surface attack

Maitenant que notre utilisateur est dans le groupe docker, on peut lancer la commande sans sudo ainsi docker run hello-world.

Il faut bien comprendre que maintenant, notre utilisateur à les mêmes pouvoirs que root pour le meilleur mais aussi pour le pire 😈

Installation et commandes de base

Exercices

James Bond

Vous connaissez certainement James Bond et Mister Q. Mister Q est le technicien qui fournit les gadgets à James Bond. Il est également un hacker hors paire capable de tout. Lorsqu’on le voit apparaître dans le film, il y a toujours un ou plusieurs écrans qui affiche des lignes de code.

C’est les écrans des hackers hollywoodiens. On va faire pareil.

Dustin Kirkland a crée une image docker qui transforme votre terminal en écran de hacker hollywoodien.

L’image ainsi que les instructions pour l’utilisé se trouve ici.

Funbox

Démarrez une des animations fournies pas la funbox

Disséquer le conteneur hello-world

L’objectif de cette manipulation est une approche top ↦ down d’un conteneur. Pour comprendre un conteneur, nous allons utiliser le conteneur de base Hello World de docker et le décortiquer pour le comprendre.

Pré-requis

Note

Docker n’est pas forcement configuré pour permettre des limitations dans les conteneurs. Par exemple, si on souhaite limiter la taille de la mémoire pour un conteneur. Pour connaître cette information il faut lancer la commande suivante

$ docker info | grep swap
WARNING: No swap limit support

Si la réponse est identique à celle ci-dessus, il faut modifier la configuration de la machine virtuelle en éditant le fichier /etc/default/grub et en modifiant la ligne suivante ainsi

GRUB_CMDLINE_LINUX_DEFAULT="maybe-ubiquity cgroup_enable=memory swapaccount=1"

Une fois le fichier modifier il faut lancer la commande de mise-à-jour de grub et redémarrer la machine virtuelle

$ sudo update-grub
$ sudo reboot

docker image

Pour obtenir l’image du conteneur, on utilise la commande docker image

ubuntu@m347:~$ docker image

Usage:  docker image COMMAND

Manage /images

Commands:
  build       Build an image from a Dockerfile
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Display detailed information on one or more /images
  load        Load an image from a tar archive or STDIN
  ls          List /images
  prune       Remove unused /images
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rm          Remove one or more /images
  save        Save one or more /images to a tar archive (streamed to STDOUT by default)
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE

Run 'docker image COMMAND --help' for more information on a command.

Comme on peut le voir, la commande permet de gérer les /images locales. On peut donc télécharger l’image du conteneur hello world à l’aide de la sous-commande pull. On peut également ajouter un tag pour savoir quelle image on souhaite parmi toutes celles disponible

docker_hello-world_tags

Nous choisirons celle par défaut qui correspond à la dernière latest

ubuntu@m347:~$ docker image pull hello-world:latest
latest: Pulling from library/hello-world
2db29710123e: Pull complete 
Digest: sha256:6d60b42fdd5a0aa8a718b5f2eab139868bb4fa9a03c9fe1a59ed4946317c4318
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest

On peut vérifier qu’elle est bien présente dans notre réserve d’images locales

ubuntu@m347:~$ docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
hello-world   latest    feb5d9fea6a5   5 months ago   13.3kB

On peut maintenant démarrer le conteneur via la sous-commande run. Cette commande instancie un conteneur en partant d’une image. On peut faire un parallèle avec la programmation lorsqu’on instancie un objet en partant d’une class. Par exemple:

Label monLabel = new Label();

La commande run fait le même travail mais pour un conteneur

ubuntu@m347:~$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share /images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Que c’est-il passé ? Et bien il suffit de traduire le message pour le comprendre

  1. Notre commande docker run à contacté le daemon docker en lui demandant de créer un conteneur basé sur l’image hello-world
  2. Le daemon est allé chercher l’image dans notre réserve. Si elle n’aurai pas été présente, il serait allé la chercher directement sur le Hub Docker
  3. Le daemon a ensuite instancié un conteneur. Dans ce conteneur, il se trouve un binaire qui affiche le message Hello from Docker. Le daemon a reçu ce message de la part du conteneur
  4. Le daemon a fait suivre ce message à la console depuis laquelle la demande de création de conteneur a été faite

On a donc assisté au démarrage d’un processus isolé dont le but est l’affichage du texte Hello from Docker.

docker inspect, l’anatomie du hello-world

Si on veut mieux comprendre ce conteneur on peut examiner sa configuration à l’aide de la commande docker inspect

ubuntu@m347:~$ docker inspect hello-world
[
    {
        "Id": "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412",
        "RepoTags": [
            "hello-world:latest"
        ],
...

        "ContainerConfig": {
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/hello\"]"
            ],

...

On peut voir que la configuration informe le daemon qu’il devra lancer la commande /bin/sh -c /hello. Autrement dit, il devra lancer l’exécution du binaire qui se nomme hello et qui se trouve à la racine du conteneur.

Essayons de voir ce binaire. Pour cela, on va demander à la commande de gestion des images de nous extraire l’image hello-world dans une archive.

ubuntu@m347:~$ docker image save hello-world > hello-world.tar
ubuntu@m347:~$ ls -l
total 28
-rw-rw-r-- 1 ubuntu ubuntu 24064 mars  22 12:14 hello-world.tar

Et si on regarde à l’intérieur de l’archive on trouve 3 fichiers et un dossier. Les noms ci-dessous sont volontairement raccourci pour une question de lisibilité

ubuntu@m347:~$ mkdir hello
ubuntu@m347:~$ tar xf hello-world.tar --directory hello
ubuntu@m347:~$ ls -l hello
total 16
drwxr-xr-x 2 ubuntu ubuntu 4096 sept. 24 01:47 c28b9 ... 912d5c
-rw-r--r-- 1 ubuntu ubuntu 1469 sept. 24 01:47 feb5d ... 6b3412.json
-rw-r--r-- 1 ubuntu ubuntu  207 janv.  1  1970 manifest.json
-rw-r--r-- 1 ubuntu ubuntu   94 janv.  1  1970 repositories

Le fichier repositories nous indique le nom du répertoire dans lequel se trouve le nécessaire au fonctionnement du conteneur

ubuntu@m347:~/hello$ cat repositories | jq .
{
  "hello-world": {
    "latest": "c28b9c2faac407005d4d657e49f372fb3579a47dd4e4d87d13e29edd1c912d5c"
  }
}

Le fichier manifest nous donne plus d’informations. On y trouve l’emplacement et le nom du fichier de configuration, les tags du dépôt de provenance et le Layers qui est l’endroit où se trouve notre binaire.

ubuntu@m347:~/hello$ cat manifest.json | jq .
[
  {
    "Config": "feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412.json",
    "RepoTags": [
      "hello-world:latest"
    ],
    "Layers": [
      "c28b9c2faac407005d4d657e49f372fb3579a47dd4e4d87d13e29edd1c912d5c/layer.tar"
    ]
  }
]

Le fichier de configuration feb5d ... 6b3412.json correspond à la configuration du conteneur. C’est la même que celle qu’on obtient avec la commande docker inspect hello-world un peu plus haut dans ce document.

Si on analyse le contenu du layer qui est également une archive, on y trouve le fichier binaire hello

ubuntu@m347:~/hello/c28b9 ... 912d5c$ mkdir tar_content
ubuntu@m347:~/hello/c28b9 ... 912d5c$ tar xf layer.tar --directory tar_content/
ubuntu@m347:~/hello/c28b9 ... 912d5c$ ls -l tar_content/
total 16
-rwxrwxr-x 1 ubuntu ubuntu 13256 sept. 24 01:47 hello

Et si on exécute ce fichier binaire ?

ubuntu@m347:~/hello/c28b9 ... 912d5c$ ls -l tar_content/hello

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share /images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Conclusion

L’image docker hello-world contient des fichiers de configurations ainsi qu’une archive empaquetant le binaire. Le fichier binaire est un programme ELF 64-bit dont les dépendances ont étés placées directement dans le fichier binaire (statically linked). Il est donc complètement autonome et ne dépend d’aucune librairies externes.

ubuntu@m347:~$ file tar_content/hello 
tar_content/hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

Quand on lance la commande docker run hello-world on démarre le binaire hello de manière isolée. Si le binaire hello cherchait, par exemple, à accéder au réseau ou à un fichier sur le disque, il ne pourrait pas le faire car il est isolé. Exactement de la même manière que notre programme bash dans le conteneur alpine que nous avons vu dans le cours précédent.

Bonus 🎁

On peut trouver le code source du programme hello-world sur le site github

#include <sys/syscall.h>
#include <unistd.h>

#ifndef DOCKER_IMAGE
    #define DOCKER_IMAGE "hello-world"
#endif

#ifndef DOCKER_GREETING
    #define DOCKER_GREETING "Hello from Docker!"
#endif

#ifndef DOCKER_ARCH
    #define DOCKER_ARCH "amd64"
#endif

const char message[] =
    "\n"
    DOCKER_GREETING "\n"
    "This message shows that your installation appears to be working correctly.\n"
    "\n"
    "To generate this message, Docker took the following steps:\n"
    " 1. The Docker client contacted the Docker daemon.\n"
    " 2. The Docker daemon pulled the \"" DOCKER_IMAGE "\" image from the Docker Hub.\n"
    "    (" DOCKER_ARCH ")\n"
    " 3. The Docker daemon created a new container from that image which runs the\n"
    "    executable that produces the output you are currently reading.\n"
    " 4. The Docker daemon streamed that output to the Docker client, which sent it\n"
    "    to your terminal.\n"
    "\n"
    "To try something more ambitious, you can run an Ubuntu container with:\n"
    " $ docker run -it ubuntu bash\n"
    "\n"
    "Share /images, automate workflows, and more with a free Docker ID:\n"
    " https://hub.docker.com/\n"
    "\n"
    "For more examples and ideas, visit:\n"
    " https://docs.docker.com/get-started/\n"
    "\n";

int main() {
    //write(1, message, sizeof(message) - 1);
    syscall(SYS_write, STDOUT_FILENO, message, sizeof(message) - 1);

    //_exit(0);
    //syscall(SYS_exit, 0);
    return 0;
}

On peut en générer un programme identique tel qu’ils le font ainsi

ubuntu@m347:~$ musl-gcc -Wl,--gc-sections -static source_code.c -o hello
ubuntu@m347:~$ strip --strip-all --remove-section=.comment hello
ubuntu@m347:~$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
ubuntu@m347:~$ ls -l
-rwxrwxr-x 1 ubuntu ubuntu 13256 sept. 24 01:47 hello

Exercice

On vous demande de réaliser l’exercice suivant :

💬 Que fait le conteneur hello-cpne ?

💬 Quelle est la commande qui permet de voir le contenu de l’image hello-cpne ?

docker inspect hello-cpne

💬 Quelle est le point d’entrée du conteneur hello-cpne ?

"Cmd": [
    "/bin/sh",
    "-c",
    "/bin/sh -c \"figlet -f /root/toilet/fonts/pagga CPNE-TI\""
]

💬 Sur quelle base le conteneur a-t-il été construit ?

Alpine car si on dissect le layer ROOTFS on trouve un fichier contenant
cat etc/apk/world 
alpine-baselayout

Exercice

Vous pouvez commencer par visualisé la vidéo de Xavki ci-dessous.

Puis, répondez aux questions suivantes :

💬 Quel différence y a-t-il entre une image et un conteneur ?

L’image c’est l’archive qui contient tout le nécessaire au démarrage d’un conteneur. L’image c’est la partie passive ou à l’arrêt du conteneur, le conteneur c’est l’image mais en fonctionnement

💬 Qu’est-ce que c’est qu’un registry ?

C’est un serveur d’images. Docker fournit son serveur

💬 Comment se nomme le registry de Microsoft ?

Microsoft Artifact Registry

💬 Est-ce une bonne idée que de placer notre utilisateur dans le groupe docker ?

À mon avis non, on aurait vite tendance à oublié que le conteneur docker tourne en tant que root. Le fait de mettre sudo devant la commande docker nous le rappel à chaque fois

💬 Dans la vidéo, Xavki utilise la commande suivante $ sudo usermod -aG docker $USER et parle de variables d’environnement. C’est quoi une variable d’environnement ?

Variable d’environnement

💬 Dans la vidéo, Xavki utilise la commande suivante $ sudo usermod -aG docker $USER pour ajouter notre utilisateur au groupe docker. Où sont défini les groupes existants, comment puis-je les trouver ?

Sur un poste Linux, les groupes se trouve dans le fichier du même nom. Pour les voir, il suffit de lister le contenu ainsi $ cat /etc/group

💬 Pourquoi Xavki doit quitter et revenir dans sa machine virtuelle après avoir ajouté son utilisateur au groupe docker ?

Parce que l’environnement se crée pour un processus au lancement de celui-ci. Pour que son invite de commandes soit au courant du changement qu’il vient de faire, il doit quitter cette invite de commandes et la relancée. C’est à ce moment que l’environnement précisant les groupes d’appartenances de son invite de commandes sera correctement mis-à-jour.
avant
$ groups 
ubuntu adm dialout cdrom floppy audio dip video plugdev users netdev

après
$ groups 
ubuntu adm dialout cdrom floppy audio dip video plugdev users netdev docker

💬 Quelle est l’adresse exacte permettant de rapatrier l’image docker pour le .NET Runtime version 7.0 du registry de Microsoft ?

registry/image/version ce qui donne mcr.microsoft.com/dotnet/runtime:7.0

💬 Qu’est-ce qui n’est pas cohérant dans cette commande docker run -it -d --name BOSS --hostname the_boss hello-world ?

L’image hello-world contient un binaire qui démarre, affiche hello world et s’arrête. On ne peut pas obtenir une invite de commande sur un processus qui est arrêté (-it) et ce programme n’est pas prévu pour fonctionner comme un démon (-d)

Création d’une image docker build

Il peut y avoir des différences de version entre dotnet 7 et 8 et 9 … Ca ne change pas le fonctionnement

Dans la manipulation précédente, nous avons utilisé l’image hello world fournie par docker. Maintenant, nous allons créer notre propre image hello world à l’aide du langage enseigné dans l’école, le C#.

Pour réaliser cette tâche, nous aurons besoin de l’environnement .NET. L’installation se réalise grâce à un script bash fournit par Microsoft qui télécharge et installe les dépendances nécessaires.

On commence par télécharger le script d’installation Linux en version bash. On lui donne ensuite les droits d’exécution.

ubuntu@m347:~$ wget https://dotnet.microsoft.com/download/dotnet/scripts/v1/dotnet-install.sh
...
  2023-03-23 11:06:08 (379 KB/s) - ‘dotnet-install.sh’ saved [58293/58293]
ubuntu@m347:~$ chmod u+x dotnet-install.sh

Selon l’aide de la commande ./dotnet-install.sh --help, on peut installer la version de notre choix, par exemple la version 8.0.201.

On peut également faire un test à blanc avec la commande ubuntu@m347:~$ ./dotnet-install.sh –dry-run -c 8.0.2xx
ubuntu@m347:~$ ./dotnet-install.sh -c 8.0.2xx
...
dotnet-install: Installed version is 8.0.201
dotnet-install: Adding to current process PATH: `/home/ubuntu/.dotnet`. Note: This change will be visible only when sourcing script.
...
dotnet-install: Installation finished successfully.

On doit ajouter le chemin d’accès à l’exécutable dotnet dans la variable d’environnement PATH.

ubuntu@m347:~$ export PATH="$PATH:$HOME/.dotnet"
ubuntu@m347:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/ubuntu/.dotnet
ubuntu@m347:~$ dotnet --version
8.0.201

Pour éviter toute confusion, nous travaillerons dans un répertoire particulier qu’il faudra créer. Ensuite, nous nous déplaçons dans ce répertoire pour créer un projet C# Console.

ubuntu@m347:~$ mkdir docker-hellocs
ubuntu@m347:~$ cd docker-hellocs/
ubuntu@m347:~/docker-hellocs$ dotnet new console -o App -n HelloCS
Le modèle « Console Application » a bien été créé.

Traitement des actions postérieures à la création en cours... Merci de patienter.
Exécution de « dotnet restore » sur App/HelloCS.csproj...
  Identification des projets à restaurer...
  Restauration effectuée de /home/ubuntu/docker-hellocs/App/HelloCS.csproj (en 66 ms).
Restauration réussie.
ubuntu@m347:~/docker-hellocs$ tree App/
App/
├── HelloCS.csproj
├── obj
│   ├── HelloCS.csproj.nuget.dgspec.json
│   ├── HelloCS.csproj.nuget.g.props
│   ├── HelloCS.csproj.nuget.g.targets
│   ├── project.assets.json
│   └── project.nuget.cache
└── Program.cs

On peut voir le contenu actuel du fichier Program.cs et se rendre compte qu’il contient déjà un Hello, World.

Nous allons simplement le modifier pour être sûr que c’est bien notre programme qui sera démarré plus tard dans notre docker. Nous allons afficher Hello, Docker! au lieu de Hello, World!

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, Docker!");

On peut déjà tester notre programme en le compilant et en l’exécutant

ubuntu@m347:~/docker-hellocs$ cd App/
ubuntu@m347:~/docker-hellocs/App$ dotnet run
Hello, Docker!

Pour avoir une application qui est créée en version finale nous allons utiliser l’outil de publication en mode Release. Comme nous souhaitons obtenir un binaire Linux dans un seul fichier, nous allons également modifier notre fichier projet.

Les RuntimeIdentifier sont décrits sur cette page: RID et le fait d’obtenir un seul fichier en sortie est décrit ici

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishSingleFile>true</PublishSingleFile>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
  </PropertyGroup>

</Project>
ubuntu@m347:~/docker-hellocs/App$ dotnet publish -c Release
MSBuild version 17.8.3+195e7f5a3 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
  HelloCS -> /home/ubuntu/docker-hellocs/App/bin/Release/net8.0/linux-x64/HelloCS.dll
  HelloCS -> /home/ubuntu/docker-hellocs/App/bin/Release/net8.0/linux-x64/publish/
ubuntu@m347:~/docker-hellocs/App$ ls bin/Release/net8.0/linux-x64/publish/
HelloCS  HelloCS.pdb
ubuntu@m347:~/docker-hellocs/App$ bin/Release/net8.0/linux-x64/publish/HelloCS 
Hello, Docker!
hello_docker.cast

Dockerfile

Pour créer sa propre image docker on doit indiquer les étapes de fabrication de celle-ci dans un fichier nommé Dockerfile. Pour construire notre image, nous allons nous baser sur celles fournient par Microsoft qui contiennent le framework déjà installé. Nous placerons les directives suivantes dans notre fichier Dockerfile.

# build image avec SDK
FROM mcr.microsoft.com/dotnet/sdk:latest AS build-env
# définit le répertoire de travail
WORKDIR /app

# Copie tout le répertoire courant dans le conteneur
COPY . ./
# Restore nécessaire pour générer le fichier de dépendances
RUN dotnet restore
# build et publish en version release dans le répertoire out_dir
RUN dotnet publish -c Release -o out_dir

# build runtime image
FROM mcr.microsoft.com/dotnet/runtime:latest
# définit le répertoire de travail
WORKDIR /app

# Met à jour le PATH pour inclure le répertoire de l'exécutable
ENV PATH="/app:${PATH}"
# Copie le contenu du répertoire out_dir du conteneur build-env dans le conteneur runtime
COPY --from=build-env /app/out_dir .
# définit la commande à exécuter lors du démarrage du conteneur
ENTRYPOINT ["HelloCS"]

Une fois le fichier créer, on peut demander à docker de construire l’image (ATTENTION au caractère . à la fin de cette ligne!)

ubuntu@m347:~/docker-hellocs/App$ docker build -t docker-hellocs -f Dockerfile .
[+] Building 84.1s (14/14) FINISHED                                                                                                                                          
 => [internal] load .dockerignore                                                                                                   0.4s
 => => transferring context: 2B                                                                                                     0.0s
 => [internal] load build definition from Dockerfile                                                                                0.3s
 => => transferring dockerfile: 390B                                                                                                0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/runtime:8.0                                                                0.9s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:8.0                                                                   1.0s
 => [build-env 1/5] FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:f712881bafadf0e56250ece1da28ba2baedd03fb3dd49a67f209f9d0cf928e81  58.5s
 => => resolve mcr.microsoft.com/dotnet/sdk:8.0@sha256:f712881bafadf0e56250ece1da28ba2baedd03fb3dd49a67f209f9d0cf928e81             0.3s
...

 => [internal] load build context                                                                                                   0.8s
 => => transferring context: 164.24MB                                                                                               0.6s
 => [stage-1 1/3] FROM mcr.microsoft.com/dotnet/runtime:8.0@sha256:db181c925678b0c193c6bdeaf9d04759051aa0a4ab12f91c267f32acc765f35f 22.3s
 => => resolve mcr.microsoft.com/dotnet/runtime:8.0@sha256:db181c925678b0c193c6bdeaf9d04759051aa0a4ab12f91c267f32acc765f35f          0.3s
...

 => => extracting sha256:c0e5d70de3d5ebb221b0f61c574f9f497fd5e4e190c5bbffc281ef5dc6f10e2c                                          61.8s
 => [stage-1 2/3] WORKDIR /app                                                                                                      0.9s
 => [build-env 2/5] WORKDIR /app                                                                                                    1.9s
 => [build-env 3/5] COPY . ./                                                                                                       2.2s
 => [build-env 4/5] RUN dotnet restore                                                                                             13.1s
 => [build-env 5/5] RUN dotnet publish -c Release -o out_dir                                                                        5.4s
 => [stage-1 3/3] COPY --from=build-env /app/out_dir .                                                                              0.9s
 => exporting to image                                                                                                              0.6s
 => => exporting layers                                                                                                             0.5s
 => => writing image sha256:edd912d838879559c5a675ea460ef1a3d77ff2605989f87c5f4db6a73f5a330c                                        0.0s
 => => naming to docker.io/library/docker-hellocs    

On trouve maintenant une nouvelle image portant notre nom docker-hellocs dans notre poll d’images.

ubuntu@m347:~$ docker images
REPOSITORY                        TAG       IMAGE ID       CREATED         SIZE
docker-hellocs                    latest    8070eff0a81d   2 minutes ago   278MB

Et on peut démarrer une instance de conteneur avec cette image. Au démarrage du conteneur c’est le binaire HelloCS qui sera lancée depuis le répertoire /app

ubuntu@m347:~$ docker run docker-hellocs
Hello, Docker!
hello_docker_suite.cast

On peut également faire le même travail en redéfinissant le point d’entrée avec la commande “bash” au lieu du HelloCS, ça va nous placer dans le conteneur et plus précisément dans une invite de commande bash

ubuntu@m347:~$ docker images
REPOSITORY                        TAG       IMAGE ID       CREATED          SIZE
docker-hellocs                    latest    8070eff0a81d   32 minutes ago   208MB
ubuntu@m347:~$ docker run -it --entrypoint "bash" docker-hellocs

On peut alors constater :

On peut alors manuellement lancer le programme HelloCS

root@f5171bc227ca:/app# ls -l
total 1
-rwxr-xr-x 1 root root 66743194 Mar 21 13:59 HelloCS
-rw-rw-r-- 1 root root    10476 Mar 21 13:59 HelloCS.pdb
root@f5171bc227ca:/app# echo $PATH
/app:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
root@f5171bc227ca:/app# HelloCS 
Hello Docker!

Exercice guidé Lorem Ipsum

Le lorem ipsum (également appelé faux-texte, lipsum, ou bolo bolo1) est, en imprimerie, une suite de mots sans signification utilisée à titre provisoire pour calibrer une mise en page, le texte définitif venant remplacer le faux-texte dès qu’il est prêt ou que la mise en page est achevée.

Généralement, on utilise un texte en faux latin (le texte ne veut rien dire, il a été modifié), le Lorem ipsum ou Lipsum. L’avantage du latin est que l’opérateur sait au premier coup d’œil que la page contenant ces lignes n’est pas valide et que l’attention du lecteur n’est pas dérangée par le contenu, lui permettant de demeurer concentré sur le seul aspect graphique.

Il circule des centaines de versions différentes du lorem ipsum, mais ce texte aurait originellement été tiré de l’ouvrage écrit par Cicéron en 45 av. J.-C., De finibus bonorum et malorum (Liber Primus, 32), texte populaire à cette époque, dont l’une des premières phrases est : « Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit… » (« Il n’existe personne qui aime la souffrance pour elle-même, ni qui la recherche ni qui la veuille pour ce qu’elle est… »).

source

Il est fréquent en programmation de devoir réaliser des tâches que d’autres ont réalisés avant nous. Ainsi, le Lorem Ipsum existant depuis plusieurs années, il est fort probable qu’il existe déjà un code source permettant de le générer.

C’est le cas avec ce paquet nommé NLipsum que l’on peut installer grâce au gestionnaire de paquets NuGet. Il nous reste à savoir comment l’utiliser et comment l’installer.

Installation

Après avoir crée un projet C# tel que cela a été vu lors de la manipulation précédente, il suffit de lancer la commande fournie sur le site pour ajouter cette librairie à notre projet

ubuntu@m347:~/lorem/App$ dotnet add package NLipsum --version 1.1.0

Utilisation

Cette partie est plus compliquée car la documentation est plutôt pauvre sur le site… On trouve cependant un tout petit exemple sur leur wiki.

// Génère un tableau de 1 phrase et prend la première phrase du tableau
NLipsum.Core.LipsumGenerator.GenerateSentences(1)[0]

On peut traduire cette exemple ainsi:

const int NUM_PHRASES = 3; // On veut 3 phrases
string[] lorem = NLipsum.Core.LipsumGenerator.GenerateSentences(NUM_PHRASES); // On génère un tableau de 3 phrases

On peut également utiliser des paragraphes ainsi:

const int NUM_PARAGRAPHS = 3; // On veut 3 paragraphes
string[] lorem = NLipsum.Core.LipsumGenerator.GenerateParagraphs(NUM_PARAGRAPHS); // On génère un tableau de 3 paragraphes

Votre travail

En vous basant sur cette librairie, on vous demande de réalisez un conteneur capable de vous fournir un texte de type Lorem Ipsum de 3 paragraphes. Ci-dessous, vous trouvez le résultat final que vous devez obtenir. Le texte est chaque fois différent.

ubuntu@m347:~$ docker run lorem
Ullamcorper ipsum vero takimata elitr dolore laoreet sed rebum no rebum takimata ut voluptua. Wisi rebum tempor commodo takimata eos tempor lorem eos diam eirmod. Dolore amet dolor gubergren ut sed magna no et lorem clita sit assum. Dolore nulla adipiscing erat sed sit qui dolore duis voluptua clita sea elitr duis magna. Aliquip sea sea sanctus kasd dolore elitr et kasd stet eum te dolor ipsum dolor sea eros. Justo lorem dolore eos commodo eirmod duo no justo feugait accusam sea sit lorem suscipit est ipsum et consequat. Lorem nonumy eos tempor aliquyam illum.
Et nostrud labore dolor dignissim amet ipsum diam gubergren dolor ut. Ipsum ad mazim clita et sit sea consetetur. Gubergren sadipscing ut ipsum rebum ipsum esse lorem. Takimata velit eirmod tempor takimata. Dolore stet velit gubergren vero sanctus labore sed ut nulla facilisi diam. Diam liber consetetur. Invidunt aliquip invidunt enim vero illum est aliquyam ut at et. Liber ipsum rebum vel dolor ipsum dolore sanctus consequat aliquyam ex magna dolor ea enim aliquip eros gubergren. Dolore consetetur kasd sit at assum no commodo vero magna lorem. Option ea est. Aliquyam elitr feugait. Consetetur kasd ipsum luptatum. Iriure stet mazim dolore tincidunt lorem. Suscipit aliquip tempor nonumy. Liber et et kasd quis ea diam clita ut velit velit aliquyam magna lorem. Sit suscipit duo lorem est possim amet tation invidunt in vero. Diam clita gubergren ut takimata invidunt. Accumsan consectetuer diam amet justo.
Amet labore tation amet eirmod et dolore magna vel dolores diam dolor tincidunt. Elit illum ea ut tempor ipsum est molestie elitr labore eleifend. Sanctus duis sit ut ullamcorper doming voluptua justo option clita dolor luptatum justo diam. Justo sanctus labore ipsum zzril rebum magna takimata option duo ex ut labore et elitr. Wisi liber doming sed dolor. Sed clita duo eleifend eos et amet gubergren ut dolor nonumy lorem tempor duo lorem sanctus ea vero sed. Et sit nam vel nobis sed. Magna feugiat justo amet tempor diam diam. Vero diam kasd consequat ex velit sed magna nonumy dignissim elitr no ipsum dolore dolores labore voluptua consetetur. Erat ea ut commodo sadipscing sit et justo accumsan congue aliquyam dolor nisl esse aliquip praesent est lorem tempor. Consequat invidunt vel. Sed at doming sadipscing commodo kasd kasd ipsum ea. Voluptua liber invidunt. Sed soluta lorem rebum stet feugiat zzril voluptua dolore amet. Blandit amet sit ut diam justo augue takimata lorem feugiat vel accusam. Dolore diam magna kasd enim dolor velit sanctus sanctus ut eros ut. Takimata sed rebum elitr amet iriure sea.
ubuntu@m347:~$ docker run lorem
Ipsum diam diam ad ut tempor sea ea gubergren diam ipsum vero enim gubergren nulla tempor. Et nobis et feugait. Iriure in exerci et et eirmod duo. Accusam
...
lorem.cast

Sécurité

Dans cette manipulation, nous allons mettre en pratique un des aspects de la sécurité lié à l’emploi de conteneur docker. Par manque de temps, nous ne traiterons pas de tous les autres aspects listés ci-dessous:

Cela nous vous dispense pas de vous documentez et de les mettre en pratique si vous devez utiliser docker dans un monde professionnel.

Les bases

Dans tous les systèmes d’exploitation, les processus sont lancés avec une identité. Avec Linux, l’identité correspond le plus souvent au login de l’utilisateur mais cette identité peut-être une autre parmi celles présente dans le fichier /etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
ubuntu:x:1000:1000:Module 347:/home/ubuntu:/bin/bash

Dans cette liste, on reconnaît des utilisateurs présents sur la machine tels que:

Il y a d’autres utilisateurs qui sont des utilisateurs systèmes. C’est-à-dire des utilisateurs qui ne peuvent pas se loguer sur le système mais qui sont utilisés par le système pour réaliser certaines tâches. Par exemple, l’utilisateur système www-data correspond à l’utilisateur système utilisé pour réaliser des tâches liée au serveur web.

Explication

Lorsque vous naviguez sur Internet et que vous visitez le site www.example.com, quel nom d’utilisateur le système d’exploitation qui fait fonctionner ce site va-t-il vous attribuer ? Il ne vous connais pas… pourtant vous devez exister en tant que quelqu’un chez lui vu que chaque processus est lié à un utilisateur.

www-data

Et bien vous allez devenir www-data et obtenir les droits correspondants. Ainsi le système d’exploitation pourra vous donnez accès à certains dossiers et certains fichiers. Dans l’exemple ci-dessous, vous aurez le droit de lire le contenu du fichier index.html

-r--r----- 1 www-data www-data 10918 jun 30  2021 index.html

Pour comprendre les droits affichés ci-dessus faites un tour sur la page wikipedia traitant des permissions UNIX.

Lorsque l’utilisateur lance un processus, le système d’exploitation va le plus souvent utiliser l’identité de l’utilisateur pour démarrer ce processus. Dès lors, ce processus aura les droits associés à l’utilisateur. Par exemple, si l’utilisateur ubuntu lance le processus hello, le processus hello aura les droits de l’utilisateur ubuntu.

ubuntu@m347:~$ ./hello
    PID USER      PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+  Command                                         
  22410 ubuntu     20   0  1068     4     0 R  0.0  0.0  0:00.00 ./hello

Dans l’example ci-dessus, on peut voir dans la colonne USER que l’utilisateur ubuntu à lancé le processus hello. Dès lors, le processus hello a les droits de l’utilisateur ubuntu. Par exemple, si le processus hello essaye de créer un nouvel utilisateur dans le fichier /etc/passwd, il ne pourra pas le faire car l’utilisateur ubuntu n’a pas les droits d’écriture sur ce fichier.

ubuntu@m347:~$ ls -l /etc/passwd
-rw-r--r-- 1 root root 1766 Mar 23 10:18 /etc/passwd

Le processus hello est donc limité dans ses actions. Si un pirate informatique parvient à détourner le fonctionnement du processus hello, il ne pourra pas faire grand chose.

La sécurité dans docker

Comme on l’a déjà vu précédemment, on conteneur va isolé un processus. Le processus isolé ne verra pas les interfaces réseaux, les points de montage, les autres processus etc. Malgrès tout, par défaut, docker démarrera le processus en tant que root et c’est potentiellement dangereux.

Démonstration root

Dans un terminal, on démarre un conteneur en mode interactif. En réalité, ce qui est démarré ici c’est un processus bash isolé dans une arborescence Ubuntu. On peut voir que l’identifiant du conteneur est 2b66b6cd7d5d et apparemment, l’utilisateur qui a démarré ce processus est root

ubuntu@m347:~$ sudo docker run -it ubuntu:latest
root@88b4b5bb20e7:/# id
uid=0(root) gid=0(root) groups=0(root)
root@88b4b5bb20e7:/# whoami
root

Dans un autre terminal sur l’hôte, on cherche ce processus et on regarde qui l’a lancé

ubuntu@m347:~$ ps faux
...
root         762  0.0  0.0 720436  5832 ?        Sl   07:19   0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 88b4b5bb20e7
root         784  0.0  0.0   4624  2248 pts/0    Ss+  07:19   0:00  \_ /bin/bash

On voit le processus bash démarré par docker par le biais de containerd, shim et runc et on voit également que l’utilisateur associé au processus est bien root. Donc le root du conteneur est le root de l’hôte. Autrement dit, depuis le conteneur j’ai un contrôle total sur l’hôte !

Si on est développeur, ça peut-être très gênant de démarrer un processus en tant que root alors qu’on est entrain de le développer et qu’il est, par conséquent, un processus qui n’est pas abouti. Cela peut ouvrir la porte à de sérieux problèmes de sécurités.

Avec les mêmes conséquence, on peut également imaginer qu’un pirate informatique cherchera à déclencher des attaques connus sur le système d’exploitation de l’hôte. Par exemple, il peut chercher à déclencher une attaque de type Shellshock. Pour cela, il va utiliser un processus qui va exploiter une faille de sécurité dans le shell bash de l’hôte. Comme le processus est lancé en tant que root, il aura tous les droits sur l’hôte.

Namespace user

Il existe un espace de nom qui permet de palier à ce problème. C’est le namespace user. Son objectif est de créer un mappage entre les utilisateurs de l’hôte et les utilisateurs du conteneur.

Faisons un exemple tout simple. Prenons deux terminaux bash. Dans les deux terminaux, on se place à la racine de l’utilisateur ubuntu.

ubuntu@m347:~$ cd ~
ubuntu@m347:~$ pwd
/home/ubuntu

Dans le premier terminal, on lance le processus bash en passant par la commande unshare. On demande à cette commande de projeter notre utilisateur ubuntu en tant que root dans un nouveau bash conteneurisé.

namespace user

Ainsi, bash démarre un nouveau bash en tant que root. Mais ce root n’est pas le root de l’hôte. Il est le root du processus bash qui est isolé dans un nouvel espace de nom.

Dans ce bash, seul l’espace de nom est isolé. On peut donc accéder au reste de la machine qui n’a pas été isolé. Dans un vrai conteneur, le reste de la machine est également isolé

Créons maintenant un fichier en placant un texte quelconque dedans et vérifions que le fichier a bien été créé avec les bons droits.

## Point de départ, on est dans le premier terminal
## Qui suis-je ?
ubuntu@m347:~$ whoami
ubuntu
## Projette moi en tant que root dans un nouveau espace de nom et démarre un nouveau processus bash
ubuntu@m347:~$ unshare -Ur bash
## Qui suis-je maintenant ?
root@m347:~# whoami
root
## Créons un fichier
root@m347:~# echo "Un texte quelconque" > /tmp/test.txt
## Vérifions les droits du fichier
root@m347:~# ls -l /tmp/test.txt
-rw-rw-r-- 1 root root 20 mar 24 10:40 /tmp/test.txt
# Il appartient bien à root. Vérifions son contenu
root@m347:~# cat /tmp/test.txt 
Un texte quelconque

Dans le second terminal qui est resté dans l’espace de nom d’origine:

## Qui suis-je ?
ubuntu@m347:~$ whoami
ubuntu
## Vérifions que le fichier existe ainsi que son contenu
ubuntu@m347:~$ cat /tmp/test.txt 
Un texte quelconque
## Qui est le propriétaire du fichier ?
ubuntu@m347:~$ ls -l /tmp/test.txt
-rw-rw-r-- 1 ubuntu ubuntu 20 mar 24 11:00 /tmp/test.txt

On voit que le fichier appartient bien à root dans le bash dont l’espace de nom est isolé mais qu’il appartient à ubuntu dans l’espace de nom d’origine. C’est le mappage qui a été fait par unshare qui a permis de faire cela. On a donc créer une isolation de l’utilisateur entre le bash démarré par unshare et le bash de l’hôte. Le root dans l’espace de nom isolé n’est qu’un simple utilisateur dans l’espace de nom standard.

Namespace user dans docker

Dans les conteneurs docker, le mappage n’est pas présent avec une configuration de base comme on a pu le constater précédemment. On peut configurer docker pour faire du mappage d’utilisateur et faire en sorte que dans le conteneur, l’utilisateur root soit un root du conteneur. Ce ne sera pas le même root que celui de l’hôte. Dès lors, le root du conteneur aura tous les droits dans le conteneur mais en dehors du conteneur, ce même root aura les droits de l’utilisateur nobody, c’est-à-dire, aucuns droits.

mappage

Le mappage est définit dans les fichiers /etc/subuid et /etc/subgid. Par exemple, dans le fichier /etc/subuid on trouve les informations suivantes:

ubuntu@m347:~$ cat /etc/subuid
ubuntu:100000:65536

Elles signifient que l’utilisateur ubuntu à le droit de faire de l’impersonation. Il a le droits de créer 65536 mappages. Les identifiants du conteneur seront augmentés de 100000 par rapport aux identifiant de l’hôte. Ainsi, l’utilisateur root du conteneur qui à l’identifiant 0 dans ce dernier, aura l’identifiant 100000 dans l’hôte. Il ne sera donc personne dans le système hôte car il n’y a rien dans le système hôte qui a l’identifiant 100000.

En informatique, l’impersonation consiste à Subroger l’identité d’un utilisateur dans un système informatique, notamment dans le cas où cette opération est conduite de façon légitime par un opérateur accédant au compte d’un client dans le cadre d’une assistance utilisateur.

daemon.json

Pour réaliser ce travail, on va devoir démarrer docker en lui demandant d’utiliser le namespace user. On peut faire ce travail de différente manière. Celle proposée ici consiste à modifier le fichier de configuration du démon docker. Dans ce fichier, on va ajouter la ligne userns.

ubuntu@m347:~$ sudo nano /etc/docker/daemon.json
[sudo] password for ubuntu:
{
    "userns-remap":     "ubuntu:ubuntu"
}

Une fois que c’est fait, il faut redémarrer le démon docker

ubuntu@m347:~$ sudo systemctl restart docker

Si on relance le même conteneur que précédemment, du point de vue du conteneur, on est toujours root avec l’identifiant 0

ubuntu@m347:~$ docker run -it ubuntu:latest
root@82a2343d3597:/# id
uid=0(root) gid=0(root) groups=0(root)
root@82a2343d3597:/# whoami
root

Mais cette fois, du point de vue de l’hôte, le processus bash est démarré en tant qu’utilisateur 100000.

ubuntu@m347:~$ ps faux | grep -A2 82a2343d3597
root        2378  0.0  0.0 712212  6572 ?        Sl   09:03   0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 82a2343d3597
100000      2399  0.0  0.0   4492  2276 pts/0    Ss+  09:03   0:00  \_ /bin/bash

Création d’un utilisateur dans un conteneur

Si on va un peu plus loin, on peut créer un utilisateur dans le conteneur, par exemple, l’utilisateur ubuntu.

oot@82a2343d3597:/# useradd ubuntu
root@82a2343d3597:/# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
ubuntu:x:1000:1000::/home/ubuntu:/bin/sh

Il obtiendra l’identifiant 1000 dans le conteneur. Par défaut, lorsqu’on switch sur l’utilisateur ubuntu avec la commande su, c’est un terminal sh qui sera lancé. On peut voir cette particularité dans la dernière colonne de la ligne concernant l’utilisateur ubuntu (ci-dessus). C’est pour cette raison que le prompt est maintenant $

root@82a2343d3597:/# su ubuntu
$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
$ whoami
ubuntu

Du point de vue de l’hôte, le processus sh à du être lancé par l’utilisateur

idfinal=iduserconteneur+100000=1000+100000=101000 id\ final = id\ user\ conteneur + 100'000 = 1000 + 100'000 = 101'000

La preuve

ubuntu@m347:~$ ps faux | grep -A4 82a2343d3597
root       25180  0.0  0.4 711708  8264 ?        Sl   11:02   0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 82a2343d3597
100000     25200  0.0  0.1   4108  3332 pts/0    Ss   11:02   0:00  \_ /bin/bash
100000     25262  0.0  0.1   4492  2768 pts/0    S    11:13   0:00      \_ su ubuntu
101000     25264  0.0  0.0   2608   532 pts/0    S+   11:13   0:00          \_ sh

Conclusion sur la sécurité

la grande force de docker est aussi sa plus grande faiblesse. docker démarre ses conteneurs en mode root par défaut. C’est une manière de procéder qui a été choisie pour des raisons de simplicité. En effet, en tant que root, on peut faire tout ce que l’on veut dans le conteneur. Malheureusement, cette simplicité à un coût. Si un attaquant parvient à s’évader du conteneur, il se trouvera sur la machine hôte avec les mêmes droits que root.

Le fait d’être le même root que l’hôte pose également un problème lié aux failles de sécurité du noyau. Si le noyau est vulnérable, l’attaquant peut exploiter cette vulnérabilité pour prendre le contrôle de la machine hôte. C’est ce qu’on appelle une vulnérabilité de l’escalade de privilèges. C’est le cas par exemple de la vulnérabilité DirtyCow qui a été découverte en 2016. Cette vulnérabilité permet à un utilisateur lambda de prendre le contrôle de la machine hôte en exploitant une faille de sécurité du noyau. Cette vulnérabilité a été exploitée par des attaquants pour prendre le contrôle de serveurs Azure.

Il reste encore le fait de pouvoir générer nous même nos images et les mettre à disposition de tous sur le site Docker Hub. C’est une pratique qui peut être dangereuse. En effet, si l’image que vous avez créée contient une faille de sécurité, tous les utilisateurs de cette image seront vulnérables. De même, si vous ne maintenez pas votre image à jour en la regénérant régulièrement, les utilisateurs de cette image seront vulnérables aux failles de sécurité découvertes dans les versions précédentes de l’image.

Si on combine ses 3 éléments, on obtient un cocktail explosif.

Il reste encore les problèmes inhérents à docker lui-même.

nist

ChaosDB: How We Hacked Databases of Thousands of Azure Customers

D’autres systèmes de conteneurs tel que incus démarre automatiquement leur conteneurs en mode unprivileged, c’est-à-dire, en utilisant automatiquement le namespace user.

ubuntu@m347:~$ incus launch ubuntu:24.04 c1
Creating c1
Starting c1                                 
ubuntu@m347:~$ incus exec c1 bash
root@c1:~# id -u
0
root@c1:~# whoami
root

Vu de l’hôte

root       31475  0.0  0.2  46924  5420 ?        S    11:32   0:00      \_ /snap/incus/current/bin/incus forkexec c1
1000000    31479  0.0  0.1   8960  2668 pts/1    Ss+  11:32   0:00          \_ bash

Quand on sait que le développeur des conteneurs Incus Stéphane Graber est également un des auteurs du code source du namespace user du noyau Linux, on comprend que ce système utilise ça par défaut 😉.

Exercice invente-un-mu

Steeve Droz a crée un programme en Java permettant de créer une machine à inventer des mots. Le fonctionnement du programme est décrit sur son dépôt git.

Votre travail consiste à créer deux recettes Dockerfile permettant la création d’une image docker capable de faire fonctionner ce programme.

Dans un premier temps, vous devez déjà être en mesure de faire fonctionner le programme sur votre machine. Soyez précis et notez toutes les étapes nécessaires pour faire fonctionner le programme dans votre machine hôte.

Choisissez ensuite une image de base qui convient à la réalisation du travail. Vous pouvez utiliser une image ubuntu, alpine ou une autre image qui vous semble pertinante.

Particularités

Le programme invente-un-mu doit être démarré automatiquement lors du démarrage du conteneur. Le programme générera toujours le même nombre de mots, par exemple 20 mots.

ubuntu@m347:~$ docker run invente-un-mu
abille  
abrimpluna
assinas

X20
...
Attention à l’encodage des caractères. Les caractères accentués générés doivent s’afficher correctement

Dans une seconde étape, vous devrez donner la possibilité à l’utilisateur du conteneur de paramétrer le nombre de mots à générer. Par exemple, si l’utilisateur lance la commande docker run invente-un-mu --count 10, le programme générera 10 mots. L’utilisation des arguments peut être réalisée autrement que celle proposée ici.

ubuntu@m347:~$ docker run invente-un-mu --count 10
abille
abrimpluna
assinas

X10
...

Exercice figlet

Le programme figlet est un programme qui permet de créer des bannières de texte. Il est très utilisé dans le monde de l’administration système pour créer des bannières de texte pour les scripts shell.

On vous demande de réaliser une image docker permettant la réalisation de la banière suivante

Vous avez déjà vu cette image dans ce cours mais cette fois, c’est à vous de réaliser le travail.

Persistance des données

Les valeurs qui suivent dépendes de l’activation du namespace user. Dans ce qui suit, le namespace est activé

Lorsqu’on démarre un conteneur, docker crée automatiquement des points de montage.

On peut les visualiser à l’aide de la commande mount

root@aa898b7f3cab:/# mount
overlay on / type overlay (rw,relatime,lowerdir=/var/snap/docker/common/var-lib-docker/100000.100000/overlay2/l/ASXXAW2547JEJWSOAZOI7UFZCA:/var/snap/docker/common/var-lib-docker/100000.100000/overlay2/l/HPAS7BR2367OIJ6UUXP6HENA4Q,upperdir=/var/snap/docker/common/var-lib-docker/100000.100000/overlay2/fb886671867b90cfe2bfb40744d7e0d2b747a908f444f0b509d9058ea5635eb5/diff,workdir=/var/snap/docker/common/var-lib-docker/100000.100000/overlay2/fb886671867b90cfe2bfb40744d7e0d2b747a908f444f0b509d9058ea5635eb5/work,xino=off)
...

On y trouve notamment la racine / qui pointe sur un système de fichier de type OverlayFS. Le principe du système de fichiers overlay est décrit sur la page docker overlayfs.

overlay_constructs

Il existe d’autre système de fichier (overlay2, aufs, btrfs, zfs, devicemapper, …) mais l’overlay est le plus utilisé par docker (et podman). Pour connaître le système de fichier utilisé par docker, on peut utiliser la commande docker info

ubuntu@m347:~$ docker info | grep -i "Storage Driver"
 Storage Driver: zfs

On peut facilement retrouver ces informations pour notre conteneur grâce à la commande docker inspect nom_de_l_image

jq est un utilitaire de mise en forme des flux JSON. Il permet de filtrer les données JSON et de les formater. Il est disponible dans les dépôts officiels de la plupart des distributions Linux.
ubuntu@m347:~$ docker image inspect ubuntu --format '{{json .GraphDriver.Data}}' | jq .
{
  "MergedDir": "/var/snap/docker/common/var-lib-docker/100000.100000/overlay2/9d15649fd988...ebb/merged",
  "UpperDir": "/var/snap/docker/common/var-lib-docker/100000.100000/overlay2/9d15649fd988...ebb/diff",
  "WorkDir": "/var/snap/docker/common/var-lib-docker/100000.100000/overlay2/9d15649fd988...ebb/work"
}

Ce qui nous intéresse vraiment dans ce principe et qui sera la cause de certains problèmes, c’est qu’on conteneur docker est éphémère. Ce qui signifie que si je crée un fichier dans un conteneur, au moment ou je détruis ce conteneur, tout ce qui aura été fait dans ce conteneur sera perdu.

Démonstration

Dans la démonstration ci-dessous on peut voir le démarrage d’un conteneur ubuntu. Une fois dans le conteneur, on crée un fichier nouveau_fichier dans lequel on place le texte "Hello". On quitte le conteneur et on le détruit. Lorsqu’on relance un conteneur ubuntu, le fichier à disparut. On peut aussi facilement comprendre ça en voyant que le nom d’hôte du conteneur est chaque fois différent. C’est comme si on était chaque fois sur une autre machine…

ubuntu@m347:~$ docker run -it ubuntu
root@e1f008654921:/# ls
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@e1f008654921:/# echo "Hello" > nouveau_fichier
root@e1f008654921:/# ls
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  nouveau_fichier  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@e1f008654921:/# exit
exit
ubuntu@m347:~$ docker rm -f e1f008654921
ubuntu@m347:~$ docker run -it ubuntu
root@7d9773708a2a:/# ls
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

On peut choisir de rester dans le même conteneur sans le détruire. Dans ce cas, on peut voir que le fichier est toujours présent.

overlay.cast

Si on redémarre le même conteneur alors les données seront toujours présentes.

ubuntu@m347:~$ docker run -it -d --name c1 ubuntu

cdb41f74b886b7ef46fda1758fd1ae6b71493d70c2ea1b7c1b11ba41a0724837
ubuntu@m347:~$ docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED         STATUS         PORTS     NAMES
cdb41f74b886   ubuntu    "/bin/bash"   5 seconds ago   Up 3 seconds             c1
ubuntu@m347:~$ docker exec -it c1 /bin/bash
root@cdb41f74b886:/# echo test > /home/ubuntu/fichier.txt
root@cdb41f74b886:/# ls -l /home/ubuntu
total 1
-rw-r--r-- 1 root root 5 May 27 06:22 fichier.txt
root@cdb41f74b886:/# exit
exit
ubuntu@m347:~$ docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS          PORTS     NAMES
cdb41f74b886   ubuntu    "/bin/bash"   59 seconds ago   Up 57 seconds             c1
ubuntu@m347:~$ docker stop c1

c1
ubuntu@m347:~$ docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS                       PORTS     NAMES
cdb41f74b886   ubuntu    "/bin/bash"   About a minute ago   Exited (137) 4 seconds ago             c1
ubuntu@m347:~$ docker start c1
c1
ubuntu@m347:~$ docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS         PORTS     NAMES
cdb41f74b886   ubuntu    "/bin/bash"   About a minute ago   Up 3 seconds             c1
ubuntu@m347:~$ docker exec -it c1 /bin/bash
root@cdb41f74b886:/# ls -l /home/ubuntu
total 1
-rw-r--r-- 1 root root 5 May 27 06:22 fichier.txt

Persistance des données

Docker propose différente solution pour persister des données. L’idée étant de conserver les données même après la destruction d’un conteneur. Pour comprendre où les données seront déposées, il faut se placer du point de vue de l’hôte. Vu de l’hôte, les données peuvent être sauvegardées dans:

types-of-mounts

docker volume

Les volumes sont crées et gérés par docker. Il existe une commande spécifique pour ça, elle se nomme sans surprise docker volume

ubuntu@m347:~$ docker volume

Usage:  docker volume COMMAND

Manage volumes

Commands:
  create      Create a volume
  inspect     Display detailed information on one or more volumes
  ls          List volumes
  prune       Remove all unused local volumes
  rm          Remove one or more volumes

Run 'docker volume COMMAND --help' for more information on a command.

Lorsqu’on demande à docker de nous créer un volume, il va crée un emplacement sur le disque accessible aux conteneurs qui en feront la demande.

ubuntu@m347:~$ docker volume create sharedvol
sharedvol
ubuntu@m347:~$ docker volume ls
DRIVER    VOLUME NAME
local     sharedvol
ubuntu@m347:~$ docker volume inspect sharedvol
[
    {
        "CreatedAt": "2023-03-22T09:56:12Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/100000.100000/volumes/sharedvol/_data",
        "Name": "sharedvol",
        "Options": {},
        "Scope": "local"
    }
]

Mountpoint nous indique où se situe réellement les informations que l’on persistera. On peut se rendre compte que le chemin d’accès comporte un double numéro 100000.100000. Cette valeur est en lien avec le namespace user expliqué plus haut. On peut également se rendre compte que les droits sur ce répertoire sont attribués ainsi:

ubuntu@m347:~$ sudo ls -l /var/lib/docker/100000.100000/volumes/sharedvol/
total 4
drwxr-xr-x 2 100000 100000 4096 mars  22 13:48 _data

L’utilisateur ayant pour uid 100000 aura les droits d’écritures dans ce répertoire. Autrement dit, chaque conteneur démarrer ayant un mappage utilisateur

Conteneur Hôte
root -
uid 0 uid 100000

permettra au root du conteneur d’écrire des informations dans ce répertoire sur l’hôte.

Il ne reste plus qu’à indiquer au conteneur l’endroit dans lequel il va devoir monter ce volume. Ça se fait ainsi:

ubuntu@m347:~$ docker run -it --volume sharedvol:/vol:rw ubuntu
root@af589abbd7ec:/# 

Le paramètre --volume est composé de trois valeurs séparées par :

Nom du volume Point de montage Droits
sharedvol /vol rw (read / write)

On peut voir maintenant dans le conteneur un nouveau dossier qui se nomme /vol. Si on place des informations dans ce dossier, elles iront se placer dans le répertoire de /var/lib/docker/100000.100000/volumes/sharedvol/_data l’hôte.

Démonstration

Depuis le conteneur

root@70a9671bebb3:/# ls
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  vol
root@70a9671bebb3:/# echo "Test" > /vol/nouveau_fichier
root@70a9671bebb3:/# ls -l /vol
total 1
-rw-r--r-- 1 root root 5 Mar 22 10:00 nouveau_fichier

Depuis l’hôte

ubuntu@m347:~$ sudo ls -l /var/lib/docker/100000.100000/volumes/sharedvol/_data
total 1
-rw-r--r-- 1 100000 100000 5 Mar 22 10:00 nouveau_fichier
ubuntu@m347:~$ sudo cat /var/lib/docker/100000.100000/volumes/sharedvol/_data/nouveau_fichier
Test

Si on arrête le conteneur et qu’on le détruit, puis qu’on démarre un nouveau conteneur faisant référence au même point de montage, on pourra retrouver les mêmes informations.

Partage entre conteneurs

On pourra même partager des informations entre les conteneurs. En effet, chaque conteneur qui demandera un point de montage sur le dossier de l’hôte y aura accès.

Démonstration

On démarre un conteneur debian en demandant l’accès au volume. On retrouve le répertoire /vol dans lequel se trouve le fichier nouveau_fichier. On peut ajouter du texte dans le fichier.

ubuntu@m347:~$ docker run -it --volume sharedvol:/vol:rw debian
root@68529ce0b064:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  vol
root@68529ce0b064:/# ls /vol
nouveau_fichier
root@68529ce0b064:/# echo "Ajout d'une ligne" >> /vol/nouveau_fichier 

Dans un autre conteneur ubuntu qui tourne en parallèle, on trouve le même dossier avec le même fichier dedans et on peut voir le nouveau texte qui a été ajouté depuis l’autre conteneur

ubuntu@m347:~$ docker run -it --volume sharedvol:/vol:rw ubuntu
root@70a9671bebb3:/# ls
bin  boot  dev  etc  home  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  vol
root@70a9671bebb3:/# echo "Test" > /vol/nouveau_fichier
root@70a9671bebb3:/# ls /vol
nouveau_fichier

Suppression des volumes

Pour supprimer un volume, il faut qu’il ne soit pas utilisé. Lors de mon essai de suppression, j’ai obtenu un message d’erreur car des conteneurs étaient liés à ce volume (même s’ils sont à l’arrêt).

ubuntu@m347:~$ docker volume rm sharedvol
Error response from daemon: remove sharedvol: volume is in use - [70a9671bebb33375c0bf4af23b1debda49d75d0688044c984e88d618fe8ae652, 68529ce0b0648dc6f4af83384e5c4aaaeb81513009bbe720295c79195059fb42, 7a013025fb621e71612758db1992910a62cdde493ee727d1d0c0c1bacb2d01b3, d1957186d2fa99f1a7e2537220bee1dd58d050adb15836c7381af411b63153ba, b0b61d4709fcb3c7c2990b94e12c9a81f35bd703dad9c8210d88fcc1f0c1cbc0]
ubuntu@m347:~$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED          STATUS                      PORTS     NAMES
b0b61d4709fc   ubuntu    "bash"    41 seconds ago   Exited (0) 39 seconds ago             condescending_kilby
d1957186d2fa   ubuntu    "bash"    45 seconds ago   Exited (0) 43 seconds ago             sweet_chandrasekhar
7a013025fb62   ubuntu    "bash"    48 seconds ago   Exited (0) 46 seconds ago             lucid_hermann
68529ce0b064   debian    "bash"    28 minutes ago   Exited (0) 13 minutes ago             vibrant_hypatia
70a9671bebb3   ubuntu    "bash"    52 minutes ago   Exited (0) 13 minutes ago             hopeful_nash

Pour supprimer ce volume, il faut supprimer les conteneurs qui lui sont associés. On peut faire cela ligne par ligne ou utiliser un assemblage de commande. En effet, s’il y a plusieurs dizaines de conteneurs ça commence à devenir pénible de faire cela ligne par ligne. À vous d’essayer de comprendre cet assemblage 🥴

ubuntu@m347:~$ docker rm $(docker ps -aq)
b0b61d4709fc
d1957186d2fa
7a013025fb62
68529ce0b064
70a9671bebb3
ubuntu@m347:~$ docker volume rm sharedvol
sharedvol
ubuntu@m347:~$ docker volume ls
DRIVER    VOLUME NAME

La documentation officielle nous propose un autre assemblage de commandes

ubuntu@m347:~$ docker rm $(docker ps --filter status=exited -q)

Exercice volume

On vous demande de mettre en place un conteneur capable de compiler un programme écrit en C#. L’utilisateur prépare son code source Program.cs sur son hôte et le place dans un répertoire nommé works qui est partagé avec le conteneur. Il démarre ensuite le conteneur worker qui se charge de compiler le programme et qui place le résultat de son travail dans le répertoire, également partagé, deliverable.

Le programme compiler sera autonome, c’est-à-dire qu’il pourra fonctionner sans dépendance particulière.

Option

L’utilisateur peut placer un fichier de configuration pour déterminer comment le programme doit être compilé. Par exemple, il peut placer un fichier config.json dans le répertoire works qui contiendra les informations suivantes

{
  "target": "exe",
  "architecture": "x64",
  "name": "mon_programme"
}

Le réseau docker network

La partie réseau de docker est très riche. On peut créer des réseaux, les lister, les inspecter, les supprimer, les connecter à des conteneurs, les connecter à des réseaux externes, … Comme nous sommes dans le domaine de l’informatique de développement, on se contentera de comprendre les bases et notamment l’installation de base qui nous crée un bridge ainsi que le publication de port.

Comme la vidéo l’explique, lorsqu’on crée une image docker, on peut préciser une metadonnée qui indiquera à docker qu’il doit publier un port. C’est le travail des développeurs de l’image nginx. Ils ont précisé dans le fichier Dockerfile que le port 80 devait être publié. On peut voir cela dans le fichier Dockerfile de l’image nginx sur le site Docker Hub

En cliquant sur le lien en dessous du titre Supported tags and respective Dockerfile links, on peut voir le fichier Dockerfile de l’image nginx. On y trouve la ligne suivante:

EXPOSE 80

Exercice nginx

On aimerait conteneuriser un site web statique. Le site se trouve sur github. Bien entendu, on ne placera dans le conteneur que les parties utiles au site web et pas le fichier .gitignore par exemple.

Le site sera personnalisé avec votre nom et prénom (y.c. dans le titre). On utilisera l’image nginx:alpine pour servir le site. Le conteneur exposera le port 80. Au démarrage, on publiera le port 80 du conteneur sur le port 8080 de l’hôte.

Il faudra encore rendre accessible le site en utilisant le nom de domaine prénom-nom.local depuis l’hôte.

La résolution DNS sera uniquement locale et elle n’a rien à voir avec le conteneur

nginx.cast

docker compose

docker compose est un outil qui permet de définir et de lancer des applications multi-conteneurs. Il permet de définir les services qui composent une application dans un fichier yaml et de les lancer en une seule commande.

Prenons comme exemple un site web que vous connaissez déjà:

Le site est composé de trois parties:

Selon le principe de conteneurisation, chaque partie de l’application sera isolée dans un conteneur différent mais chaque conteneur devra être capable de communiquer avec les autres. En effet, le serveur web devra pouvoir passer les requêtes php au conteneur php et le conteneur php devra pouvoir se connecter à la base de données.

Réalisation

Les images de base

On peut partir de zéro et tout construire à la main. Mais on peut également partir d’image de base déjà préparée et officielles. C’est le cas de l’image php par exemple. On peut trouver une image php sur le site Docker Hub. Cette image peut-être utilisée seule, c’est-à-dire uniquement avec php ou alors en combinaison avec apache.

Nous utiliserons l’image php:<version>-apache pour notre site web. Comme il est indiqué dans la documentation de l’image

This image contains Debian’s Apache httpd in conjunction with PHP (as mod_php) and uses mpm_prefork by default.

La documentation nous indique également comment construire un fichier Dockerfile et comment placer notre site web à l’intérieur de l’image.

Si on veut aller encore plus loin, on peut trouver le fichier Dockerfile utilisé pour créer cette image. Il suffit de se rendre sur le lien docs repo’s php/ directory. On y trouve un fichier Dockerfile pour chaque version de php et pour chaque type d’installation (cli, apache, fpm, …).

Avec le même raisonnement, on trouve une image mysql sur le site Docker Hub.

La configuration apache

Le site recettes demande une configuration particulière. Ceci est du au fait qu’il utilise le framework codeigniter. Parmis les spécificités, on peut citer:

.env

CI_ENVIRONMENT = production
app.baseURL = 'http://recettes.local'

database.default.hostname = mysql-container
database.default.database = recettes
database.default.username = u_recette
database.default.password = u_recette
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306

Dockerfile

# Use an official PHP Apache runtime
FROM php:8.2-apache

# Set the working directory to /var/www/html
ENV APACHE_DOCUMENT_ROOT=/var/www/html

# Set the hostname
ENV APACHE_HOST_NAME=recettes.local

# Prepare the new apache 000-default.conf file 
ENV VIRTUAL_HOST="<VirtualHost *:80>\n \
    ServerName ${APACHE_HOST_NAME}\n \
    DocumentRoot ${APACHE_DOCUMENT_ROOT}/public\n \
    <Directory ${APACHE_DOCUMENT_ROOT}/public>\n \
        AllowOverride All\n \
        Require all granted\n \
    </Directory>\n \
    ErrorLog \${APACHE_LOG_DIR}/error.log\n \
    CustomLog \${APACHE_LOG_DIR}/access.log combined\n \
</VirtualHost>"

# Set the new apache 000-default.conf file
RUN echo "${VIRTUAL_HOST}" > /etc/apache2/sites-available/000-default.conf

# Update package lists
RUN apt-get update

# Install any needed packages
RUN apt-get install -y curl git zip unzip libicu-dev iputils-ping iproute2 nmap

# Install some optional packages for debugging
RUN apt-get install -y iputils-ping iproute2 nmap

# Install Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Allow root to run composer
ENV COMPOSER_ALLOW_SUPERUSER=1

# Make composer runnable and update-it
RUN chmod +x /usr/local/bin/composer
RUN /usr/local/bin/composer self-update

# Enable Apache modules
RUN a2enmod rewrite

# Install any extensions you need
RUN docker-php-ext-install mysqli intl

# Set the working directory to /var/www/html
WORKDIR ${APACHE_DOCUMENT_ROOT}

# Copy the source code in the local /recettes directory into the container at /var/www/html
COPY recettes ${APACHE_DOCUMENT_ROOT}

# Install the composer dependencies
RUN /usr/local/bin/composer install

# Set the rights on the writable directory
RUN chmod -R 775 "${APACHE_DOCUMENT_ROOT}/writable"
RUN chown -R www-data:www-data "${APACHE_DOCUMENT_ROOT}/writable"

On peut, dès lors, construire l’image avec la commande docker build et démarrer le conteneur

ubuntu@m347:~$ docker build -t apache-container -f Dockerfile .
ubuntu@m347:~$ docker run -d --name apache-container apache-container

La configuration mysql

Pour le conteneur mysql tout se fera directement dans le fichier docker-compose.yml. On peut y préciser le nom de la base de données, le nom d’utilisateur et le mot de passe. Il faudra bien sûr veiller à ce que ces informations coincident avec celles qui se trouvent dans le fichier .env du site recettes.

docker-compose.yml

Il reste à écrire la recette qui permettra de démarrer les deux conteneurs en même temps. C’est le rôle du fichier docker-compose.yml. Ce fichier est écrit en yaml. On y trouve la référence au fichier Dockerfile pour le conteneur apache et la configuration du conteneur mysql.

version: "3.9"
services:
    web:
      container_name: apache-container
      build: 
        context: .
        dockerfile: Dockerfile
      ports:
        - 80:80
    mysql:
      container_name: mysql-container
      image: mysql:8.3
      environment:
        MYSQL_ROOT_PASSWORD: my-sup3r-p4ssw0rd
        MYSQL_DATABASE: recettes
        MYSQL_USER: u_recette
        MYSQL_PASSWORD: u_recette
      ports:
        - 3306:3306
compose.cast

Conclusion sur docker compose

On peut imaginer que le développeur de l’application recettes ait fourni les fichiers Dockefile et docker-compose.yml avec le code source de l’application. Il ne resterait plus qu’à lancer la commande docker-compose up pour démarrer l’application. Cependant, les connaissances requises pour écrire le fichier Dockerfile et le fichier docker-compose.yml sont plus proches de l’administration système que du développement. Pas sûr qu’un dévloppeur soit en mesure de réaliser les deux tâches.

On remarque également que le conteneur apache est très similiaire à un conteneur système. Là où docker se présente comme un conteneur d’application, dans certaines situations, il est plus proche d’un conteneur système. Du coup, plutôt que de gérer tout ces fichiers avec docker, on pourrait utiliser des conteneurs système tel qu’incus, multipass ou d’autres. Pour réaliser le même travail, on peut choisir d’utiliser cloud-init. Dès lors la configuration complète du conteneur système se trouve dans un seul fichier yaml.

Pour peut qu’on connaisse l’utilisation d’une distribution tel qu’Ubuntu, on peut aisaiment comprendre le contenu du fichier.

cloud-init.yaml
## template: jinja
#cloud-config
package_update: true
package_upgrade: true

timezone: Europe/Zurich
locale: fr_CH.UTF-8

packages:
    - git
    - apache2
    - php
    - mysql-server
    - php-mysql
    - php-intl
    - php-mbstring
    - php-xml
    - php-json

write_files:
    - path: "/etc/apache2/sites-available/{{ds.meta_data.local_hostname}}.conf"
      owner: root:root
      permissions: '0644'
      content: |
        <VirtualHost _default_:443>
            ServerAdmin {{ds.meta_data.local_hostname}}@localhost
            ServerName {{ds.meta_data.local_hostname}}.local
            DocumentRoot /var/www/html/public
            ErrorLog ${APACHE_LOG_DIR}/{{ds.meta_data.local_hostname}}.error.log
            CustomLog ${APACHE_LOG_DIR}/{{ds.meta_data.local_hostname}}.access.log combined
            SSLEngine on
            SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
            SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
            <Directory /var/www/html>
                AllowOverride FileInfo
                Require all granted
            </Directory>
        </VirtualHost>

runcmd:
    - a2enmod rewrite
    - a2enmod ssl
    - a2dissite 000-default
    - a2dissite default-ssl
    - sed -i 's/\(Listen.*80\)/#\1/g' /etc/apache2/ports.conf
    - sed -i 's/\(NameVirtualHost.*\*:\)80/\1443/g' /etc/apache2/ports.conf
    - git clone https://git.s2.rpn.ch/SteeveDroz/recettes
    - rm -rf recettes/LICENSE recettes/spark recettes/phpunit.xml.dist recettes/README.md recettes/tests/ recettes/.gitignore recettes/.git recettes/env
    - echo -e "CI_ENVIRONMENT = production\napp.baseURL = 'http://recettes.local'\ndatabase.default.hostname = localhost\ndatabase.default.database = {{ds.meta_data.local_hostname}}\ndatabase.default.username = {{ds.meta_data.local_hostname}}\ndatabase.default.password = {{ds.meta_data.local_hostname}}" > recettes/.env
    - mv -f recettes/{.,}* /var/www/html
    - a2ensite "{{ds.meta_data.local_hostname}}"
    - systemctl restart apache2
    - mysql -u root -e "CREATE DATABASE IF NOT EXISTS {{ds.meta_data.local_hostname}};"
    - mysql -u root -e "CREATE USER IF NOT EXISTS '{{ds.meta_data.local_hostname}}'@'localhost' IDENTIFIED BY '{{ds.meta_data.local_hostname}}';"
    - mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO '{{ds.meta_data.local_hostname}}'@'localhost';"
    - mysql -u root -e "FLUSH PRIVILEGES;"

users:
    - name: "{{ds.meta_data.local_hostname}}"
      gecos: Local User
      sudo: ALL=(ALL) NOPASSWD:ALL
      groups: users, admin
      shell: /bin/bash
      lock_passwd: true

On crée le conteneur avec la commande:

ubuntu@m347:~$ lxc launch ubuntu:22.04/cloud recettes --config user.user-data="$(cat cloud-init.yaml)"

Là encore, on est proche de la configration système et un peu trop loins du développement.

Publication d’image

Vous avez créer une image qui vous semble utile et que vous aimeriez partager avec d’autres personnes. Vous pouvez le faire en la publiant sur Docker Hub. Pour cela, il faut créer un compte sur le site en suivant les instructions. Une fois votre compte créé, cliquez sur le lien Create Repository. Vous arrivez alors sur une page où vous devez saisir le nom de votre dépôt, ainsi qu’une courte description. Vous aurez également la possibilité de régler la visibilité de votre dépôt.

Sur la droite de la page, vous trouverez directement les commandes à réaliser pour publier votre image.

$ docker tag local-image:tagname new-repo:tagname
$ docker push new-repo:tagname

Avant de pouvoir faire ces commandes, vous devrez vous connecter à votre compte depuis la ligne de commande avec la commande docker login. Vous devrez saisir votre nom d’utilisateur et votre mot de passe.

Reprenez l’image HelloCS que vous avez créée dans le chapitre docker build et publiez-la sur Docker Hub.

Références

LXC

Le mélange de docker et d’incus sur une même machine peut induire des problèmes, notament à cause des règles de firewall. Si docker démarre après incus, il réécrit des règles qui empêche les conteneurs incus d’accéder à internet. Pour éviter cela, on peut démarrer incus après docker ou alors définir des règles de firewall qui permettent à incus d’accéder à internet.

Billet de Stéphane Graber

$ sudo iptables -I DOCKER-USER -i incusbr0 -j ACCEPT
$ sudo iptables -I DOCKER-USER -o incusbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

Docker

Docker