AprĂšs un billet sur la virtualisation et les containers, je vous proposer d’aller plus loin dans le dĂ©tail de ces derniers car il s’agit dĂ©sormais d’un Ă©lĂ©ment incontournable dans la façon de dĂ©ployer un logiciel. Je ne reviendrai pas sur ce qu’est un container puisqu’on l’a abordĂ© dans le prĂ©cĂ©dent article et vous invite Ă  aller le consulter avant si besoin. Ici, nous allons voir l’un des principaux outils du marchĂ©, Docker, ses alternatives, comment ça marche, et l’Ă©cosystĂšme d’outillage qui s’y est greffĂ© pour amĂ©liorer leur utilisation.

Si j’aborde Docker principalement, et en parallĂšle Podman, c’est tout simplement parce que c’est la techno de container avec laquelle j’ai le plus d’expĂ©rience.

Qu’est-ce que Docker

Docker est un ensemble de produits utilisant la virtualisation cĂŽtĂ© OS pour fournir un service de Platform as a Service dont la premiĂšre version date de 2013. Il est maintenu par le sociĂ©tĂ© Docker Inc. et possĂšde une version communautaire libre et gratuite sous licence Apache 2.0, ainsi qu’une offre entreprise payante sous forme propriĂ©taire. Je vous renvoie Ă  mon article sur le Cloud Computing pour y retrouver la dĂ©finition du PaaS si besoin. Il se prĂ©sente sous la forme d’un daemon qui fourni une interface en ligne de commandes pour piloter les containers et les objets dont ils ont besoin, et qui fait office d’interface avec les fonctions d’isolation du Kernel Linux (notamment les cgroups et kernel namespaces).

Je parlerai Ă©videmment du monde Linux car c’est Ă  ça que je carbure, mais Docker est Ă©galement disponible sous Microsoft Windows et MacOS. Fut un temps oĂč Docker sous Windows reposait sur Virtualbox et une image Linux pour fonctionner, mais il me semble qu’avec le sous systĂšme Linux embarquĂ© par Windows, l’outil est dĂ©sormais natif.

Docker est Ă©crit en Go et s’exploite sous la forme d’une commande unique : docker qui permet de communiquer avec le daemon dockerd. Le client peut exĂ©cuter les commandes docker depuis n’importe quelle machine autorisĂ©e sur le rĂ©seau. Le daemon dockerd ne tourne que sur le serveur hĂ©bergeant des containers. Enfin, les images Docker sont rĂ©cupĂ©rĂ©es depuis ce qu’on appelle une registry, par dĂ©faut le serveur Docker Hub proposĂ© par l’Ă©diteur. Le Container Registry peut aussi ĂȘtre un systĂšme auto hĂ©bergĂ© ou bien fourni par un Cloud Provider (exemple : Azure Container Registry, etc).

Le schéma suivant vous décrit de maniÚre succincte les interactions entre les briques de Docker selon les commandes.

archi docker L’architecture de Docker du point de vue utilisation. Comme on peut le voir, tout passe par le daemon.

  • FlĂšche rouge pour la commande docker build (crĂ©ation d’une image). Le daemon gĂ©nĂšre l’image et la stocke dans la registry locale du serveur hĂ©bergeant le servie.
  • FlĂšche verte pour la commande docker pull (rĂ©cupĂ©ration d’une image depuis un registry). Le daemon tĂ©lĂ©charge l’image depuis le service en ligne et la stocke localement.
  • FlĂšche violette pour la commande docker run (dĂ©marrage d’un container). Le daemon prend l’image disponible sur le systĂšme (Ă  dĂ©faut, il la rĂ©cupĂ©rera depuis une registry), puis dĂ©marre un container en se basant dessus.

Si historiquement Docker utilisait les fonctions namespace (isolation de process) et cgroups (affectation d’un accĂšs contrĂŽlĂ© au CPU et mĂ©moire), le daemon s’appuie depuis quelques versions sur sa propre implĂ©mentation via la libcontainer qui exploite les fonctionnalitĂ©s de virtualisation du kernel et permet ainsi d’avoir des interfaces d’abstraction avec libvirt, LXC, et systemd-nspawn (respectivement, bibliothĂšque de virtualisation, containĂ©risation, et isolation de process). Ceci est dĂ©taillĂ© dans le schĂ©ma ci-aprĂšs.

interface docker Comment le daemon Docker s’interface avec le Kernel Linux

