LightcodeSysadmin Open Source

Comprendre les conteneurs Linux

Les conteneurs Linux séduisent de plus en plus d’administrateurs systèmes et de développeurs pour déployer des applications. Dans cet article, nous verrons quelles sont les technologies qui se cachent derrière Docker.

La conteneurisation

Un conteneur est une solution logicielle permettant d’exécuter des processus dans un dans un environnement isolé du reste système d’exploitation. Autrement dit, au sein d’un conteneur, vous verrez uniquement ce qui concerne ce conteneur et vous serez limité dans vos interactions avec le reste du système d’exploitation ou avec les autres conteneurs. On appelle cette technique le cloisonnement et peut vous faire penser à ce que l’on fait déjà avec la virtualisation.

La principale différence entre la conteneurisation (le fait de créer des conteneurs) et la virtualisation réside dans les moyens mis en œuvre pour réaliser l’isolation entre le système à l’intérieur du conteneur et le reste. Le logiciel utilisé pour la virtualisation (hyperviseur) va émuler du matériel virtuel que la machine virtuelle utilisera au lieu d’accéder directement aux ressources présentes de la machine. Cela ajoute une couche supplémentaire qui n’existe pas avec les conteneurs. L’isolation procuré par la conteneurisation est réalisé par le noyau du système d’exploitation hôte. Tous les conteneurs et l’hôte se partage un même noyau Linux dans lequel des espaces de nommages (namespace) sont créés.

Figure n°1 - Architecture des conteneurs et des machines virtuelles

La figure n°1 montre la différence d’architecture entre les deux systèmes. D’un côté, on voit que les conteneurs reposent sur le noyau du système d’exploitation qui les accueille. De l’autre, les machines virtuelles, utilisent le noyau du système d’exploitation à l’intérieur de la VM. Celui-ci n’a pas directement accès aux ressources physiques et utilise l’hyperviseur pour accéder aux ressources physiques (processeur, RAM, disque dur). Notons toutefois que l’accès au matériel par l’hyperviseur est pas toujours direct. Avec un hyperviseur de type 2, l’hyperviseur repose sur le système d’exploitation de l’hôte, ce qui réduit l’efficacité de la virtualisation.

Note : bien que je compare ici conteneurisation et virtualisation, les deux techniques ne rentrent pas forcément en concurrence et il est possible d’utiliser les deux technologies conjointement.

Comme on l’a vu, l’isolation est réalisée à l’aide d’espaces de nommage. Il existe différents espaces de nommage dans un noyau, voici les grandes familles :

  • Les processus : le conteneur ne peut voir que les processus démarrés dans celui-ci.
  • Les montages du système de fichier : cela permet de masquer le reste du système de fichier au conteneur.
  • Le nom d’hôte : la modification du nom du conteneur n’altère pas celui de l’hôte.
  • Le réseau : le réseau peut être configuré spécialement pour un conteneur. Sa connexion au réseau peut être différente de celle de l’hôte, il est également possible de mettre tous les conteneurs dans un même réseau ou de les séparer. Les règles de routages et de filtrages peuvent être associées uniquement à un conteneur indépendamment des autres.

Les espaces de nommage présentés ci-dessus sont présents nativement dans le noyau Linux sous l’appellation “cgroups”. Cette technologie est incluse dans le noyau Linux depuis la version 2.6.24. Cette fonctionnalité est exploitée par différentes technologies de conteneurisation comme Docker, LXC et systemd. Toutefois, ce n’est pas la seule technologie existante, OpenVZ par exemple, utilise un noyau Linux modifié pour y inclure des fonctionnalités de conteneurisation.

Utilisation des conteneurs

Il existe plusieurs raisons d’utiliser les conteneurs, la principale étant axée sur la sécurité. Le cloisonnement des applications permet, en cas d’intrusion, de limiter au maximum d’influence sur le reste du système. Par exemple, vous avez un site web compromis au sein d’un conteneur. Le pirate a maintenant la possibilité d’exécuter des commandes sur votre système et de modifier des fichiers, directement depuis votre site web. Grâce à l’isolation, le pirate aura beaucoup plus de mal à atteindre le reste de votre machine où d’autres applications sont hébergées.

Attention toutefois, le fait d’exécuter vos applications dans des conteneurs ne prévient pas de tous les risques. Une bonne pratique en matière de sécurisation des conteneurs est de donner le minimum de droit à celui-ci : limiter le nombre de montage, filtrer les connexions réseaux au strict nécessaire… Un conteneur n’aura pas besoin de serveur SSH, il est alors superflu d’en garder un actif dans le conteneur.

