tuto5: add part on filesystems
This commit is contained in:
parent
75c35a36d1
commit
d02f573142
|
@ -0,0 +1,36 @@
|
|||
::::: {.exercice}
|
||||
|
||||
## Exercice {-}
|
||||
|
||||
Continuons notre script `registry_play` de la partie précédente afin d'être en
|
||||
mesure d'extraire également les images en plusieurs couches.
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
42sh$ cd $(mktemp -d)
|
||||
|
||||
42sh$ ./registry_play library/python:latest
|
||||
|
||||
42sh$ tree -L 1
|
||||
.
|
||||
./lower0
|
||||
./lower1
|
||||
./lower2
|
||||
./lower3
|
||||
./lower4
|
||||
./lower5
|
||||
./lower6
|
||||
./lower7
|
||||
./lower8
|
||||
./upper
|
||||
./work
|
||||
./rootfs
|
||||
|
||||
42sh# chroot rootfs /usr/bin/python3
|
||||
Python 3.23.0 (main) on linux
|
||||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> _
|
||||
```
|
||||
</div>
|
||||
|
||||
:::::
|
|
@ -0,0 +1,10 @@
|
|||
## Pour aller plus loin
|
||||
|
||||
* [Unioning file systems: Architecture, features, and design choices](https://lwn.net/Articles/324291/) :\
|
||||
<https://lwn.net/Articles/324291/>
|
||||
|
||||
Des expériences sont en cours pour essayer de faire un format
|
||||
d'archive pour les couches qui permettrait d'utiliser les conteneurs
|
||||
sans avoir eu besoin préalablement d'avoir télécharger le contenu des
|
||||
couches :
|
||||
<https://github.com/containerd/stargz-snapshotter/blob/main/docs/estargz.md>
|
|
@ -0,0 +1,403 @@
|
|||
\newpage
|
||||
|
||||
Systèmes de fichiers et couches
|
||||
===============================
|
||||
|
||||
Les images de conteneurs sont distribuées en couches, chaque couche contenant
|
||||
les différences apportée par rapport à la couche précédente : l'ajout d'un
|
||||
fichier, la suppression d'un dossier, ...
|
||||
|
||||
L'intérêt principal est bien entendu d'optimiser l'espace de stockage, en
|
||||
favorisant la réutilisation des couches d'un conteneur à l'autre. Cela permet
|
||||
en outre d'accélérer le processus de création des conteneurs, puisqu'il n'y a
|
||||
pas besoin de commencer par recopier les fichiers de l'image avant de pouvoir
|
||||
lancer le conteneur.
|
||||
|
||||
Pour réaliser ces actions, Docker dispose de plusieurs techniques, implémentées
|
||||
sous forme de *storage drivers*. Beaucoup de ces *drivers* s'appuient sur des
|
||||
mécanismes existant dans le noyau, mais la diversité des configurations impose
|
||||
d'avoir plusieurs solutions pour ce problème. D'ailleurs si aucun mécanisme
|
||||
n'est disponible, Docker utilise le driver `vfs`. Ce *driver* va alors
|
||||
recopier, au moment du lancement d'un conteneur, chacune des couches ; la
|
||||
méthode n'est alors pas très optimale, mais a le mérite d'exister
|
||||
indépendamment des implémentations.
|
||||
|
||||
## Union de systèmes de fichiers
|
||||
|
||||
Le principe général de ce système de couches repose sur l'union de système de
|
||||
fichiers : il s'agit de faire une combinaison logique de deux couches (ou plus,
|
||||
selon l'implémentation), afin de créer un unique système de fichiers qui est la
|
||||
combinaison de chaque couche.
|
||||
|
||||
### Historique
|
||||
|
||||
Les premières implémentations de ce type de systèmes de fichiers est apparu
|
||||
avec les LiveCD : on disposait d'une distribution Linux complètement
|
||||
opérationnelle sur un support en lecture seule, mais on pouvait dédier un
|
||||
espace de stockage sur son disque dur (ou en RAM, au travers d'un `tmpfs`) pour
|
||||
modifier artificiellement le contenu du CD, notamment pour mettre à jour les
|
||||
paquets, ou ajouter ses propres applications, documents, photos, ...
|
||||
|
||||
Historiquement, le noyau Linux devait être *patché* pour supporter ce type de
|
||||
système de fichiers (que ce soit `unionfs` ou `aufs`, les deux principaux
|
||||
*patch* apportant cette fonctionnalité). Les systèmes BSD disposent d'une
|
||||
implémentation depuis au moins 1995 et c'est SunOS qui fût le premier OS à
|
||||
développer cette technique dès 1986 (pour un système de fichier appelé
|
||||
*Translucent File Service*). Pour Linux, il aura fallu attendre 2014 pour voir
|
||||
l'arrivée du système de fichier OverlayFS dans un noyau sans *patch*.
|
||||
|
||||
### Usages
|
||||
|
||||
En dehors de l'exemple des LiveCD que l'on vient de décrire, les unions de
|
||||
systèmes de fichiers trouvent leur intérêt également dans les systèmes
|
||||
embarqués : on peut garder le système de base en lecture seule (entre deux
|
||||
mises à jour du système) et rajouter une couche pour l'utilisateur en
|
||||
lecture/écriture, ce qui donne la possibilité de faire facilement une
|
||||
réinitialisation à la demande de l'utilisateur, ou en cas de corruption du
|
||||
système de fichiers en écriture.
|
||||
|
||||
On trouve également usage de cette fonctionnalité pour réaliser des sauvegardes
|
||||
en place des données.
|
||||
|
||||
|
||||
### Généralités
|
||||
|
||||
Les unions de systèmes de fichiers partagent un certain nombre de concepts que
|
||||
nous allons illustrer au travers du schéma suivant :
|
||||
|
||||
![Accès aux fichiers en fonction des couches](overlayfs.png){height=6cm}
|
||||
|
||||
On voit un système de fichiers à deux couches, on parle de deux *branches* dans
|
||||
le jargon. Elles sont notées *Lower* pour la couche la plus basse et *upper* la
|
||||
couche qui s'insère par dessus la couche *lower* ; et enfin *Merged* le
|
||||
résultat. Certaines implémentations supportent plus que 2 branches, avec des
|
||||
politiques d'accès et de modifications parfois complexes.
|
||||
|
||||
Lorsque l'on supprime un fichier de l'union, un fichier dit *whiteout file* est
|
||||
placé dans la couche en écriture pour indiquer que ce fichier ne doit plus être
|
||||
affiché dans la couche *merged*. Le même concept existe pour les dossiers, mais
|
||||
on parle alors d'*opaque directory*.
|
||||
|
||||
Lorsqu'il s'agit d'accéder à un fichier présent dans la branche *lower* et qui
|
||||
n'a pas été modifié dans *upper*, on accède directement au fichier de *lower*.
|
||||
|
||||
Lorsqu'un fichier est modifié, on recopie son contenu intégralement dans la
|
||||
branche *upper*, depuis la branche *lower*. Un fichier qui est ajouté, écrasé
|
||||
ou modifié aura donc son contenu intégralement dans la couche *upper*.
|
||||
|
||||
::::: {.question}
|
||||
|
||||
#### Pourrait-on se contenter d'un *Copy-on-Write* au niveau des blocs ? {-}
|
||||
\
|
||||
|
||||
C'est en effet une solution qui existe (les *snapshots* LVM par exemple, que
|
||||
Docker peut utiliser au travers du *driver* `device-mapper`). Dans ce cas,
|
||||
seuls les blocs modifiés seront réécrits, cela peut sembler être une
|
||||
alternative performante. Il faut noter cependant qu'outre les blocs liés au
|
||||
fichier modifié, il faut également mettre à jour les métadonnées (*inodes*,
|
||||
...).
|
||||
|
||||
Dans les scénarios d'écriture intensive, il s'avère que ce type de système perd
|
||||
beaucoup en performance face à une union de système de fichiers.
|
||||
|
||||
Il est généralement admis également que le *Copy-on-Write* tend à occuper
|
||||
davantage de place au fil du temps et des modifications, que l'union.
|
||||
|
||||
:::::
|
||||
|
||||
|
||||
## OverlayFS
|
||||
|
||||
OverlayFS est arrivé dans le noyau 3.18, après de plus de 4 années de réécritures
|
||||
et d'amélioration structurelles, pour atteindre le niveau d'exigence et sans
|
||||
compromis nécessaire à son intégration dans le noyau officiel.
|
||||
|
||||
::::: {.question}
|
||||
|
||||
#### Quelles problématiques rendent l'implémentation d'une union de systèmes de fichier compliquée ? {-}
|
||||
\
|
||||
|
||||
L'un des problèmes les plus délicats est de trouver une manière de représenter
|
||||
les suppressions de fichiers et de dossiers : cela doit être un fichier valide
|
||||
(avec ou sans métadonnée) car il faut pouvoir stocker l'information
|
||||
concrètement. Dans de nombreuses implémentations, un fichier `.wh.<filename>`
|
||||
sert de *whiteout file*, ce qui peut créer des conflits avec des fichiers de
|
||||
l'utilisateur (ou réduire ses choix de noms de fichiers).
|
||||
|
||||
Un problème similaire s'applique aux dossiers : est-ce qu'il faut supprimer
|
||||
chaque fichier contenu dans le dossier ou la simple présence d'un *opaque
|
||||
directory* empêche toute découverte ?
|
||||
|
||||
L'usage de la mémoire peut vite devenir incontrôlable, surtout si
|
||||
l'implémentation autorise beaucoup de branches, car si on veut que le système
|
||||
soit performant il faudra avoir en mémoire les topologies de chaque système de
|
||||
fichiers.
|
||||
|
||||
L'implémentation de `mmap(2)` est nécessairement un cauchemar : lorsqu'un
|
||||
fichier est modifié par deux processus qui le `mmap(2)`, on s'attend
|
||||
normalement à voir les modifications dans les deux processus, or le premier à
|
||||
faire une modification a créer un nouveau fichier dans la branche accessible en
|
||||
écriture. Il est ardu de réconcilier les pointeurs deux des processus.
|
||||
|
||||
D'une manière similaire, il faut penser à la gestion des *hard links* : tous
|
||||
les pointeurs d'un contenu mis à jour devrait être modifié dans la couche en
|
||||
écriture, cependant il n'y a pas d'index des pointeurs, il n'est donc pas
|
||||
facile de retrouver les fichiers à mettre à jour.
|
||||
|
||||
Ajoutons aussi que les systèmes de fichiers sous-jacents de chacune des
|
||||
branches n'ont pas forcément les mêmes contraintes (tailles des noms de
|
||||
fichiers, attributs étendus, métadonnées, encodage des accents, ...) et qu'il
|
||||
faut réussir à jongler entre chaque, tout en retournant des erreurs cohérentes
|
||||
le cas échéant.
|
||||
|
||||
Et bien d'autres encore. Notamment `readdir(2)` qui doit être stable malgré
|
||||
les turbulences qui pourraient arriver entre deux appels, ...
|
||||
|
||||
Voir cette série d'articles résumant les différentes implémentations, leurs
|
||||
choix et différences : <https://lwn.net/Articles/325369/>,
|
||||
<https://lwn.net/Articles/327738/>.
|
||||
|
||||
:::::
|
||||
|
||||
Afin de satisfaire les contraintes d'intégration au noyau, le minimum de
|
||||
fonctionnalités ont été retenues : on ne peut notamment avoir qu'une seule
|
||||
couche en écriture, qui se positionne nécessairement au sommet, en
|
||||
superposition des autres. C'est de là que vient le nom du système de fichiers,
|
||||
puisqu'il s'agit davantage d'une superposition (*overlay*) d'un système de
|
||||
fichiers sur un autre, plutôt qu'une union de plusieurs systèmes aux politiques
|
||||
d'écritures potentiellement plus variées.
|
||||
|
||||
|
||||
### Utilisation
|
||||
|
||||
L'usage d'OverlayFS est plus complexe que la plupart des autres systèmes de
|
||||
fichiers. Il faut bien évidemment indiquer le/les systèmes de fichiers à
|
||||
utiliser comme branches basses, ainsi que l'éventuelle couche en
|
||||
lecture/écriture, mais il faut aussi disposer d'un dossier de travail, qui
|
||||
permettra à l'implémentation de préparer certaines actions qui nécessitent
|
||||
d'être atomiques.
|
||||
|
||||
On peut réaliser une opération atomique en déplaçant un fichier préalablement
|
||||
créé et rempli (plutôt qu'en le créant et en l'écrivant en place). Afin de
|
||||
pouvoir satisfaire à l'atomicité, le répertoire *upper* et le dossier de
|
||||
travail doivent être obligatoirement sur le même système de fichiers. Dans le
|
||||
cas contraire, un appel à `rename(2)` retournerait `EXDEV` et l'opération ne
|
||||
pourrait alors pas être atomique.
|
||||
|
||||
Voici un exemple général de création d'une union simple entre un système de
|
||||
fichiers en lecture seule et un en lecture/écriture :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
mount -t overlay -olowerdir=/lower,upperdir=/upper,workdir=/work ignored /merged
|
||||
```
|
||||
</div>
|
||||
|
||||
Le type à utiliser est `overlay`, avec les options `lowerdir` qui indique
|
||||
l'emplacement du/des dossiers à combiner en lecture seule (on les sépare par
|
||||
des `:` lorsqu'il y en a plusieurs), on indique également le répertoire
|
||||
contenant le système en lecture/écriture dans l'option `upperdir`, et on il
|
||||
faut pas oublier l'option `workdir` un chemin sur la même partition que
|
||||
l'`upperdir`, qui doit être vide.
|
||||
|
||||
On termine l'appel par donner le périphérique source, qui est inutile dans
|
||||
notre cas (`ignored` ou tout autre chaîne fera l'affaire), et enfin le dossier
|
||||
vers lequel sera monté notre union : `/merged` dans l'exemple.
|
||||
\
|
||||
|
||||
Analysons un conteneur Docker en cours d'exécution pour en apprendre davantage.
|
||||
|
||||
D'abord, on vérifie que l'on utilise bien le *storage driver* `overlay2` :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ docker info | grep "Storage Driver"
|
||||
Storage Driver: overlay2
|
||||
```
|
||||
</div>
|
||||
|
||||
C'est le cas (en fonction de la configuration de votre noyau, Docker aura
|
||||
peut-être choisi un *driver* différent), commençons donc l'analyse :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ docker container run --rm -it debian
|
||||
incntr$ mount | grep "on / "
|
||||
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/B62UNV3UB3X4TBWQMM6XCMM6W5:/var/lib/docker/overlay2/l/V6HGFN3C3PEW6CZ6XWRSHHDKJH,upperdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/diff,workdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/work)
|
||||
```
|
||||
</div>
|
||||
|
||||
On remarque que 2 `lowerdir` sont utilisés. Il s'agit de liens symboliques
|
||||
pointant vers les dossiers identifiant les couches (les noms des liens sont
|
||||
aléatoires, il s'agit en fait d'avoir un chemin raccourci par rapport au chemin
|
||||
complet vers le système de fichiers de la couche, car le nombre de caractères
|
||||
que l'on peut passer à l'appel système `mount(2)` est limité).
|
||||
|
||||
La branche la plus basse (le plus à droite du paramètre `lowerdir`) contient
|
||||
l'unique couche de notre image `debian`, celle un peu plus à gauche superpose
|
||||
un certain nombre de fichiers de configuration nécessaire à l'exécution du
|
||||
conteneur (`/etc/hosts`, `resolv.conf`, ...).
|
||||
|
||||
La branche en lecture/écriture est également enregistrée dans le dossier
|
||||
`/var/lib/docker/overlay2` et l'on peut voir son identifiant. L'`upperdir` se
|
||||
trouve dans le dossier `diff`, tandis que le `workdir` est dans le dossier
|
||||
`work`, sous le même identifiant de couche.
|
||||
|
||||
On peut également voir les dossiers utilisés en inspectant notre conteneur :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ docker container inspect youthful_wilbur | jq .[0].GraphDriver.Data
|
||||
```
|
||||
```json
|
||||
{
|
||||
"LowerDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31-init/diff:/var/lib/docker/overlay2/2cc3656c06...c0fb91d6/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/work"
|
||||
}
|
||||
```
|
||||
</div>
|
||||
|
||||
Si on teste avec une image avec plus de couches, on obtient davantage de
|
||||
`lowerdir`, un par couche. N'hésitez pas à faire la même série de commandes
|
||||
avec l'image `python` par exemple.
|
||||
|
||||
|
||||
### Ajout de fichiers
|
||||
|
||||
À ce stade, si nous regardons le contenu de notre dossier `upperdir`, nous
|
||||
pouvons remarqué que celui-ci est vide. C'est normal puisque nous n'avons apporté
|
||||
aucune modification.
|
||||
|
||||
Dans notre conteneur précédemment lancé, apportons une modification, en
|
||||
ajoutant un fichier :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
incntr$ echo "newfile" > /root/foobar
|
||||
```
|
||||
</div>
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
└── root
|
||||
└── foobar
|
||||
```
|
||||
</div>
|
||||
|
||||
Notre nouveau fichier, qui n'est pourtant pas le seul dans l'arborescence que
|
||||
l'on voit dans le conteneur, a été ajouté comme on pouvait s'y attendre, dans
|
||||
la branche en lecture/écriture.
|
||||
|
||||
|
||||
### Modification de fichiers
|
||||
|
||||
Si nous apportons une modification à un fichier, par exemple en ajoutant une
|
||||
ligne, ce n'est pas seulement la différence qui est stockée dans la branche en
|
||||
écriture, mais bien tout le fichier, tel qu'il a été modifié :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
incntr$ echo "Bienvenue dans le conteneur" >> /etc/issue
|
||||
```
|
||||
</div>
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
└── etc
|
||||
└── issue
|
||||
```
|
||||
</div>
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/issue
|
||||
Debian GNU/Linux 11 \n \l
|
||||
Bienvenue dans le conteneur
|
||||
```
|
||||
</div>
|
||||
|
||||
|
||||
### Suppression de fichiers
|
||||
|
||||
Lorsque l'on souhaite supprimer un fichier que l'on vient d'ajouter, il n'y a
|
||||
pas grand chose à faire puisque supprimer ce fichier de la branche en écriture
|
||||
fera bien disparaître le fichier de l'arborescence montée.
|
||||
|
||||
Lorsqu'il s'agit de supprimer un fichier présent dans une branche en lecture
|
||||
seule, il faut réussir à faire en sorte de masquer ce fichier au moyen d'un
|
||||
marqueur. En fonction du *storage driver*, ce marqueur est différent : dans
|
||||
`OverlayFS`, une suppression est matérialisée par un fichier spécial de type
|
||||
caractère du même nom.
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
incntr$ rm /etc/adduser.conf
|
||||
```
|
||||
</div>
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/1531651afa872006a4b2b9b913d5d8ee317cf12be7883517ba77f3d094f871b4/diff
|
||||
└── etc
|
||||
└── adduser.conf
|
||||
```
|
||||
</div>
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
cat: No such device or address
|
||||
|
||||
42sh$ stat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
File: /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
Size: 0 Blocks: 0 IO Block: 4096 character special file
|
||||
Device: fe0bh/65035d Inode: 515773 Links: 2 Device type: 0,0
|
||||
```
|
||||
</div>
|
||||
|
||||
Notons ici `Device type: 0,0`.
|
||||
|
||||
Pour créer nous-mêmes un fichier similaire, il faudrait utiliser :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ mkdir /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin
|
||||
42sh$ mknod /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin/sh c 0 0
|
||||
```
|
||||
</div>
|
||||
|
||||
::::: {.warning}
|
||||
|
||||
Faire cette commande `mknod` alors que l'union de système de fichiers est
|
||||
montée par ailleurs ne va pas faire disparaître le fichier `/bin/sh` car les
|
||||
modifications qui pourraient être apportées aux branches en dehors du système
|
||||
monté conduisent à des résultats explicitement indéfinis.
|
||||
|
||||
:::::
|
||||
|
||||
|
||||
### Suppressions pour `unionfs` et AuFS
|
||||
|
||||
Le concept de *whiteout file*, comme on a pu le voir, diffère en fonction du
|
||||
système de fichiers. Il s'avère que même si l'OverlayFS a été intégré dans le
|
||||
noyau Linux après maintes péripéties, Docker, lorsqu'à été spécifié le format
|
||||
des archives utilisées pour distribuer les couches, utilise aujourd'hui le
|
||||
format d'AuFS pour représenter les suppressions. Il est donc important de le
|
||||
voir également.
|
||||
|
||||
Au lieu d'utiliser un fichier spécial, AuFS crée un fichier standard
|
||||
`.wh.<filename>`, où `<filename>` est le nom du fichier à masquer.
|
||||
|
||||
Afin de s'adapter au *storage driver*, lors de la décompression de l'archive,
|
||||
Docker s'emploie à convertir[^MOBYWHITEOUT] les *whiteout files* qu'il rencontre dans le
|
||||
format attendu.
|
||||
|
||||
[^MOBYWHITEOUT]: Voir le code
|
||||
<https://github.com/moby/moby/blob/master/pkg/archive/archive_linux.go#L27>
|
|
@ -1,6 +1,12 @@
|
|||
include ../pandoc-opts.mk
|
||||
|
||||
SOURCES = tutorial.md oci.md registry.md runc.md linuxkit.md linuxkit-content.md rendu.md
|
||||
SOURCES = tutorial.md \
|
||||
registry.md \
|
||||
../4/filesystem.md ../4/filesystem-ex.md ../4/filesystem-more.md \
|
||||
oci.md \
|
||||
runc.md \
|
||||
linuxkit.md linuxkit-content.md \
|
||||
rendu.md
|
||||
|
||||
|
||||
all: tutorial.pdf
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
## Prérequis
|
||||
|
||||
Si vous n'avez pas déjà le binaire `linuxkit`, vous pouvez télécharger ici :\
|
||||
Si vous n'avez pas déjà le binaire `linuxkit`, vous pouvez le télécharger
|
||||
ici :\
|
||||
<https://github.com/linuxkit/linuxkit/releases/latest>.
|
||||
|
||||
Notez qu'étant donné qu'il est écrit en Go, aucune dépendance n'est nécessaire
|
||||
en plus du binaire[^lollibc] ;-)
|
||||
en plus du binaire[^lollibc]. Un simple `chmod +x` vous permettra de l'exécuter
|
||||
depuis n'importe quel dossier
|
||||
|
||||
[^lollibc]: à condition tout de même que vous utilisiez une libc habituelle.
|
||||
|
||||
|
@ -19,51 +21,77 @@ parties :
|
|||
- `kernel` : il est attendu ici une image OCI contenant le nécessaire pour
|
||||
pouvoir utiliser un noyau : l'image du noyau, ses modules et un initramfs ;
|
||||
- `init` : l'ensemble des images OCI de cette liste seront fusionnés pour
|
||||
donner naissance au *rootfs* de la machine. On n'y place normalement qu'un
|
||||
gestionnaire de conteneur, qui sera chargé de lancer chaque conteneur au bon
|
||||
donner naissance au *rootfs* de la machine. On y place normalement qu'un
|
||||
gestionnaire de conteneurs, qui sera chargé de lancer chaque conteneur au bon
|
||||
moment ;
|
||||
- `onboot`, `onshutdown` et `services` : il s'agit de conteneurs qui seront
|
||||
lancés par le système disponible dans l'`init`, au bon moment. Les conteneurs
|
||||
indiqués dans `onboot` seront lancés **séquentiellement** au démarrage de la
|
||||
machine, ceux dans `onshutdown` seront lancés lors de l'arrêt de la
|
||||
machine. Les conteneurs dans `services` seront lancés simultanément une fois
|
||||
que le dernier conteneur de `onboot` aura rendu la main ;
|
||||
- `files` : des fichiers supplémentaires à placer dans le rootfs.
|
||||
lancés par le gestionnaire disponible dans l'`init`, au moment désigné.\ Les
|
||||
conteneurs indiqués dans `onboot` seront lancés **séquentiellement** au
|
||||
démarrage de la machine, ceux dans `onshutdown` seront lancés lors de l'arrêt
|
||||
de la machine. Les conteneurs dans `services` seront lancés simultanément une
|
||||
fois que le dernier conteneur de `onboot` aura rendu la main ;
|
||||
- `files` : des fichiers supplémentaires à placer dans le système de fichier à
|
||||
l'emplacement déterminé.
|
||||
|
||||
Le format est documenté ici :
|
||||
<https://github.com/linuxkit/linuxkit/blob/master/docs/yaml.md>
|
||||
|
||||
|
||||
## Hello?
|
||||
### Hello?
|
||||
|
||||
L'image la plus simple que l'on puisse réaliser pourrait être :
|
||||
|
||||
<div lang="en-US">
|
||||
```yaml
|
||||
kernel:
|
||||
image: linuxkit/kernel:5.10.76
|
||||
image: linuxkit/kernel:5.10.104
|
||||
cmdline: "console=tty0 console=ttyS0"
|
||||
init:
|
||||
- linuxkit/init:eb597ef74d808b5320ad1060b1620a6ac31e7ced
|
||||
- linuxkit/runc:21dbbda709ae138de0af6b0c7e4ae49525db5e88
|
||||
- linuxkit/containerd:2f0907913dd54ab5186006034eb224a0da12443e
|
||||
- linuxkit/init:8f1e6a0747acbbb4d7e24dc98f97faa8d1c6cec7
|
||||
- linuxkit/runc:f01b88c7033180d50ae43562d72707c6881904e4
|
||||
- linuxkit/containerd:de1b18eed76a266baa3092e5c154c84f595e56da
|
||||
onboot:
|
||||
- name: dhcpcd
|
||||
image: linuxkit/dhcpcd:v0.8
|
||||
image: linuxkit/dhcpcd:52d2c4df0311b182e99241cdc382ff726755c450
|
||||
command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
|
||||
services:
|
||||
- name: getty
|
||||
image: linuxkit/getty:ed32c71531f5998aa510847bb07bd847492d4101
|
||||
image: linuxkit/getty:76951a596aa5e0867a38e28f0b94d620e948e3e8
|
||||
env:
|
||||
- INSECURE=true
|
||||
trust:
|
||||
org:
|
||||
- linuxkit
|
||||
```
|
||||
</div>
|
||||
|
||||
L'image `getty` est très pratique pour déboguer, car elle permet d'avoir un
|
||||
shell sur la machine !
|
||||
On retrouve nos différentes sections : `kernel` indique qu'il faut récupérer
|
||||
l'image `linuxkit/kernel` depuis le registre Docker : il ne s'agit pas d'une
|
||||
image qui sera lancé, elle est plutôt utilisée comme une archive de stockage
|
||||
pour le noyau et ses modules. LinuxKit au moment de la construction de l'image
|
||||
se chargera de placer les fichiers aux bons endroits.
|
||||
|
||||
Ensuite, nous avons une section `init` qui déclare 3 images Docker :
|
||||
- `linuxkit/init` contient les fichiers de base et un binaire `/sbin/init` qui
|
||||
servira de système d'initialisation ;
|
||||
- `linuxkit/runc` nous donnera les outils pour lancer des conteneurs ;
|
||||
- `linuxkit/containerd` apporte un daemon pour gérer les conteneurs pendant
|
||||
leur durée de vie.
|
||||
|
||||
Ces trois images ne sont pas non plus des images Docker conventionnelles, dans
|
||||
le sens où on ne peut pas les utiliser pour faire un `docker container
|
||||
run`. Elles contiennent chacune une partie de l'arborescence du système de
|
||||
fichiers, uniquement les fichiers nécessaire, en plus, au fonctionnement du
|
||||
programme qu'elles ajoutent. Les images déclarées dans la section `init` seront
|
||||
fusionnées ensemble et formeront le système de fichiers de base de notre image
|
||||
LinuxKit.
|
||||
|
||||
Enfin les sections `onboot` et `services` sont plus conventionnelles : il
|
||||
s'agit bien d'images Docker, les conteneurs seront lancés comme tel, à partir
|
||||
d'une configuration `runc` qui sera générée au moment de la construction de
|
||||
l'image LinuxKit.
|
||||
|
||||
L'image `getty` est notamment très pratique pour déboguer, car elle permet
|
||||
d'avoir un shell sur la machine !
|
||||
|
||||
::::: {.more}
|
||||
|
||||
On notera cependant que, positionné dans `services`, le shell que nous
|
||||
obtiendrons sera lui-même exécuté dans un conteneur, nous n'aurons donc pas un
|
||||
|
@ -74,6 +102,8 @@ la partie `init`, elle sera alors lancée comme un équivalent de
|
|||
[^infogetty]: Plus d'infos à
|
||||
<https://github.com/linuxkit/linuxkit/blob/master/pkg/getty/README.md#linuxkit-debug>
|
||||
|
||||
:::::
|
||||
|
||||
|
||||
## `namespaces`
|
||||
|
||||
|
@ -81,7 +111,7 @@ Chaque nouveau conteneur est donc lancé dans un espace distinct où il ne pourr
|
|||
pas interagir avec les autres, ou déborder s'il s'avérait qu'il expose une
|
||||
faille exploitable.
|
||||
|
||||
Néanmoins, contrairement à Docker qui va de base nous dissocier du maximum de
|
||||
Néanmoins, contrairement à Docker qui, de base, va nous dissocier du maximum de
|
||||
*namespaces*, `linuxkit` ne le fait pas pour les *namespaces* `net`, `ipc` et
|
||||
`uts`. C'est-à-dire que, par défaut, la pile réseau est partagée entre tous les
|
||||
conteneurs, tout comme les IPC et le nom de la machine.
|
||||
|
@ -173,10 +203,29 @@ serveur SSH aux `services` :
|
|||
<div lang="en-US">
|
||||
```yaml
|
||||
- name: sshd
|
||||
image: linuxkit/sshd:add8c094a9a253870b0a596796628fd4ec220b70
|
||||
image: linuxkit/sshd:4696ba61c3ec091328e1c14857d77e675802342f
|
||||
binds.add:
|
||||
- /root/.ssh:/root/.ssh
|
||||
```
|
||||
</div>
|
||||
|
||||
::::: {.question}
|
||||
|
||||
#### Que fait la ligne `binds.add` ? {-}
|
||||
\
|
||||
|
||||
Avec l'instruction `binds.add`, LinuxKit va créer un *bind mount* selon le même
|
||||
principe que les volumes des conteneurs Docker. Ici nous allons partager le
|
||||
dossier `/root/.ssh` de notre image LinuxKit avec celui du conteneur `sshd`.
|
||||
\
|
||||
|
||||
Un certain nombre de *bind mounts* sont effectués par défaut. Ceux-ci sont
|
||||
déclarés dans les métadonnées des images. Pour avoir la liste, il convient de
|
||||
regarder le fichier `build.yml` de chaque image :\
|
||||
<https://github.com/linuxkit/linuxkit/blob/master/pkg/sshd/build.yml#L5>
|
||||
|
||||
:::::
|
||||
|
||||
Comme nous n'avons défini aucun mot de passe, il va falloir utiliser une clef
|
||||
SSH pour se connecter. Voilà un bon début d'utilisation de la section `files` :
|
||||
|
||||
|
@ -188,12 +237,10 @@ SSH pour se connecter. Voilà un bon début d'utilisation de la section `files`
|
|||
```
|
||||
</div>
|
||||
|
||||
Ceci va aller chercher votre clef RSA publique sur votre machine, pour
|
||||
la placer directement comme contenu du fichier `authorized_keys`.
|
||||
Ceci va aller chercher votre clef RSA publique sur votre machine, pour la
|
||||
placer directement comme contenu du fichier `authorized_keys`. À adapter en
|
||||
fonction de votre situation.
|
||||
|
||||
Notons qu'il est inutile d'ajouter un *bind mount* du dossier `.ssh` ainsi
|
||||
recopié, car le *package* `linuxkit/sshd` défini déjà cela :\
|
||||
cf. <https://github.com/linuxkit/linuxkit/blob/master/pkg/sshd/build.yml#L5>
|
||||
|
||||
|
||||
## Interface réseau virtuelle
|
||||
|
|
|
@ -7,6 +7,8 @@ Formée en juin 2015, l'Open Container Initiative (OCI) a pour but d'établir le
|
|||
standard commun aux programmes de contenerisation, afin d'éviter une
|
||||
fragmentation de l'écosystème.
|
||||
|
||||
## Spécifications
|
||||
|
||||
Trois spécifications ont été écrites :
|
||||
|
||||
- [`runtime-spec`](https://github.com/opencontainers/runtime-spec/blob/master/spec.md#platforms): définis les paramètres du démarrage d'un conteneur ;
|
||||
|
@ -14,7 +16,7 @@ Trois spécifications ont été écrites :
|
|||
- [`distribution-spec`](https://github.com/opencontainers/distribution-spec/blob/master/spec.md): définis la manière dont sont partagées et récupérées les images.
|
||||
|
||||
|
||||
## `runtime-spec`
|
||||
### `runtime-spec`
|
||||
|
||||
`runc` est l'implémentation de cette spécification ; elle a été extraite de
|
||||
`docker`, puis donnée par Docker Inc. à l'OCI.
|
||||
|
@ -38,7 +40,7 @@ plus de conteneur a proprement parlé, il fait seulement en sorte d'atteindre
|
|||
l'état voulu par cette spécification, avant de passer la main à `runc`.
|
||||
|
||||
|
||||
## `image-spec`
|
||||
### `image-spec`
|
||||
|
||||
Une image OCI est composée d'un manifest, d'une suite de couches de systèmes de
|
||||
fichiers, d'une configuration ainsi qu'un index d'image optionnel.
|
||||
|
@ -68,14 +70,14 @@ là-dedans que finissent toutes les métadonnées que l'on inscrit dans nos
|
|||
des couches du système de fichiers, ainsi que l'historique de l'image.
|
||||
|
||||
|
||||
## `distribution-spec`
|
||||
### `distribution-spec`
|
||||
|
||||
Dernière née de l'organisme, cette spécification fédère la notion de
|
||||
*registre* : une API REST sur HTTP où l'on peut récupérer des images, mais
|
||||
aussi en envoyer.
|
||||
|
||||
|
||||
## Pour aller plus loin {-}
|
||||
### Pour aller plus loin {-}
|
||||
|
||||
Si maintenant `docker` fait appel à un programme externe pour lancer
|
||||
effectivement nos conteneurs, c'est que l'on peut changer cette
|
||||
|
|
|
@ -49,7 +49,7 @@ registre.
|
|||
Avec `jq`, on peut l'extraire grâce à :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
| jq -r .token
|
||||
```
|
||||
</div>
|
||||
|
@ -149,7 +149,7 @@ tar xzf ${DL_LAYER} -C rootfs
|
|||
Et voilà, nous avons extrait notre première image, nous devrions pouvoir :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# chroot rootfs /hello
|
||||
Hello from Docker!
|
||||
[...]
|
||||
|
@ -164,7 +164,7 @@ Hello from Docker!
|
|||
Réalisez un script pour automatiser l'ensemble de ces étapes :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh$ cd $(mktemp -d)
|
||||
|
||||
42sh$ ./registry_play library/hello-world:latest
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
\newpage
|
||||
|
||||
`runc`
|
||||
======
|
||||
------
|
||||
|
||||
`runc` est le programme qui est responsable de la création effective du
|
||||
conteneur : c'est lui qui va mettre en place toute la machinerie, les points de
|
||||
|
@ -18,14 +16,14 @@ Pour appréhender l'utilisation de `runc` sans l'aide de Docker, nous allons
|
|||
essayer de lancer un shell `alpine` avec un volume dans notre home.
|
||||
|
||||
|
||||
## Prérequis
|
||||
### Prérequis
|
||||
|
||||
Vous devriez avoir le binaire `runc` ou `docker-runc`. Si ce n'est pas le cas,
|
||||
vous pouvez télécharger la dernière version :
|
||||
<https://github.com/opencontainers/runc/releases>.
|
||||
|
||||
|
||||
## Extraction du rootfs
|
||||
### Extraction du rootfs
|
||||
|
||||
À l'aide du script d'extraction de registre déjà réalisé, extrayons le
|
||||
*rootfs* d'alpine : `library/alpine` dans le registre Docker.
|
||||
|
@ -40,7 +38,7 @@ docker image save alpine | tar xv -C rootfs
|
|||
</div>
|
||||
|
||||
|
||||
## Modèle de configuration
|
||||
### Modèle de configuration
|
||||
|
||||
L'écriture complète d'un fichier `config.json` pour `runc` est plutôt
|
||||
fastidieux et répétitif, nous allons donc gagner du temps et utiliser la
|
||||
|
@ -57,7 +55,7 @@ Pour savoir à quoi correspondent tous ces éléments, vous pouvez consulter :\
|
|||
|
||||
Rassurez-vous, il n'y a que très peu de champs à modifier.
|
||||
|
||||
## Test brut
|
||||
### Test brut
|
||||
|
||||
Voici comment nous pouvons tester le fonctionnement de notre *bundle* :
|
||||
|
||||
|
@ -87,7 +85,7 @@ virli1 12345 running /tmp/work/runctest 2012-12-12T12:12:12.123456789Z root
|
|||
</div>
|
||||
|
||||
|
||||
## Attacher notre `home`
|
||||
### Attacher notre `home`
|
||||
|
||||
Dans le modèle de `config.json`, il y a déjà de nombreux systèmes de fichiers
|
||||
qui sont montés. Nous pouvons les filtrer avec :
|
||||
|
|
Loading…
Reference in New Issue