Un container Docker Ă©tant un process du systĂšme d’exploitation, il est possible d’interagir de la mĂȘme façon que n’importe quel autre process : contrĂŽler les ressources utilisĂ©es, voir les appels systĂšme avec, etc.

Y’a pas que Docker

Comme dit prĂ©cĂ©demment, j’Ă©voque beaucoup Docker car c’est l’un de ceux dont on entend le plus parler mĂȘme s’il n’est pas l’acteur historique domaine, et c’est aussi celui avec lequel j’ai le plus d’expĂ©rience. NĂ©anmoins, il existe d’autres implĂ©mentations de containers. Voici une petite liste sans rĂ©el classement ni exhaustivitĂ©.

  • Linux VServer
  • LXC, implĂ©mentation historique des containers dans Linux
    • LXD est une surcouche de LXC dĂ©veloppĂ©e par Canonical
  • Podman, une implĂ©mentation similaire Ă  Docker (les commandes sont identiques) se passant d’un daemon, proposĂ©e nativement par Red Hat et ses dĂ©rivĂ©es.

Travaillant principalement sur Fedora et avec de la famille Red Hat, j’ai tendance Ă  utiliser nativement Podman plutĂŽt que Docker. De ce fait, les exemples qui seront donnĂ©s dans cet article seront avec cet outil.

Qu’y a-t-il dans une image de container

Qu’est-ce qu’une image de container ?

Ce qu’on appelle une image de container, c’est un ensemble ordonnĂ© de changements sur un systĂšme de fichier racine qui correspond aux paramĂštres d’exĂ©cution du container. Cette dĂ©finition peut paraĂźtre un peu compliquĂ©e, donc nous pouvons reprĂ©senter une image Docker, sous cette forme :

pancake Une image de container, c’est comme une pile de pancakes. Source pixabay

OK, ça n’aide pas plus mais l’idĂ©e est lĂ  : chaque changement qu’on applique lors de la crĂ©ation d’une image est sauvegardĂ© dans une couche (un pancake). La couche est immuable. Lorsqu’elle est enregistrĂ©e elle ne changera jamais d’Ă©tat. C’est l’ajout d’une couche supĂ©rieure qui permet d’amender la prĂ©cĂ©dente. Voici un schĂ©ma explicatif pour un container qui se contente de dire : “Hello World !”.

hellow

Notre image Hello World prĂ©sente deux couches. La premiĂšre instruction lui dit de se baser sur l’image Alpine Linux en version latest (la derniĂšre disponible). Cette premiĂšre couche de l’image fixe donc la base du systĂšme de fichier qu’elle va contenir, elle est dĂ©sormais immuable. Si je lance le container avec cette seule instruction, il ne se passera pas grand chose car le container va dĂ©marrer et 
 s’arrĂȘter.

Exemple en image.

# Mon Dockerfile contient seulement "FROM alpine:latest"
$ podman build -t helloworld -f Dockerfile 
STEP 1: FROM alpine:latest
STEP 2: COMMIT helloworld
--> d4ff818577b
d4ff818577bc193b309b355b02ebc9220427090057b54a59e73b79bdfe139b83
$ podman run --rm helloworld
# et soudain, rien ne se passa..
$

J’ajoute donc l’instruction du schĂ©ma ci dessous Ă  la source de l’image. La premiĂšre couche demeure inchangĂ©e, par contre il enregistre une seconde couche lui disant d’exĂ©cuter l’action : echo "hello world".

$ podman build -t helloworld -f Dockerfile 
STEP 1: FROM alpine:latest
STEP 2: CMD ["echo", "hello world"]
--> Using cache 812cdf0f081ae1c45a1d2e3c15e038919e1626f563f2532fa6af3b4591813d3e
STEP 3: COMMIT helloworld
--> 812cdf0f081
812cdf0f081ae1c45a1d2e3c15e038919e1626f563f2532fa6af3b4591813d3e
# ce qui donne au lancement : 
$ podman run --rm helloworld
hello world
$

Parcourir le contenu d’une image de container

AprĂšs ma petite introduction pancake, vous allez surement me dire : “c’est bien gentil, mais ne nous sait toujours pas ce qu’il y a dans l’image d’un container”. Et bien il suffit d’aller voir directement en se connectant dessus !

$ podman run --rm -it helloworld /bin/sh
/ # 