Un autre atout des conteneurs est la possibilité d’avoir plusieurs environnements Linux au sein d’une même machine sans avoir recours à la virtualisation qui est plus gourmande en ressource. Au sein d’une même machine, il sera plus simple d’avoir différentes versions d’un même langage de script par exemple. Cet atout peut être couplé au fait que les applications que vous développez dépendent sûrement d’autres logiciels comme un langage de script ou même de librairies tierces. Il peut être intéressant de créer un conteneur afin de transporter votre application ainsi que ses dépendances afin de faciliter leur déploiement et d’éviter les problèmes liés à des changements dans les numéros de version des dépendances. C’est un des principaux intérêts de Docker qui propose un format de conteneur permettant d’utiliser le même conteneur indépendamment de la machine utilisée (le conteneur doit quand même avoir la même architecture que la machine hôte : x86, ARM…).

Démonstration

Nous allons maintenant voir quelques exemples permettant de mieux assimiler la notion de conteneur et des différentes fonctionnalités qu’ils proposent. Vous pouvez reproduire ces exemples facilement, j’ai utilisé une machine virtuelle sous Ubuntu 14.10 pour mes tests.

Le système de fichier (avec chroot)

Pour illustrer simplement le principe d’isolation du système de fichier, nous allons utiliser la commande chroot. Cette commande permet d’exécuter un processus en lui donnant un dossier de votre système de fichier comme racine. Bien entendu, ce n’est pas une méthode de création de conteneur à proprement parler, mais cela est encore utilisé pour sécuriser des processus.

Nous allons utiliser la commande debootstrap pour télécharger une arborescence de Debian Wheezy :

root@ubuntu:~# debootstrap wheezy debchroot

Si vous regardez ce que contient ce dossier, vous verrez une arborescence Linux classique. Nous l’utiliserons pour démarrer bash afin de se rendre à “l’intérieur du chroot”. Lançons un processus bash avec comme racine, notre distribution Debian minimale :

root@ubuntu:~# chroot /root/debchroot /bin/bash

Note : il est important que le processus bash soit capable de retrouver les librairies dont il a besoin.

root@ubuntu:~# ldd debchroot/bin/bash
        linux-vdso.so.1 =>  (0x00007fffa9960000)
        libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f11f4278000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f11f4074000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f11f3caf000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f11f44a8000)

Toutes les librairies doivent être présentes dans le chroot. Si vous créez un chroot minimal avec juste quelques programmes, il ne faut pas oublier de placer correctement les librairies visibles grâce à la commande ldd.

Dans l’exemple, la distribution Debian peut s’utiliser normalement. On peut essayer d’installer un paquet avec apt-get, celui-ci fonctionnera mais des messages d’erreurs vont apparaître :

Can not write log, openpty() failed (/dev/pts not mounted?)

Cela signifie qu’il manque quelques montages pour que le système puisse fonctionner correctement. Vous pouvez les faire avec ces commandes :

root@ubuntu:~# cd chroot /root/debchroot
mount -t proc proc proc
mount -t sysfs sys sys
mount -o bind /dev dev
mount -o bind /dev/pts dev/pts

Ces montages permettent aux programmes de communiquer avec les périphériques et le noyau. Il est donc indispensable de les créer pour assurer un bon fonctionnement des applications. Les gestionnaires de conteneurs réalisent ces opérations à votre place, vous n’avez donc pas besoin de vous en soucier avec Docker ou LXC.

Les processus (avec LXC)

J’ai parlé à plusieurs reprises de LXC, c’est un logiciel qui permet de créer des conteneurs. Nous allons maintenant l’utiliser pour voir le principe d’isolation des conteneurs. Commençons d’abord par en créer un :

root@ubuntu:~# lxc-create -t download -n conteneur -- --dist ubuntu --release trusty --arch amd64

Vous le voyez, je peux choisir une distribution et une architecture. Il faut qu’elle soit adapté au processeur qui fera fonctionner le conteneur. Pour exécuter un programme dans un conteneur, on utilise la commande lxc-start :

root@ubuntu:~# lxc-start --name conteneur /bin/bash

Vous pouvez vérifier l’état de vos conteneurs avec la commande lxc-ls :

