Désacraliser le overlay filesystem de Linux dans Docker
By WORMS David
3 juin 2021
Ne ratez pas nos articles sur l'open source, le big data et les systèmes distribués, fréquence faible d’un email tous les deux mois.
Le overlay filesystem (également appelés union filesystems) est une technologie fondamentale dans Docker pour créer des images et des conteneurs. Ils permettent de créer une union de répertoires pour créer un système de fichiers. Plusieurs systèmes de fichiers, qui ne sont que des répertoires, sont superposés les uns sur les autres pour créer un nouveau système de fichiers. Ces répertoires sont appelés des couches, layers en anglais, et le processus d’unification est appelé un montage d’union. Si deux fichiers ayant le même chemin d’accès existent dans deux couches, seul le dernier fichier apparaîtra dans le overlay filesystem.
Nous apprendrons comment créer nous-mêmes un overlay filesystem et comment Docker l’utilise pour construire des images et exécuter des conteneurs.
Créer un overlay filesystem est facile
Le overlay filesystem est composé de deux types de systèmes de fichiers. Un ou plusieurs systèmes de fichiers dit lower (inférieurs) qui sont immuables. Leur contenu est seulement lu et aucune modification ne se produira à l’intérieur. Un système de fichiers dit upper (supérieur) reçoit tous les changements du overlay filesystem, y compris les créations, modifications et suppressions de fichiers.
Créer un overlay fileystem est facile et nous allons le mettre en pratique. Pour cela, il vous fera bien sur disposer d’une machine Linux, une machine virtuelle faisant tout à fait l’affaire. Un accès root ou sudoer est requis.
Commonçons par la création de plusieurs dossiers, chacun d’entre eux correspondant à une couche. Nous avons également besoin d’un dossier mount
à l’emplacement où nous voulons que le overlay filesystem soit créé et d’un dossier workdir
pour les besoins internes.
mkdir overlay; cd overlay
mkdir \
lower-layer-1 lower-layer-2 lower-layer-3 upper-layer \
mount \
workdir
Créons également quelques fichiers dans 3 dossiers. Nous laissons le dossier supérieur vide.
echo "Content layer 1" > ./lower-layer-1/file-in-layer-1
echo "Content layer 2" > ./lower-layer-2/file-in-layer-2
echo "Content layer 3" > ./lower-layer-3/file-in-layer-3
Deux répertoires ou plus sont nécessaires. Ils constituent une liste de répertoires inférieurs et un répertoire supérieur. Les répertoires inférieurs du système de fichiers sont en lecture seule, alors que le répertoire supérieur peut être utilisé à la fois en lecture et en écriture. La commande mount
crée le overlay filesystem avec le type externe -t
fixé à overlay
. Elle doit être exécutée en tant que root
.
sudo mount -t overlay my-overlay \
-o lowerdir=$HOME/overlay/lower-layer-1:$HOME/overlay/lower-layer-2:$HOME/overlay/lower-layer-3,upperdir=$HOME/overlay/upper-layer,workdir=$HOME/overlay/workdir \
La commande df
liste tous les systèmes de fichiers avec quelques informations utiles comme la quantité d’espace libre et le type de système de fichiers lorsqu’elle est exécutée avec le flag -T
. Le flag -h
est seulement ajouté pour afficher la taille du système de fichiers dans un format lisible pour l’Humain.
df -Th | grep overlay
my-overlay overlay 20G 5.5G 14G 29% /home/ubuntu/overlay/mount
Le overlay filesystem est créé et monté dans le dossier mount
. Il contient les fichiers de tous les systèmes de fichiers originaux.
ls -l mount
total 12
-rw-rw-r-- 1 ubuntu ubuntu 13 Jun 2 22:38 file-in-layer-1
-rw-rw-r-- 1 ubuntu ubuntu 13 Jun 2 22:38 file-in-layer-2
-rw-rw-r-- 1 ubuntu ubuntu 13 Jun 2 22:38 file-in-layer-3
cat mount/file-in-layer-3
Content layer 3
Essayons de créer un fichier dans le dossier mount
:
echo "new content" > mount/new-file
Il est écrit dans le répertoire supérieur upper-layer
:
tree
.
├── lower-layer-1
│ └── file-in-layer-1
├── lower-layer-2
│ └── file-in-layer-2
├── lower-layer-3
│ └── file-in-layer-3
├── mount
│ ├── new-file
│ ├── file-in-layer-1
│ ├── file-in-layer-2
│ └── file-in-layer-3
├── upper-layer
│ └── new-file
└── workdir
└── work [error opening dir]
7 directories, 8 files
Maintenant, modifions un fichier, par exemple le fichier file-in-layer-1
.
echo 'Add a new line' >> mount/file-in-layer-1
Le fichier originel présent dans lower-layer-1
n’est pas modifié. Au lieu de cela, un nouveau fichier est créé dans upper-layer
:
cat lower-layer-1/file-in-layer-1
Content layer 1
cat upper-layer/file-in-layer-1
Content layer 1
Add a new line
cat mount/file-in-layer-1
Content layer 1
Add a new line
Le fichier originel dans lower-layer-2
est toujours présent. Un nouveau fichier dans upper-layer
a été créé avec un type spécial : c’est un fichier de caractères. C’est ainsi que le overlay filesystem représente un fichier supprimé.
ls -l lower-layer-2/file-in-layer-2
-rw-rw-r-- 1 ubuntu ubuntu 13 Jun 2 22:38 lower-layer-2/file-in-layer-2
ls -l upper-layer/file-in-layer-2
c--------- 1 root root 0, 0 Jun 2 23:33 upper-layer/file-in-layer-2
Maintenant que le lab est terminé, nous pouvons démonter le système de fichiers et purger nos fichiers.
sudo umount $HOME/overlay/mount
ls -l mount/
total 0
cd ..
rm -rf overlay
Overlay dans Docker
Docker prend en charge plusieurs drivers de stockage pour écrire des données sur la couche d’un conteneur, OverlayFS est le driver de stockage recommandé. Si vous affichez les informations de votre installation locale de Docker, il y a de fortes chances qu’elle affiche le driver de stockage overlay2
.
docker info | grep "Storage Driver"
Storage Driver: overlay2
Docker utilise le overlay filesystem pour créer des images ainsi que pour positionner la couche conteneur par-dessus les couches d’images.
Lorsqu’une image est téléchargée, ses couches sont situées dans le dossier /var/lib/docker/overlay2
. Par exemple, le téléchargement d’une image à 3 couches en utilisant docker pull ubuntu
crée 3+1 répertoires. Le répertoire l
contient des identifiants de couches raccourcis en tant que liens symboliques.
docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
345e3491a907: Pull complete
57671312ef6f: Pull complete
5e9250ddb7d0: Pull complete
Digest: sha256:adf73ca014822ad8237623d388cedf4d5346aa72c270c5acc01431cc93e18e2d
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
Dans mon cas, 3 couches sont téléchargées :
ls -l /var/lib/docker/overlay2/
total 16
drwx------ 4 root root 4096 Jun 3 11:21 289a71f4e07caadc95892ac5b4027606bb93c69d1a23d0e866818cdb1179644b/
drwx------ 4 root root 4096 Jun 3 11:21 40766b9f546e9826ff353976c167f60cb615f57c01926a607ab48a2df64806ab/
drwx------ 3 root root 4096 Jun 3 11:21 88826e8f5f21df691dbd998df70d94e1b6b480e489c4dbb5999dcc8a7367159e/
drwx------ 2 root root 4096 Jun 3 11:21 l/
ls -l /var/lib/docker/overlay2/l/
total 12
lrwxrwxrwx 1 root root 72 Jun 3 11:21 NSEHV6LZKQIRKICXA2T7T5252D -> ../88826e8f5f21df691dbd998df70d94e1b6b480e489c4dbb5999dcc8a7367159e/diff/
lrwxrwxrwx 1 root root 72 Jun 3 11:21 QPAIOX2SCZPFZIIXB27PFVHUPH -> ../40766b9f546e9826ff353976c167f60cb615f57c01926a607ab48a2df64806ab/diff/
lrwxrwxrwx 1 root root 72 Jun 3 11:21 USIDUBYHQEGWIRN4JOSF74ZWIL -> ../289a71f4e07caadc95892ac5b4027606bb93c69d1a23d0e866818cdb1179644b/diff/
Ces couches sont également exposées en inspectant l’image Docker. La sortie de la commande Docker est en JSON. Nous utilisons jq pour filtrer la partie qui nous intéresse le plus.
docker image inspect ubuntu | jq -r '.[0] | {Data: .GraphDriver.Data}'
{
"Data": {
"LowerDir": "/var/lib/docker/overlay2/40766b9f546e9826ff353976c167f60cb615f57c01926a607ab48a2df64806ab/diff:/var/lib/docker/overlay2/88826e8f5f21df691dbd998df70d94e1b6b480e489c4dbb5999dcc8a7367159e/diff",
"MergedDir": "/var/lib/docker/overlay2/289a71f4e07caadc95892ac5b4027606bb93c69d1a23d0e866818cdb1179644b/merged",
"UpperDir": "/var/lib/docker/overlay2/289a71f4e07caadc95892ac5b4027606bb93c69d1a23d0e866818cdb1179644b/diff",
"WorkDir": "/var/lib/docker/overlay2/289a71f4e07caadc95892ac5b4027606bb93c69d1a23d0e866818cdb1179644b/work"
}
}
L’ordre de priorité commence par le répertoire supérieur, puis évalue les répertoires inférieurs de gauche à droite. Ainsi, les couches sont évaluées dans cet ordre :
1: 88826e8f5f21df691dbd998df70d94e1b6b480e489c4dbb5999dcc8a7367159e
2: 40766b9f546e9826ff353976c167f60cb615f57c01926a607ab48a2df64806ab
3: 289a71f4e07caadc95892ac5b4027606bb93c69d1a23d0e866818cdb1179644b
En commençant par la première couche à évaluer, son contenu est le système de fichiers Ubuntu :
ls /var/lib/docker/overlay2/88826e8f5f21df691dbd998df70d94e1b6b480e489c4dbb5999dcc8a7367159e/diff
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
A partir de là, la deuxième couche de répertoires supplémentaires :
tree /var/lib/docker/overlay2/40766b9f546e9826ff353976c167f60cb615f57c01926a607ab48a2df64806ab/diff/
├── etc
│ ├── apt
│ │ └── apt.conf.d
│ │ ├── docker-autoremove-suggests
│ │ ├── docker-clean
│ │ ├── docker-gzip-indexes
│ │ └── docker-no-languages
│ └── dpkg
│ └── dpkg.cfg.d
│ └── docker-apt-speedup
├── usr
│ └── sbin
│ ├── initctl
│ └── policy-rc.d
└── var
└── lib
└── dpkg
├── diversions
└── diversions-old
10 directories, 9 files
Et la troisième couche :
tree /var/lib/docker/overlay2/289a71f4e07caadc95892ac5b4027606bb93c69d1a23d0e866818cdb1179644b/diff/
/var/lib/docker/overlay2/289a71f4e07caadc95892ac5b4027606bb93c69d1a23d0e866818cdb1179644b/diff/
└── run
└── systemd
└── container
2 directories, 1 file
Les instructions créant les couches sont définies à l’intérieur de Dockerfile. La commande curl
télécharge le Dockerfile.
curl https://raw.githubusercontent.com/tianon/docker-brew-ubuntu-core/c5bc8f61f0e0a8aa3780a8dc3a09ae6558693117/focal/Dockerfile
Par défault, le contenu du fichier s’affiche dans la console.
FROM scratch
ADD ubuntu-focal-core-cloudimg-amd64-root.tar.gz /
# a few minor docker-specific tweaks
# see https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap
RUN set -xe \ \# https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L40-L48 && echo '#!/bin/sh' > /usr/sbin/policy-rc.d \ && echo 'exit 101' >> /usr/sbin/policy-rc.d \ && chmod +x /usr/sbin/policy-rc.d \ \# https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L54-L56 && dpkg-divert --local --rename --add /sbin/initctl \ && cp -a /usr/sbin/policy-rc.d /sbin/initctl \ && sed -i 's/^exit.*/exit 0/' /sbin/initctl \ \# https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L71-L78 && echo 'force-unsafe-io' > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup \ \# https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L85-L105 && echo 'DPkg::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };' > /etc/apt/apt.conf.d/docker-clean \ && echo 'APT::Update::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };' >> /etc/apt/apt.conf.d/docker-clean \ && echo 'Dir::Cache::pkgcache ""; Dir::Cache::srcpkgcache "";' >> /etc/apt/apt.conf.d/docker-clean \ \# https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L109-L115 && echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/docker-no-languages \ \# https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L118-L130 && echo 'Acquire::GzipIndexes "true"; Acquire::CompressionTypes::Order:: "gz";' > /etc/apt/apt.conf.d/docker-gzip-indexes \ \# https://github.com/docker/docker/blob/9a9fc01af8fb5d98b8eec0740716226fadb3735c/contrib/mkimage/debootstrap#L134-L151 && echo 'Apt::AutoRemove::SuggestsImportant "false";' > /etc/apt/apt.conf.d/docker-autoremove-suggests
# verify that the APT lists files do not exist
RUN [ -z "$(apt-get indextargets)" ]# (see https://bugs.launchpad.net/cloud-images/+bug/1699913)
# make systemd-detect-virt return "docker"
# See: https://github.com/systemd/systemd/blob/aa0c34279ee40bce2f9681b496922dedbadfca19/src/basic/virt.c#L434
RUN mkdir -p /run/systemd && echo 'docker' > /run/systemd/container
CMD ["/bin/bash"]
La commande ADD
a créé la première couche. La première commande RUN
a créé la deuxième couche. La deuxième commande RUN
n’a pas créé de couche car aucun fichier n’a été créé. La troisième commande RUN
a créé la troisième couche. La commande CMD
n’a pas créé de couche car elle est évaluée au moment de l’exécution lorsque le conteneur est créé à partir de l’image.
Conclusion
Une fois que nous avons compris le fonctionnement des systèmes de fichiers superposés, il est aisé de voir comment Docker a utilisé le overlay filesystem dans son Dockerfile avec une mise en cache supplémentaire entre chaque couche. Il est facilement combiné avec le chroot jail
pour fournir un système de fichiers isolé au conteneur par-dessus les systèmes de fichiers immuables des couches d’images. La distribution des images consiste simplement à combiner plusieurs images ensemble en tant qu’archive tar
.