Désacraliser le overlay filesystem de Linux dans Docker

Désacraliser le overlay filesystem de Linux dans Docker

WORMS David

By WORMS David

3 juin 2021

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.

Canada - Morocco - France

International locations

10 rue de la Kasbah
2393 Rabbat
Canada

Nous sommes une équipe passionnée par l'Open Source, le Big Data et les technologies associées telles que le Cloud, le Data Engineering, la Data Science le DevOps…

Nous fournissons à nos clients un savoir faire reconnu sur la manière d'utiliser les technologies pour convertir leurs cas d'usage en projets exploités en production, sur la façon de réduire les coûts et d'accélérer les livraisons de nouvelles fonctionnalités.

Si vous appréciez la qualité de nos publications, nous vous invitons à nous contacter en vue de coopérer ensemble.