Via cette commande, j’ai demandĂ© de lancer le container en session interactive et de lui faire exĂ©cuter /bin/sh pour avoir un shell. Et qu’est-ce qu’on a lĂ  dedans ?

/ # ls -l
total 56
drwxr-xr-x    2 root     root          4096 Jun 15 14:34 bin
drwxr-xr-x    5 root     root           360 Jul 11 20:33 dev
drwxr-xr-x   15 root     root          4096 Jul 11 20:33 etc
drwxr-xr-x    2 root     root          4096 Jun 15 14:34 home
drwxr-xr-x    7 root     root          4096 Jun 15 14:34 lib
drwxr-xr-x    5 root     root          4096 Jun 15 14:34 media
drwxr-xr-x    2 root     root          4096 Jun 15 14:34 mnt
drwxr-xr-x    2 root     root          4096 Jun 15 14:34 opt
dr-xr-xr-x  565 nobody   nobody           0 Jul 11 20:33 proc
drwx------    2 root     root          4096 Jul 11 20:33 root
drwxr-xr-x    3 root     root          4096 Jul 11 20:33 run
drwxr-xr-x    2 root     root          4096 Jun 15 14:34 sbin
drwxr-xr-x    2 root     root          4096 Jun 15 14:34 srv
dr-xr-xr-x   13 nobody   nobody           0 Jul  9 15:06 sys
drwxrwxrwt    2 root     root          4096 Jun 15 14:34 tmp
drwxr-xr-x    7 root     root          4096 Jun 15 14:34 usr
drwxr-xr-x   12 root     root          4096 Jun 15 14:34 var

Un filesystem Linux, on pourrait presque croire que j’ai fait un ls -l sur mon PC, mais on a un moyen pour savoir si c’est vraiment le cas :

/ # cat /etc/alpine-release 
3.14.0
# alors que je suis sur Fedora.
# Un autre détail amusant ?
# Alpine utilise le Kernel Linux de ma distrib Fedora 33 (fc33).
/ # uname -a
Linux e72f83eb9f5f 5.11.17-200.fc33.x86_64 #1 SMP Wed Apr 28 17:34:39 UTC 2021 x86_64 Linux

Voici donc ce que contient une image de container : un filesystem Linux avec les binaires et bibliothĂšques de base nĂ©cessaires pour produire un environnement d’exĂ©cution applicatif, mais qui utilise le Kernel de l’hĂŽte et non le sien (Ă  la diffĂ©rence d’une machine virtuelle, comme indiquĂ© dans l’article dĂ©diĂ© Ă  ce sujet).

/ # grep --version
grep: unrecognized option: version
BusyBox v1.33.1 () multi-call binary.
/ # exit
# je quitte le container
$ grep --version
grep (GNU grep) 3.4
Copyright © 2020 Free Software Foundation, Inc.

Un exemple d’image multi couches

Mon petit Hello World est sympa, mais il ne permet pas de comprendre la notion de couches et le cĂŽtĂ© immuable de celles-ci. Allons donc un peu plus loin avec une image qui va partir d’une base Debian, mettre Ă  jour les paquets, et installer Hugo pour afficher sa version Ă  la fin.

hugo

Ce découpage permet de comprendre la notion de couches immuables, explications en partant du bas :

  • Couche 1 : Le filesystem de l’image est identique Ă  celui de l’image de base : Debian
  • Couche 2 : Le filesystem est amendĂ© en modifiant des fichiers via la commande de mise Ă  jour des paquets installĂ©s sur l’image de base.
  • Couche 3 : Le filesystem est Ă  nouveau modifiĂ© en installant la commande wget
  • Couche 4 : Le filesystem est encore une fois modifiĂ© avec le tĂ©lĂ©chargement du paquet deb de Hugo
  • Couche 5 : Le filesystem est mis Ă  jour avec l’installation du paquet Hugo
  • Couche 6 : Le filesystem n’est pas modifiĂ©, on demande simplement au container d’afficher la version de Hugo.

Traduit avec la commande de build de l’image, cela donne le rĂ©sultat suivant :

