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.