Création de containers avec systemd

· Read in about 8 min · (1539 words)

Il m’arrive fréquemment de monter un laboratoire avec des machines virtuelles “jetables” où des containers pour faire mes tests. Dans un précédent article, j’avais présenté la solution UML, cependant, cette solution ne correspond pas à mes besoins.

Dans cet article, nous allons voir comment monter rapidement un laboratoire avec systemd spawn en créans des containers.

Création de notre premier container

Pour permettre de créer notre container, nous devons utiliser systemd, qui permet de démarrer, d’arrêter et de réinitialiser des services. Pour permettre de démarrer un container, nous allons utiliser systemd-spawn.

Tout d’abord, nous devons créer notre répertoire qui va contenir tous les fichiers de notre nouveau système:

$ mkdir ~/lab-test/test-arch

Puis, nous pouvons créer notre distribution dans le système. Pour les utilisateurs d’ArchLinux, installer le paquet arch-install-scripts pour utiliser pacstrap et permettre ainsi d’installer le paquet base:

$ pacstrap -c -d ~/lab-test/test-arch/ base

Pour les utilisateurs de Debian, il faut installer debootstrap:

$ mkdir ~/lab-test/test-debian
$ debootstrap --arch=amd64 unstable ~/lab-test/test-debian/

Maintenant que notre système est démarré, nous pouvons le démarrer:

$ sudo systemd-nspawn -D ~/lab-test/test-arch

Nous voilà dans le container et nous possédons un shell. Changer le mot de passe du compte root. Si nous souhaitons gérer les paquets via systemctl, nous aurons une erreur:

# systemctl status systemd-networkd
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down

Le message nous dit clairement que systemd n’est pas le premier processus initialisé et, en effet, si nous regardons dans /proc, c’est bash qui est initialisé:

# cat /proc/1/status | grep -i Name
Name:	bash

Pour régler ce problème, nous allons booter sur notre container. Pour cela, quitter le container et saisissez la commande suivante:

$ sudo systemd-nspawn -bD ~/lab-test/test-arch

Après avoir terminé la phase de boot, connectez-vous avec le compte root et le mot de passe spécifié précédemment, puis faisons un test pour vérifier si nous pouvons intéragir avec systemd:

# systemctl status systemd-networkd
* systemd-networkd.service - Network Service
     Loaded: loaded (/usr/lib/systemd/system/systemd-networkd.service; disabled; vendor preset: enabled)
     Active: inactive (dead)
       Docs: man:systemd-networkd.service(8)

Si vous avez des difficultés de connexions, supprimer les fichiers /etc/securetty et /usr/share/factory/etc/securetty dans votre container.

Gestionnaire des machines/containers

SystemD utilise le package systemd-machined pour gérer les machines virtuelles ainsi que les containers. Pour gérer ces machines, nous avons la commande machinectl:

$ machinectl
MACHINE   CLASS     SERVICE        OS   VERSION ADDRESSES
test-arch container systemd-nspawn arch -       -        

1 machines listed.

Lorsqu’un container est démarré, un fichier est créé dans le répertoire /run/systemd/machines/:

# This is private data. Do not parse.
NAME=test-arch
SCOPE=machine-test\\x2darch.scope
SERVICE=systemd-nspawn
ROOT=/home/geoffrey/lab-test/test-arch
ID=b4227f1d22324c759234b6810f8944e1
LEADER=31643
CLASS=container
REALTIME=1587988574107599
MONOTONIC=11122121091
NETIF=6

Gestion du réseau

Pour permettre d’avoir un accès réseau à notre container, nous allons devoir l’attacher à un bridge. Via le paquet bridge-utils, créer un bridge et lui affecter une IP:

# brctl addbr br0
# ip addr add 192.168.2.1/24 dev br0
# ip link set br0 up

La commande systemd-nspawn fournit différents paramètres pour gérer la partie réseau. Nous allons simplement créer ine interface virtuelle et la connecter à un bridge que nous avons créé précédemment:

# systemd-nspawn -bD ~/lab-test/test-arch/ --private-network --network-veth --network-bridge=br0

De retour dans notre container, nous allons lui affecter une IP et démarrer nginx:

# ip addr add 192.168.2.2/24 dev host0
# ip link set host0 up
# systemctl start nginx && systemctl status nginx
* nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
     Active: active (running) since Mon 2020-04-27 17:46:46 CEST; 7ms ago
    Process: 40 ExecStart=/usr/bin/nginx -g pid /run/nginx.pid; error_log stderr; (code=exited, status=0/SUCCESS)
   Main PID: 41 (nginx)
     CGroup: /system.slice/nginx.service
             |-41 nginx: master process /usr/bin/nginx -g pid /run/nginx.pid; error_log stderr;
             `-42 nginx: worker process
[...]

Maintenant qu’il est démarré, faisons un test sur le système hôte:

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Laboratoire réseau

Maintenant que nous avons vu comment démarrer un container et le connecter au réseau, nous allons désormais monter un petit laboratoire réseau. Voici le schéma que nous allons mettre en place:

Architecture de notre labo

Le serveur va héberger un service Nginx et proposera une simple page Web. Le client sera simplement un client HTTP. Pour permettre de gérer nos routeurs et qu’ils échangent leurs tables de routage, je vais utiliser la solution Bird.

La problématique

Comme le montre la figure ci-dessus, nos routeurs doivent avoir deux interfaces. Une vers le client et l’autre vers le routeur pair. Or, il n’est pas possible de créer deux veth différents via les paramètres --network-veth et --network-bridge. Pour palier à ce problème, nous devons créer une interface de type veth manuellement et le connecter sur notre bridge:

$ sudo ip link add ve-rt1 type veth peer name ve-rt2
$ sudo brctl addif br1 ve-rt2
$ brctl show
bridge name	bridge id		STP enabled	interfaces
br0		8000.86b54a73aaef	no		
br1		8000.e6b16988d1b0	no		ve-rt2

Puis, on démarre notre routeur:

$ sudo systemd-nspawn -D router1/ --private-network --network-veth --network-bridge=br0 --network-interface=ve-rt1

Notre laboratoire

Maintenant que nous avons résolu notre problème de double interface, nous pouvons reprendre notre laboratoire. Tout d’abord, nous allons créer nos deux routeurs Bird:

$ mkdir ~/lab-test/{router1,router2}
$ pacstrap -c -d ~/lab-test/router1 base
$ pacstrap -c -d ~/lab-test/router2 base

On démarre notre routeur 1:

router1# echo 1 > /proc/sys/net/ipv4/ip_forward
router1# ip addr add 91.0.0.1/24 dev host0 && ip link set host0 up
router1# ip addr add 10.0.0.1/24 dev ve-rt1 && ip link set ve-rt1 up
router1# cat /etc/bird.conf
[...]
protocol static static4 {
	ipv4;

	route 73.0.0.0/24 via 10.0.0.2;
}
[...]

Même configuration pour le routeur 2:

router2# echo 1 > /proc/sys/net/ipv4/ip_forward
router2# ip addr add 91.0.0.2/24 dev host0 && ip link set host0 up
router2# ip addr add 10.0.0.2/24 dev ve-rt2 && ip link set ve-rt2 up
router2# cat /etc/bird.conf
[...]
protocol static static4 {
	ipv4;

	route 91.0.0.0/24 via 10.0.0.1;
}
[...]

Puis, on démarre notre container pour le serveur:

server1# ip addr add 73.0.0.2/24 dev host0 && ip link set host0 up
server1# ip route add default via 73.0.0.1
server1# systemctl status nginx
* nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
     Active: active (running) since Thu 2020-04-30 14:49:11 CEST; 15min ago
    Process: 60 ExecStart=/usr/bin/nginx -g pid /run/nginx.pid; error_log stderr; (code=exited, status=0/SUCCESS)
   Main PID: 61 (nginx)
[...]

Et enfin, notre container pour le client:

client1# ip addr add 91.0.0.2/24 dev host0 && ip link set host0 up
client1# ip route add default via 91.0.0.1

Faisons ensuite nos tests de routage:

router1# ping -c 1 73.0.0.1
PING 73.0.0.1 (73.0.0.1) 56(84) bytes of data.
64 bytes from 73.0.0.1: icmp_seq=1 ttl=64 time=0.098 ms

--- 73.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.098/0.098/0.098/0.000 ms

router1# ping -c 1 73.0.0.2
PING 73.0.0.2 (73.0.0.2) 56(84) bytes of data.
64 bytes from 73.0.0.2: icmp_seq=1 ttl=63 time=0.232 ms

--- 73.0.0.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.232/0.232/0.232/0.000 ms

client1# ping -c 1 73.0.0.2
PING 73.0.0.2 (73.0.0.2) 56(84) bytes of data.
64 bytes from 73.0.0.2: icmp_seq=1 ttl=62 time=0.159 ms

--- 73.0.0.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.159/0.159/0.159/0.000 ms

Puis, un test d’accès vers le serveur Web depuis le client:

client1# curl -v 73.0.0.2
*   Trying 73.0.0.2:80...
* Connected to 73.0.0.2 (73.0.0.2) port 80 (#0)
> GET / HTTP/1.1
> Host: 73.0.0.2
> User-Agent: curl/7.69.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.18.0
[...]

Script complet

Voici le script qui me permet de monter un laboratoire:

#!/bin/sh

create_bridge() {
	# Create if not exist
	if [ ! -d /sys/class/net/$1 ]; then
	  sudo brctl addbr $1
	  sudo ip link set $1 up
	fi	
}

create_veth() {
	if [ ! -d /sys/class/net/$1 ]; then
		sudo ip link add $1 type veth peer name $2
		sudo ip link set $1 up && sudo ip link set $2 up
	fi
	if [ ! -d /sys/class/net/$3/brif/$2 ]
    then
		sudo brctl addif $3 $2
	fi
}

vm1=clt1
vm2=srv1
rt1=rt1
rt2=rt2

# Create bridges
bridge1=br0
bridge2=br1
bridge3=br2
create_bridge $bridge1
create_bridge $bridge2
create_bridge $bridge3

# Create veth
if1=ve-$rt1
if2=ve-$rt2
create_veth $if1 ve-to-$rt2 $bridge2
create_veth $if2 ve-to-$rt1 $bridge2

# Start containers
case $1 in
  clt1)
	sudo systemd-nspawn -bD ~/lab-test/clt1/ \
	  --private-network --network-veth --network-bridge=$bridge1
    ;;
  srv1)
	sudo systemd-nspawn -bD ~/lab-test/srv1/ \
	  --private-network --network-veth --network-bridge=$bridge3
	;;
  rt1)
	sudo systemd-nspawn -bD ~/lab-test/router1/ \
	  --private-network --network-veth --network-bridge=$bridge1 \
	  --network-interface=$if1
    ;;
  rt2)
	sudo systemd-nspawn -bD ~/lab-test/router2/ \
	  --private-network --network-veth --network-bridge=$bridge3 \
	  --network-interface=$if2
    ;;
esac

Le script prend un seul paramètre qui est le nom du container à démarrer.

Conclusion

Je confesse que je n’ai pas réussi à créer des containers et à les exécuter en tâche de fond, ce qui peut être problématique pour monter rapidement un lab. Mais à part ça, je trouve cette solution très intéressante, car elle est par défaut intégrer dans les systèmes linux et peut pourquoi pas, remplacer les containers LXC.