$ podman build -t testhugo -f Dockerfile_hugo
STEP 1: FROM debian:stable
STEP 2: RUN apt-get update -y
Get:1 http://security.debian.org/debian-security stable/updates InRelease [65.4 kB]
Get:2 http://deb.debian.org/debian stable InRelease [122 kB]
Get:3 http://deb.debian.org/debian stable-updates InRelease [51.9 kB]
Get:4 http://security.debian.org/debian-security stable/updates/main amd64 Packages [293 kB]
Get:5 http://deb.debian.org/debian stable/main amd64 Packages [7907 kB]
Get:6 http://deb.debian.org/debian stable-updates/main amd64 Packages [15.2 kB]
Fetched 8454 kB in 1s (6158 kB/s)
Reading package lists...
--> 258bb370c1c
STEP 3: RUN apt-get install wget -y
Reading package lists...
Building dependency tree...
Reading state information...
The following additional packages will be installed:
  ca-certificates libpcre2-8-0 libpsl5 libssl1.1 openssl publicsuffix
The following NEW packages will be installed:
  ca-certificates libpcre2-8-0 libpsl5 libssl1.1 openssl publicsuffix wget
0 upgraded, 7 newly installed, 0 to remove and 0 not upgraded.
Need to get 3833 kB of archives.
(....)
done.
--> dd25ee79bd1
STEP 4: RUN wget https://github.com/gohugoio/hugo/releases/download/v0.85.0/hugo_0.85.0_Linux-64bit.deb
--2021-07-11 21:12:16--  https://github.com/gohugoio/hugo/releases/download/v0.85.0/hugo_0.85.0_Linux-64bit.deb
Resolving github.com (github.com)... 140.82.121.3
Connecting to github.com (github.com)|140.82.121.3|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://github-releases.githubusercontent.com/11180687/29242680-dd96-11eb-9e55-5983351b46aa?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20210711%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210711T211217Z&X-Amz-Expires=300&X-Amz-Signature=b280d9ba59052eed76de8bd807ca43f7dbd3b559d2c66e3a668137d483279ed9&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=11180687&response-content-disposition=attachment%3B%20filename%3Dhugo_0.85.0_Linux-64bit.deb&response-content-type=application%2Foctet-stream [following]
--2021-07-11 21:12:17--  https://github-releases.githubusercontent.com/11180687/29242680-dd96-11eb-9e55-5983351b46aa?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20210711%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20210711T211217Z&X-Amz-Expires=300&X-Amz-Signature=b280d9ba59052eed76de8bd807ca43f7dbd3b559d2c66e3a668137d483279ed9&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=11180687&response-content-disposition=attachment%3B%20filename%3Dhugo_0.85.0_Linux-64bit.deb&response-content-type=application%2Foctet-stream
Resolving github-releases.githubusercontent.com (github-releases.githubusercontent.com)... 185.199.109.154, 185.199.110.154, 185.199.108.154, ...
Connecting to github-releases.githubusercontent.com (github-releases.githubusercontent.com)|185.199.109.154|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 13891226 (13M) [application/octet-stream]
Saving to: 'hugo_0.85.0_Linux-64bit.deb'

     0K .......... .......... .......... .......... ..........  0% 4.67M 3s
    50K .......... .......... .......... .......... ..........  0% 6.71M 2s
(...)
2021-07-11 21:12:18 (18.9 MB/s) - 'hugo_0.85.0_Linux-64bit.deb' saved [13891226/13891226]

--> 18ee877328b
STEP 5: RUN apt install ./hugo_0.85.0_Linux-64bit.deb -y

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Reading package lists...
Building dependency tree...
Reading state information...
The following NEW packages will be installed:
  hugo
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 0 B/13.9 MB of archives.
After this operation, 42.5 MB of additional disk space will be used.
Get:1 /hugo_0.85.0_Linux-64bit.deb hugo amd64 0.85.0 [13.9 MB]
debconf: delaying package configuration, since apt-utils is not installed
Selecting previously unselected package hugo.
(Reading database ... 7079 files and directories currently installed.)
Preparing to unpack /hugo_0.85.0_Linux-64bit.deb ...
Unpacking hugo (0.85.0) ...
Setting up hugo (0.85.0) ...
--> a2701628b8f
STEP 6: CMD ["/usr/local/bin/hugo", "version"]
STEP 7: COMMIT testhugo
--> 3367fc63c64
3367fc63c6411552cdaf261535b635700bb88adca57510ae6f2915832ceb6838

Lançons le container, il affiche la version de Hugo.

$ podman run --rm testhugo
hugo v0.85.0-724D5DB5 linux/amd64 BuildDate=2021-07-05T10:46:28Z VendorInfo=gohugoio