root@ubuntu:~# lxc-ls -f
NAME       STATE    IPV4  IPV6  GROUPS  AUTOSTART
-------------------------------------------------
conteneur  RUNNING  -     -     -       NO
u1         STOPPED  -     -     -       NO

Avec la commande lxc-attach, on va pouvoir créer une session Bash en mode interactif afin “d’entrer” dans notre conteneur.

root@ubuntu:~# lxc-attach --name conteneur
root@conteneur:~#

Comme en témoigne le changement de prompt, vous êtes à présent dans votre conteneur. Si on regarde la liste des processus on peut apercevoir qu’il n’y a que trois processus :

root@conteneur:~# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 Mar22 ?        00:00:00 /bin/bash
root        37     0  0 12:17 ?        00:00:00 /bin/bash
root        47    37  0 12:17 ?        00:00:00 ps -ef

Parmi les trois commandes, on retrouve en PID 1 le premier processus lancé avec la commande lxc-start. Notre conteneur ne voit qu’un petit bout de l’arbre des processus qui comporte beaucoup plus de processus comme le montre la figure n°2.

Figure n° 2 - Le même processus a deux PID différents

On peut voir sur la figure n°2 que le processus qui a le PID n°1 est bien visible sur l’hôte et possède un PID complètement différent. Le conteneur a un arbre des processus qui lui est propre et qui appartient à un namespace. Les processus sont bien visibles sur l’hôte de la machine mais sous une appellation différente.

Le réseau (avec LXC)

Les réseaux sont assez flexibles avec les conteneurs et permettent de réaliser toute sorte d’opération. Si vous avez suivi la partie précédente, vous avez créé un conteneur qui aura une interface réseau nommé eth0. Celle-ci sera différente de l’éventuelle interface portant ce nom sur votre machine hôte. D’ailleurs, si vous regardé à cette interface, vous verrez qu’elle ne comporte pas d’adresse IP car LXC a connecté votre conteneur sur un réseau différent. Ce réseau utilise DHCP pour distribuer les adresses IP aux conteneurs, pour se voir assigner une adresse IP, on peut utiliser la commande :

root@conteneur:~# dhclient eth0
root@conteneur:~# ip addr show eth0
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:16:3e:28:9f:1c brd ff:ff:ff:ff:ff:ff
    inet 10.0.3.244/24 brd 10.0.3.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::216:3eff:fe28:9f1c/64 scope link
       valid_lft forever preferred_lft forever

L’adresse IP attribuée ne se trouve pas dans le même réseau que l’interface eth0 de mon hôte. En regardant la liste des interfaces disponibles sur l’hôte, on voit deux interfaces : lxcbr0 et veth47RQMI. L’interface lxcbr0 se trouve dans le même réseau que notre conteneur.

Pour comprendre ce qui se passe, il faut s’intéresser au fonctionnement des réseaux sous Linux. Lorsque l’on créé un conteneur, un espace de nommage réseau est également créé. Par défaut, LXC utilise le type de réseau “veth” :

root@ubuntu:~# grep lxc.network.type /etc/lxc/default.conf
lxc.network.type = veth

Ce mode créé deux interfaces, la première se nomme eth0 et se situe dans le conteneur, la seconde se nomme veth47RQMI. Ces deux interfaces sont reliées, un peu comme si il y avait un câble entre les deux. L’interface veth est relié à un switch virtuel nommé lxcbr0 :

root@ubuntu:~# brctl show
bridge name     bridge id               STP enabled     interfaces
lxcbr0          8000.feeb0bcae614       no              veth47RQMI

Un serveur DHCP est démarré sur ce réseau et permet l’attribution d’adresses IP aux machine. Un espace de nommage réseau possède également sa propre table de routage et sa propre table de filtrage. Il est possible de manipuler les espaces de noms réseau à l’aide de la commande ip netns.

Conclusion

Nous avons vu quel genre d’isolation les conteneurs Linux permettaient. Leur fonctionnement s’appuie principalement sur le noyau Linux et différents outils permettent d’en tirer partie. Les conteneurs tendent à se démocratiser car ils permettent une grande souplesse tout en n’ajoutant pas de couche supplémentaire et en réduisant les couches superflues ajoutées par la virtualisation classique. Ce procédé permet de déployer plus efficacement des applications sur des centaines, voire des milliers de machines. De nombreux outils ont été créés pour gérer des clusters de machine faisant tourner des conteneurs.