LXD est une suite d’outils visant à faciliter le management de la couche LXC (LinuX Containers) du noyau Linux. Les containers sont des environnements confinés où les applications peuvent s’exécuter sans attenter à l’intégrité du système hôte ainsi que des autres containers.
Plusieurs raisons sont susceptibles de motiver la mise en container d’une application ou d’un service. On peut souhaiter les isoler par rapport au système et/ou limiter la quantité de ressources leur étant accessibles et/ou les rendre autosuffisants en terme de librairies (un peu à la manière de chroot qui, je le rappelle, n’a jamais été créé pour faire de la sécurité). LXD permet à l’administrateur de créer facilement l’enveloppe de ces applications ou services en orchestrant les couches LXC, cgroups et namespaces.
1. Les cgroups
Comme mentionné ci-dessus, il est possible de mettre une application ou un service dans un container afin de limiter leurs ressources accessibles sur le système. Je fais volontairement une distinction entre une application et un service. Une application est un fichier binaire qu’on exécute, cette exécution entraîne la création d’une entité active sur le système qu’on appelle le processus. C’est cette entité qui va consommer de la mémoire et du CPU pour faire tourner le code machine du binaire exécutable. Le cgroup est un moyen de créer des groupes de processus.
Dans un système UNIX, un processus peut avoir des fils. Par exemple, lorsque vous lancez votre navigateur web, chaque onglet correspond en réalité à un processus fils de votre navigateur. Dans ce cas-là, pas de problèmes pour grouper les processus : on part du père et on récupère tous les fils. Pas besoin d’une structure de données supplémentaire comme les cgroups. Cela se complique lorsqu’on parle de service.
Un service est une vision de plus haut niveau que l’application. On pourrait dire qu’un service regroupe X applications. Par exemple, si on considère un serveur web de e-commerce, on sait qu’il s’appuie sur trois composants : un serveur HTTP (Apache), un interpréteur de script pour du web dynamique (PHP) et une base de données (MySQL). Dans ce cas, les cgroups nous proposent une structure de donnée plus agile que le modèle père/fils pour grouper les processus.
Chaque ressource limitable par les cgroup est représentée par un subsystem. Il y en a un pour le CPU, les entrées/sorties disques, la mémoire, etc. On superpose une ou plusieurs politiques à chaque subsystem, les processus suivent ensuite une politique. Petit exemple avec la consommation de CPU. Nous allons créer deux politiques, l’une avec l’utilisation par défaut du CPU et une autre limitant volontairement le processus.
Commençons par installer ce qu’il faut pour travailler avec les cgroups :
[root@lxd ~]# yum install libcgroup libcgroup-tools
Créons une politique par défaut qui n’applique pas de limites particulières :
[root@lxd ~]# cgcreate -g cpu:/cpudefault
Créons une autre politique qui dit que lorsqu’un processus est positionné dans ce cgroup, il est dans un ratio de consommation de 2:1 sur cette ressource. Le subsystem associé au CPU associe une valeur arbitraire de 1024 à la totalité du CPU. Si on veut représenter un ratio de 2:1, il faut positionner cette valeur à 512.
[root@lxd ~]# cgcreate -g cpu:/cpulimited
[root@lxd ~]# cgset -r cpu.shares=512 cpulimited
Si on lance quatre fois de suite cette commande :
[root@lxd ~]# cgexec -g cpu:cpudefault stress -c 1 --timeout 200s &
La commande cgexec permet de lancer une commande en plaçant les processus dans un cgroup (-g). Les processus vont hériter des limitations de ce groupe. La commande stress est un utilitaire qui fait des calculs pour mettre le CPU au taquet. Ici, on lui fait utiliser 100 % d’un seul CPU (-c). On obtient la sortie suivante pour top :
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3669 root 20 0 7308 96 0 R 25.3 0.0 0:01.37 stress
3667 root 20 0 7308 100 0 R 25.0 0.0 0:01.96 stress
3671 root 20 0 7308 100 0 R 25.0 0.0 0:00.90 stress
3665 root 20 0 7308 100 0 R 24.7 0.0 0:03.52 stress
Chaque processus est dans un rapport 1:1, donc tout le monde se partage équitablement le CPU disponible.
Lançons maintenant cette commande :
[root@lxd ~]# cgexec -g cpu:cpudefault stress -c 1 --timeout 200s &
Et trois fois la même commande, mais positionnée dans notre cgroup limité au niveau CPU :
[root@lxd ~]# cgexec -g cpu:cpulimited stress -c 1 --timeout 200s &
Le résultat de top est différent :
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3689 root 20 0 7308 100 0 R 66.8 0.0 0:35.62 stress
3693 root 20 0 7308 96 0 R 11.3 0.0 0:05.52 stress
3703 root 20 0 7308 100 0 R 11.3 0.0 0:01.70 stress
3691 root 20 0 7308 100 0 R 11.0 0.0 0:09.15 stress
On voit que le processus lancé avec la politique cpudefault prend 66 % du CPU tandis que les 3 autres plafonnent à 11 %. Or, 11 x 3 = 33 et 33, c’est bien la moitié de 66. Le ratio 2:1 est donc respecté.
2. Les namespaces
Les rôles des namespaces est de créer un nouveau contexte système isolé pour le processus ciblé. Cette isolation se fait sur trois axes :
- Un nouvel espace de PID est initialisé et votre processus prend le numéro 1, tous les autres processus s'en sont allés ;
- Une nouvelle pile réseau est allouée au processus, aucun conflit possible avec les services réseaux de l'hôte ;
- Les systèmes de fichiers sont indépendants, on peut monter et démonter des volumes sans que cela ait d'incidences sur l'hôte.
Nous allons illustrer les namespaces avec la commande unshare. Cette commande permet de lancer un programme en se détachant du namespace parent. Nous allons l'utiliser pour lancer bash dans un nouveau namespace.
[root@lxd ~]# unshare --fork --pid --mount-proc bash
[root@lxd ~]# ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 115440 2024 pts/0 S 21:32 0:00 bash
root 10 0.0 0.1 155360 1872 pts/0 R+ 21:32 0:00 ps -aux
Nous avons lancé bash comme fils de unshare (--fork) avec son propre espace de PID (--pid) et sa propre instance de /proc (--mount-proc). Dans ce nouveau namespace, bash a le PID 1 et il est donc le père de tous les processus et il est seul (enfin avec la commande ps qu'on a lancé). Voyons ce que ça donne depuis le système hôte avec un pstree :
[root@lxd ~]# pstree
systemd─┬─NetworkManager─┬─2*[dhclient]
│ └─2*[{NetworkManager}]
...
├─login───bash───unshare───bash
...
On voit bien la ligne de parenté systemd → login → bash → unshare → bash. Donc du point de vue de l'hôte, le processus bash issu du unshare est un processus comme un autre. Il a un PID tout à fait standard. Vous avez maintenant les clés de compréhension pour la couche LXC/LXD.
3. Les containers LXD
Avant LXD, il y eu LXC (LinuX Containers). Pour lever tout de suite l’ambiguïté, il ne s'agit pas de choisir LXD ou LXC c'est plutôt LXC et LXD. En effet, LXC est un ensemble d'outils et de librairies en espace utilisateur faisant le lien entre les namespaces et les cgroups pour unifier ces deux technologies afin d'arriver à fournir des containers utilisables. LXD complète LXC notamment en ajoutant un accès par webservice qui rend les containers orchestrables par des systèmes type OpenStack et des outils plus haut niveau pour les gérer type gestion de profil de containers, etc.
Dernière précision sur les containers avant de passer à la pratique, on distingue deux types de containers : fat (gras) et thin (mince). Cette terminologie est empruntée aux jails BSD. Le thin jail revient à containeriser juste une application (comme on l’a fait ci-dessus avec bash). Un fat jail consiste à faire tourner un système entier dans un container. De façon schématique, on prend ce qu'on a fait au-dessus sauf qu'à la place de bash, on exécute systemd (processus de démarrage des Linux modernes). Par contre, contrairement à ce qu'on a vu précédemment dans le cas de la virtualisation complète, on ne fera tourner que du Linux dans un fat jail, car il n’y a qu'un seul noyau qui tourne sur la machine.
3.1 Installation de la couche LXD
Comme sur beaucoup de distributions (mis à part Ubuntu), LXD n'est pas disponible directement sous forme de package. Ici, on doit installer les dépôts epel et copr (dont les paquets sont de qualité assez variable) pour obtenir snapd. Les snap sont des paquets d'installation qui viennent compléter le système de gestion de paquets standard avec des paquets plus portables d'une distribution à l'autre dans la mesure où ils sont fournis avec leurs dépendances binaires (par conséquent, ils s'appuient beaucoup moins sur la distribution sous-jacente). Installons tout cela :
[root@lxd ~]# yum install yum-plugin-copr epel-release
[root@lxd ~]# yum copr enable ngompa/snapcore-el7
[root@lxd ~]# yum install snapd
[root@lxd ~]# systemctl enable --now snapd.socket
[root@lxd ~]# systemctl enable snapd
Nous avons maintenant snapd d'installé sur notre CentOS. Nous pouvons configurer le noyau pour expliciter le chargement des namespaces au démarrage du noyau (user_namespace.enable=1) et nous ménager la possibilité de lancer des containers en tant que non-root (namespace.unpriv_enable=1). Toutes ces modifications se font dans le fichier /etc/default/grub :
[root@lxd ~]# vi /etc/default/grub
...
GRUB_CMDLINE_LINUX="... user_namespace.enable=1 namespace.unpriv_enable=1 ..."
...
[root@lxd ~]# grub2-mkconfig >/boot/grub2/grub.cfg
Enfin, on passe un nombre de namespaces maximum instantiables par l'utilisateur :
[root@lxd ~]# echo "user.max_user_namespaces=3883" > /etc/sysctl.d/99-userns.conf
On redémarre la machine et on peut passer à l'installation de LXD :
[root@lxd ~]# snap install lxd
2019-03-07T22:43:22+01:00 INFO Waiting for restart...
lxd 3.10 from Canonical✓ installed
3.2 Configuration initiale de LXD
La configuration des containers passe par une commande d'initialisation qui va nous permettre de créer un profil par défaut. Sauf que nous allons tout faire nous même, donc nous allons répondre non partout.
[root@lxd ~]# lxd init
Would you like to use LXD clustering? (yes/no) [default=no]: no
Créer un emplacement où stocker les containers :
Do you want to configure a new storage pool? (yes/no) [default=yes]: no
Connexion à un serveur MAAS (produit Ubuntu pour déployer des machines automatiquement) :
Would you like to connect to a MAAS server? (yes/no) [default=no]: no
Création d'un bridge (on y reviendra) :
Would you like to create a new local network bridge? (yes/no) [default=yes]: no
Utilisation de ce bridge (on y reviendra) :
Would you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no]: no
Peut-on administrer les containers par le réseau :
Would you like LXD to be available over the network? (yes/no) [default=no]: no
Mise à jour automatique des images disponibles :
Would you like stale cached images to be updated automatically? (yes/no) [default=yes] yes
Afficher la configuration (en fait toutes les configurations de LXD se font au format YAML) :
Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: no
Pour au moins tester notre installation de LXD, il va falloir créer un storage pool. En fait, c'est juste un répertoire qui va contenir nos containers. Pour créer ces choses-là à la main, on va repasser sur les commandes lxc :
[root@lxd ~]# lxc storage create store_lxd dir source=/store/lxd
Ici, on crée un pool de stockage nommé store_lxd qui est de type fichier (on peut aussi mettre des partitions en mode bloc, volume LVM, ZFS, etc.) et qui se trouve localisé physiquement dans /store/lxd. Nous allons pouvoir déployer notre première image dans ce pool fraîchement créé. Voyons si on peut descendre une image :
[root@lxd ~]# lxc launch -s store_lxd images:centos/7/amd64 apollo
Creating apollo
The container you are starting doesn't have any network attached to it.
To create a new network, use: lxc network create
To attach a network to a container, use: lxc network attach
Starting apollo
Cette commande va créer un container basé sur une image CentOS 7. Le système nous informe que ce container n'a pas de réseau, c'est normal nous allons le créer. Listons maintenant les containers en cours d’exécution :
[root@lxd ~]# lxc list
+--------+---------+------+------+------------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+--------+---------+------+------+------------+-----------+
| apollo | RUNNING | | | PERSISTENT | |
+--------+---------+------+------+------------+-----------+
Nous retrouvons bien le container apollo. Mettons un petit mot de passe pour root. La sous-commande exec de la commande lxc lance une commande dans le container ciblé :
[root@lxd ~]# lxc exec apollo passwd root
Changing password for user root.
New password:
Retype new password:
passwd: all authentication tokens updated successfully.
Quant à lxc console, cela ouvre une console dans le container :
[root@lxd ~]# lxc console apollo
To detach from the console, press: <ctrl>+a q
CentOS Linux 7 (Core)
Kernel 3.10.0-957.5.1.el7.x86_64 on an x86_64
apollo login: root
Password:
[root@apollo ~]#
Sauf que parfois on n’arrive pas à en sortir… Dans ces cas-là, on peut exécuter un bash dans le container :
[root@lxd ~]# lxc exec apollo bash
[root@apollo ~]#
Le but maintenant c'est que ce container puisse aller sur le Web. Nous allons donc nous atteler au réseau.
4. Configuration réseau
État des lieux du réseau sur Apollo :
[root@apollo ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
...
Il n'y pas grand-chose, juste une interface de rebouclage (loopback). Notre premier travail va être de préparer l'hôte pour connecter apollo au monde extérieur. C'est ici que les fameux bridges entrent en jeu. Dans l’article page XX, nous avons dit que VirtualBox distribuait les adresses réseau en DHCP. L'idée est d'alimenter apollo avec un bail DHCP fourni par VirtualBox comme on le ferait dans un réseau domestique servi par une box ou dans un réseau d'entreprise embarquant du DHCP.
L'astuce va être de configurer un bridge sur l'hôte. Un bridge est une sorte de commutateur virtuel qu'on va monter sur un système d'exploitation. Pour mémoire, un commutateur est un concentrateur permettant aux machines qui y sont connectées de communiquer au niveau liaison (niveau 2 du modèle OSI). C'est-à-dire que les cartes réseau de ces machines peuvent s'envoyer et recevoir des trames, car elles font partie du même domaine de broadcast Ethernet (en conséquence, ils pourront recevoir les bails DHCP et contacter la passerelle par défaut ainsi que le cache DNS). Donc nous allons créer un bridge sur l'hôte et y « brancher » l'interface réseau réelle de l'hôte. Ensuite, nous allons créer une interface virtuelle pour apollo et la « brancher » sur ce bridge.
Sur l'hôte, il faut d'abord installer le paquet bridge-utils :
[root@apollo ~]# yum install bridge-utils
Ensuite, il faut configurer le bridge :
[root@lxd ~]# cat /etc/sysconfig/network-scripts/ifcfg-virbr0
DEVICE="virbr0"
BOOTPROTO="dhcp"
ONBOOT="yes"
TYPE="Bridge"
NM_CONTROLLED="no"
Les paramètres intéressants sont qu’on crée un bridge (TYPE='Bridge') correspondant au device /dev/virbr0 (DEVICE), configuré en DHCP (BOOTPROTO), monté au démarrage (ONBOOT='yes'). Ensuite, nous raccordons l'interface réseau au bridge :
[root@lxd ~]# cat /etc/sysconfig/network-scripts/ifcfg-enp0s3
TYPE=Ethernet
BOOTPROTO=none
NAME=enp0s3
UUID=dc83eab1-b18a-4c24-9ad9-818479905817
DEVICE=enp0s3
NM_CONTROLLED=no
BRIDGE=virbr0
ONBOOT=yes
Il s'agit d'une interface Ethernet (TYPE), dont le nom est enp0s3 (NAME) correspondant au device /dev/enp0s3 (DEVICE) et surtout membre du bridge virbr0 (BRIDGE). À ce moment-là, le bridge est arrosé par le réseau externe via l'interface enp0s3 de l'hôte.
La dernière étape est de créer une interface réseau sur apollo liée au bridge de l'hôte. Sur l'hôte :
[root@lxd ~]# lxc config device add apollo eth0 nic name=eth0 nictype=bridged parent=virbr0
Device eth0 added to apollo
Cette commande ajoute une carte réseau (nic) nommée eth0 (name) qui va s'attacher sur un bridge (nictype=bridged) nommé virbr0 (parent=virbr0). Retournons sur apollo :
[root@apollo ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
...
5: eth0@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
...
Tentons de récupérer un bail DHCP et de pinger Google :
[root@apollo ~]# dhclient
[root@apollo ~]# ping www.google.fr
PING www.google.fr (172.217.19.227) 56(84) bytes of data.
64 bytes from par21s11-in-f3.1e100.net (172.217.19.227): icmp_seq=1 ttl=52 time=2.48 ms
64 bytes from par21s11-in-f3.1e100.net (172.217.19.227): icmp_seq=2 ttl=52 time=2.16 ms
5. Et pour finir…
Quelques petites commandes bien utiles : comment stopper et arrêter les containers :
[root@apollo ~]# lxc stop apollo
[root@apollo ~]# lxc start apollo
Pour lancer automatiquement apollo :
[root@apollo ~]# lxc config set apollo boot.autostart=true
Afficher/modifier les valeurs par défaut lors de la création du container :
[root@lxd ~]# lxc profile edit default
config: {}
description: Default LXD profile
devices: {}
name: default
used_by:
- /1.0/containers/apollo
Et enfin, afficher la configuration du container :
[root@lxd ~]# lxc config edit apollo
architecture: x86_64
config:
image.architecture: amd64
image.description: Centos 7 amd64 (20190307_07:08)
image.os: Centos
image.release: "7"
image.serial: "20190307_07:08"
volatile.base_image: 6f065bff901509e641cb74436075d37bb5fcfde14539d74363b71911b33b9fe2
volatile.eth0.hwaddr: 00:16:3e:0f:7b:eb
volatile.idmap.base: "0"
volatile.idmap.next: '[{"Isuid":true,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":1000000000}]'
volatile.last_state.idmap: '[{"Isuid":true,"Isgid":true,"Hostid":1000000,"Nsid":0,"Maprange":1000000000}]'
volatile.last_state.power: STOPPED
devices:
eth0:
name: eth0
nictype: bridged
parent: virbr0
type: nic
root:
path: /
pool: store_lxd
type: disk
ephemeral: false
profiles:
- default
stateful: false
description: ""
On y retrouve bien tous nos éléments configurés à la main.
Conclusion
Cet article vous a présenté les bases pour appréhender les containers sous Linux. Il est très important de faire l'effort d'apprivoiser LXD dès maintenant, car tous les systèmes de cloud libres majeurs tels qu’OpenStack et OpenNebula proposent des drivers LXD pour déployer des containers sur leurs nœuds physiques. Autant partir dès maintenant sur de bonnes bases !