Quel est l’intĂ©rĂȘt de ces images ?

Je pense que vous devez l’avoir devinĂ© par vous-mĂȘme avec l’exemple de l’image Hugo. Lorsqu’on build l’image, le fichier source est exĂ©cutĂ© dans l’ordre des Ă©tapes et applique les modifications demandĂ©es Ă  l’image de base pour produire Ă  la fin un ensemble applicatif rĂ©utilisable.

Dans un premier temps, cela signifie dĂ©sormais que vous pouvez utiliser votre image Hugo n’importe oĂč, elle s’exĂ©cutera toujours de la mĂȘme façon sans avoir besoin de rĂ©installer l’outil et ses dĂ©pendances. Son lancement sera immĂ©diat et ne nĂ©cessitera plus aucune installation prĂ©alable en dehors du tĂ©lĂ©chargement de l’image depuis un registry.

Autre Ă©lĂ©ment intĂ©ressant, l’image peut servir Ă  plusieurs containers. La mĂȘme image est donc utilisĂ©e, ce qui Ă©vite la multiplicitĂ© des donnĂ©es la composant sur le disque.

Ensuite, il y a une mĂ©canique de cache lors de la construction. Si je relance la commande de build, il verra qu’il n’y a aucune diffĂ©rence par rapport Ă  l’Ă©tat actuel et conservera donc celui-ci :

$ podman build -t testhugo -f Dockerfile_hugo 
STEP 1: FROM debian:stable
STEP 2: RUN apt-get update -y
--> Using cache a174a35fb009e866a96028b5ab28b3b73b6f5daae0b63d4cf27cbfe040db6816
--> a174a35fb00
STEP 3: RUN apt-get install wget -y
--> Using cache 0894bd26b26519557b9abceb5d020dfd89723e14a1bd9486665dcc2018a315a7
--> 0894bd26b26
STEP 4: RUN wget https://github.com/gohugoio/hugo/releases/download/v0.85.0/hugo_0.85.0_Linux-64bit.deb
--> Using cache 874a24164260d2dea13c7663f18a82ad9d777930c114a79fc5ca99bd541921ad
--> 874a2416426
STEP 5: RUN apt install ./hugo_0.85.0_Linux-64bit.deb -y
--> Using cache bf4a2a9bd12839a0491e7a3f54507298fda35baf5b5ccac6fd37e031bd2d4d4d
--> bf4a2a9bd12
STEP 6: CMD ["/usr/local/bin/hugo", "version"]
--> Using cache dadc6da33c44d417691ca2e8f5e15cf8d964172bc4c4d39a7e4b2777f0d8c8b0
STEP 7: COMMIT testhugo
--> dadc6da33c4
dadc6da33c44d417691ca2e8f5e15cf8d964172bc4c4d39a7e4b2777f0d8c8b0

Mieux encore : ce cache est conservĂ© pour chaque application de couche. Dans la premiĂšre version de cette image, je faisais afficher l’aide de Hugo et non sa version. L’Ă©tat de la commande CMD Ă  la fin a Ă©tĂ© sauvegardĂ© dans le cache de podman et lorsque je le remets, il ne recalcule rien, il se contente de reprendre cette du cache. (on peut Ă©videmment lui dire de l’ignorer)

$ podman build -t testhugo -f Dockerfile_hugo 
STEP 1: FROM debian:stable
(...)
STEP 6: CMD ["/usr/local/bin/hugo", "--help"]
--> Using cache c01cec6f2227786408c098e2de7ac1ed3636359739c5e125e70d0295851d6b6b
STEP 7: COMMIT testhugo
--> c01cec6f222
c01cec6f2227786408c098e2de7ac1ed3636359739c5e125e70d0295851d6b6b

Pour résumer

Une image de container c’est donc :

  • Une image de base permettant d’interagir avec le Kernel de l’hĂŽte
  • Diverses successions de modifications pour avoir l’environnement d’exĂ©cution nĂ©cessaire Ă  notre application
  • Un objet immuable qui repartira toujours de son Ă©tat initial lorsqu’un container est dĂ©marrĂ© ou redĂ©marrĂ©

Dans un prochain billet, nous verrons comment un container interagi avec le systĂšme. Notamment sur la partie rĂ©seau et disque. Egalement, nous aborderons dans un autre article dĂ©diĂ© la notion d’orchestration qui permet de tirer partie de la puissance du modĂšle des containers.