Créer son propre cloud à la maison ?

J’utilise constamment des machines virtuelles pour tester des scripts, pour héberger des services, pour faire des tests de déploiement, etc. J’ai pour habitude d’utiliser Proxmox dans le cadre de mon lab, et Libvirt au travail.

Depuis peu, j’approfondis mes connaissances sur les clouds publiques comme AWS, GCP, Azure, etc. Et s’il y a bien une chose qui me fascine, c’est la vitesse à laquelle on peut créer une machine virtuelle.

Il m’arrive d’utiliser Cloud-Init pour automatiser la création de mes machines virtuelles ou Packer pour créer des templates de VM, mais nous parlons de quelques minutes (et non de secondes).

C’est en faisant mes recherches sur ce sujet que je suis tombé sur Firecracker, un projet open-source d’AWS qui permet de créer des microVMs en quelques millisecondes (oui oui, millisecondes). Cette solution est utilisée par les services AWS Lambda et AWS Fargate mais aussi par d’autres entreprises comme fly.io, Koyeb ou AppFleet.

Alors, je veux pouvoir créer des machines virtuelles en quelques millisecondes, mais aussi pouvoir les détruire et les recréer à la volée. De ce fait, ces machines virtuelles pourront être utilisées pour des tests, pour des déploiements, pour des services, etc.

Pourquoi ne pas utiliser des conteneurs ?

Bonne question. Je pourrais utiliser des conteneurs, mais mon objectif est d’avoir un système d’exploitation complet, avec un noyau, des services, etc. Je souhaite pouvoir utiliser des outils comme l’eBPF pour faire du monitoring, utiliser des namespaces pour isoler mes services, modifier la configuration réseau… Bref, impossible de faire tout ça avec des conteneurs.

Qu’est-ce que Firecracker ?

Firecracker est un hyperviseur open-source qui permet de créer des microVMs. Celles-ci sont des machines virtuelles légères, sécurisées et isolées. Elles sont basées sur KVM et sont donc des machines virtuelles Linux comme les autres.

Firecracker est écrit en Rust et est de ce fait compilé en binaire. Il est possible de le lancer en root ou en tant qu’utilisateur non privilégié (rootless). De même, on peut exécuter plusieurs instances de Firecracker sur la même machine (nous verrons ça plus tard).

Firecracker

Pour citer la documentation officielle : “Firecracker combine la sécurité et les propriétés d’isolation fournies par la technologie de virtualisation matérielle avec la vitesse et la flexibilité des conteneurs.

Installer Firecracker

Firecracker est disponible sous la forme d’un binaire compilé pour les architectures x86_64 et aarch64.

Avant de télécharger le binaire, nous allons vérifier que notre machine est compatible avec Firecracker.

[ -r /dev/kvm ] && [ -w /dev/kvm ] && echo "OK" || echo "FAIL"

Si la commande retourne OK, alors la machine est compatible avec la virtualisation matérielle.

ARCH="$(uname -m)"
release_url="https://github.com/firecracker-microvm/firecracker/releases"
latest=$(basename $(curl -fsSLI -o /dev/null -w  %{url_effective} ${release_url}/latest))
curl -L ${release_url}/download/${latest}/firecracker-${latest}-${ARCH}.tgz \
| tar -xz

mv release-${latest}-$(uname -m)/firecracker-${latest}-${ARCH} /usr/bin/firecracker

Astuce

Pour faire du rootless, vous pouvez créer une ACL pour que votre utilisateur ait les permissions de lecture et d’écriture sur le périphérique /dev/kvm.

sudo apt install acl -y
sudo setfacl -m u:${USER}:rw /dev/kvm

Démarrer sa première VM

Dans un premier terminal, lançons Firecracker via la commande suivante :

/usr/bin/firecracker --api-sock /tmp/firecracker.socket

Puis, dans un second terminal, nous allons créer une VM à partir de l’image hello world téléchargeable sur Amazon. Nous allons définir le kernel (et les arguments de boot), le disque et enfin démarrer la VM.

mkdir -p /var/lib/firecracker
cd /var/lib/firecracker
mkdir hello && cd hello
curl -fsSL -o hello-vmlinux.bin https://s3.amazonaws.com/spec.ccfc.min/img/hello/kernel/hello-vmlinux.bin
curl -fsSL -o hello-rootfs.ext4 https://s3.amazonaws.com/spec.ccfc.min/img/hello/fsfiles/hello-rootfs.ext4

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/boot-source' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "kernel_image_path": "/var/lib/firecracker/hello/hello-vmlinux.bin",
        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/drives/rootfs' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "drive_id": "rootfs",
        "path_on_host": "/var/lib/firecracker/hello/hello-rootfs.ext4",
        "is_root_device": true,
        "is_read_only": false
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/actions' \
    -H  'Accept: application/json' \
    -H  'Content-Type: application/json' \
    -d '{
        "action_type": "InstanceStart"
    }'

Sur le premier terminal, nous nous retrouvons avec une interface nous permettant de nous connecter à la machine virtuelle. C’est grâce au bootarg console=ttyS0 rendant disponible un terminal via la liaison série de l’instance nouvellement créée.

Welcome to Alpine Linux 3.8
Kernel 4.14.55-84.37.amzn2.x86_64 on an x86_64 (ttyS0)

localhost login:

Par défaut, le couple identifiant/mot de passe est root/root.

C’est bien beau, mais on est ici sur du matos plutôt ancien : une Alpine 3.8 et le noyau 4.14 datent tout-deux de 2018. Commençons alors par créer une machine un peu plus récente.

Compiler son propre noyau

Dans l’objectif de mettre à jour notre VM, compilons d’abord notre propre noyau. Je vais volontairement passer sur les détails de la compilation du noyau (ce n’est pas là le sujet). Cependant, si cela vous intéresse, je vous invite à regarder la rediffusion du live d’Olivier Poncet qui explique très bien comment compiler un noyau.

À ce jour, la dernière version du noyau est la 6.6, nous allons donc la compiler.

apt install -y git build-essential flex bison libncurses5-dev libssl-dev gcc bc libelf-dev pahole
git clone --depth=1 -b linux-6.6.y git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
cd linux-stable 
curl -fsSL -o .config https://raw.githubusercontent.com/firecracker-microvm/firecracker/main/resources/guest_configs/microvm-kernel-ci-x86_64-6.1.config # Configuration du noyau pour firecracker

make menuconfig # Optionnel, permet de modifier la configuration du noyau

yes ''  | make vmlinux -j$(nproc)
cp vmlinux /var/lib/firecracker/6.6-vmlinux # Copie du noyau dans le dossier de firecracker

Avec notre nouveau noyau fraichement compilé, nous allons créer une nouvelle VM à partir de celui-ci.

Création d'une nouvelle VM
curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/boot-source' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "kernel_image_path": "/var/lib/firecracker/6.6-vmlinux",
        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/drives/rootfs' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "drive_id": "rootfs",
        "path_on_host": "/var/lib/firecracker/6.6-vmlinux/hello/hello-rootfs.ext4",
        "is_root_device": true,
        "is_read_only": false
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/actions' \
    -H  'Accept: application/json' \
    -H  'Content-Type: application/json' \
    -d '{
        "action_type": "InstanceStart"
    }'
Starting default runlevel

Welcome to Alpine Linux 3.8
Kernel 6.6.8 on an x86_64 (ttyS0)

localhost login: root
Password:
Welcome to Alpine!

The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org>.

You can setup the system with the command: setup-alpine

You may change this message by editing /etc/motd.

login[637]: root login on 'ttyS0'
localhost:~# uname -a
Linux localhost 6.6.8 #1 SMP PREEMPT_DYNAMIC Wed Dec 27 12:59:19 CET 2023 x86_64 Linux

Notre VM possède maintenant un noyau plus récent 🥳

Passons à présent au système de fichier (rootfs) actuellement basé sur Alpine 3.8.

Créer son propre rootfs sous Alpine

Nous allons créer notre propre rootfs. Pour cela, initions une partition vierge à l’emplacement /tmp/alpine.ext4.

dd if=/dev/zero of=/tmp/alpine.ext4 bs=1G count=16
mkfs.ext4 /tmp/alpine.ext4
mkdir -p /mnt/alpine
mount /tmp/alpine.ext4 /mnt/alpine

Pour le contenu de notre rootfs, je vais m’appuyer sur la version cloud d’Alpine, disponible sous forme de qcow2 sur le site officiel. Je monte donc le fichier qcow2 dans un dossier temporaire (/tmp/cloud-alpine-3.19-qcow2) et copie le contenu dans notre rootfs (sur /mnt/alpine).

curl -fsSL -o /tmp/alpine-3.19.qcow2 https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/cloud/nocloud_alpine-3.19.0-x86_64-bios-tiny-r0.qcow2
apt install qemu-utils
modprobe nbd
qemu-nbd --connect=/dev/nbd0 /tmp/alpine-3.19.qcow2 
mkdir -p /tmp/cloud-alpine-3.19-qcow2
mount /dev/nbd0 /tmp/cloud-alpine-3.19-qcow2

Nous devrions avoir un système de fichier monté dans /tmp/cloud-alpine-3.19-qcow2.

# ls /tmp/cloud-alpine-3.19-qcow2/
bin  boot  dev  etc  home  lib  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

J’utilise alors ce dernier pour copier le contenu dans mon rootfs.

cd /tmp/cloud-alpine-3.19-qcow2/
rsync -av --progress . /mnt/alpine --exclude boot/

Je peux entrer dans mon rootfs via la commande chroot. J’en profite pour faire des mises à jour de mon système, installer des paquets, etc. mais surtout, je peux définir un mot de passe pour l’utilisateur root (car par défaut, il n’y en a pas).

chroot /mnt/alpine/ /bin/ash
echo "nameserver 1.1.1.1" > /etc/resolv.conf
apk update
apk add vim curl git
exit # Pour sortir du chroot

Avertissement

Ne soyez pas surpris si votre prompt ne change pas après la commande chroot. C’est un comportement normal.

Lorsque j’ai terminé de copier le contenu du qcow2 dans mon rootfs, je peux le démonter.

umount /mnt/qcow2_mount
qemu-nbd --disconnect /dev/nbd0

Notre rootfs est maintenant prêt, nous pouvons le copier dans le dossier de Firecracker et créer une nouvelle VM.

umount /mnt/alpine
cp /tmp/alpine.ext4 /var/lib/firecracker/alpine-rootfs.ext4
Création VM Alpine 3.19
curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/boot-source' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "kernel_image_path": "/var/lib/firecracker/6.6-vmlinux",
        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/drives/rootfs' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "drive_id": "rootfs",
        "path_on_host": "/var/lib/firecracker/alpine-rootfs.ext4",
        "is_root_device": true,
        "is_read_only": false
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/actions' \
    -H  'Accept: application/json' \
    -H  'Content-Type: application/json' \
    -d '{
        "action_type": "InstanceStart"
    }'

Welcome to Alpine Linux 3.19
Kernel 6.6.8 on an x86_64 (/dev/ttyS0)

(none) login: root

Nous gardons notre kernel 6.6 et nous avons bien une Alpine 3.19 (la dernière version).

J’aime beaucoup Alpine, mais qu’en est-il du cas où je dispose d’un programme adapté pour Debian plutôt qu’Alpine ?

Créer son propre rootfs sous Debian

Pour les besoins des programmes fonctionnant sur Debian/Ubuntu, je vais installer une Debian Trixie (qui n’est pas officiellement sortie mais après tout : pourquoi pas ?).

Comme pour Alpine, je vais créer notre fichier rootfs et le formater en ext4.

dd if=/dev/zero of=/tmp/debian-trixie.ext4 bs=1G count=16
mkfs.ext4 /tmp/debian-trixie.ext4
mkdir -p /mnt/debian
mount /tmp/debian-trixie.ext4 /mnt/debian

Je vais ensuite utiliser debootstrap pour créer une Debian Trixie dans le répertoire /mnt/debian.

Information

Debootstrap est un outil qui permet d’installer un système Debian de base dans le sous-répertoire d’un autre système déjà existant. Il n’a pas besoin d’un CD d’installation, juste d’un accès à un dépôt Debian. Il peut être installé et exécuté à partir d’un autre système d’exploitation.

source

apt install debootstrap -y
mkdir -p /tmp/debian-debootstrap/
debootstrap --include openssh-server,unzip,git,apt,vim 'trixie' /tmp/debian-debootstrap/  http://deb.debian.org/debian

Une fois la commande terminée, nous aurons une arborescence complète dans le répertoire /tmp/debian-debootstrap, je vais lancer un nouveau chroot pour mettre un mot de passe à l’utilisateur root. Vous pouvez aussi ajouter une clé ssh et modifier l’hostname (par défaut l’hostname est le même que la machine ayant lancé le debootstrap).

# chroot /tmp/debian-debootstrap/
root@firecracker:/# passwd
New password:
Retype new password:
passwd: password updated successfully
root@firecracker:/# vim /etc/hostname /etc/hosts
root@firecracker:/# exit

Astuce

Si vous souhaitez utiliser un DHCP pour de l’adressage dynamique (nous verrons ça plus tard dans l’article), vous pouvez ajouter les lignes suivantes dans le fichier /etc/network/interfaces du chroot :

allow-hotplug eth0
iface eth0 inet dhcp

Il ne reste qu’à le copier dans /mnt/debian (qui correspond à notre fichier /tmp/debian-trixie.ext4).

cd /tmp/debian-debootstrap
rsync -av --progress . /mnt/debian --exclude boot/
umount /mnt/debian
cp /tmp/debian-trixie.ext4 /var/lib/firecracker/debian-rootfs.ext4

Essayons tout de suite notre nouveau rootfs sous Debian Trixie :

Création d'une VM Debian Trixie
curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/boot-source' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "kernel_image_path": "/var/lib/firecracker/6.6-vmlinux",
        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/drives/rootfs' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "drive_id": "rootfs",
        "path_on_host": "/var/lib/firecracker/debian-rootfs.ext4",
        "is_root_device": true,
        "is_read_only": false
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/actions' \
    -H  'Accept: application/json' \
    -H  'Content-Type: application/json' \
    -d '{
        "action_type": "InstanceStart"
    }'

Maintenant, je peux cloner un petit dépôt git et lancer un programme de test.

Internet ? Comment j’ai pu oublier ça ? J’ai besoin d’internet ou d’un accès au réseau pour communiquer avec d’autres machines, pour faire des mises à jour, pour installer des paquets, etc.

Créer un réseau NAT pour nos VM

La première méthode pour que nos machines aient un accès au réseau est de créer un réseau isolé et de faire du NAT.

Cette méthode s’avère efficace car nous n’avons pas à gérer d’adresses sur notre réseau actuel. Aussi, elle est plutôt facile à configurer.

Il est obligatoire de passer par une interface TUN/TAP pour relier notre VM à un réseau et d’activer le routage des paquets sur notre machine hôte.

ip tuntap add tap0 mode tap
ip addr add 172.16.0.1/24 dev tap0
ip link set tap0 up
sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"

Nous obtenons une interface réseau tap0 avec l’adresse 172.16.0.1. Nous créons les règles IPTables pour faire du NAT sur l’interface ens18:

sudo iptables -t nat -A POSTROUTING -o ens18 -j MASQUERADE
sudo iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i tap0 -o ens18 -j ACCEPT

Avertissement

Dans mon cas, j’utilise l’interface ens18 pour accéder à ma passerelle. Pensez à changer cette interface dans les commandes si vous utilisez eth0 ou autre.

Nous pouvons d’ores et déjà tester notre configuration en créant une VM.

curl --unix-socket /tmp/firecracker.socket -i \
  -X PUT 'http://localhost/network-interfaces/eth0' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
      "iface_id": "eth0",
      "guest_mac": "AA:FC:00:00:00:01",
      "host_dev_name": "tap0"
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/boot-source' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "kernel_image_path": "/var/lib/firecracker/6.6-vmlinux",
        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/drives/rootfs' \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d '{
        "drive_id": "rootfs",
        "path_on_host": "/var/lib/firecracker/debian-rootfs.ext4",
        "is_root_device": true,
        "is_read_only": false
    }'

curl --unix-socket /tmp/firecracker.socket -i \
    -X PUT 'http://localhost/actions' \
    -H  'Accept: application/json' \
    -H  'Content-Type: application/json' \
    -d '{
        "action_type": "InstanceStart"
    }'

Dès que la VM est démarrée, nous pouvons nous y connecter et configurer une adresse IP statique (car nous n’avons pas de serveur DHCP).

ip addr add 172.16.0.2/24 dev eth0
ip link set eth0 up
ip route add default via 172.16.0.1 dev eth0

Une fois l’IP configuré, je peux faire mes mises à jour et accéder à internet.

Ajouter un DHCP

Mais pour éviter cette étape de configuration de l’adresse IP sur l’instance virtuelle, nous pouvons mettre en place un serveur DHCP sur notre machine hôte qui répondra au requêtes DHCP sur l’interface tap0.

J’utilise dnsmasq comme serveur DHCP sur ma machine hôte. Pour cela, j’ajoute le paquet dnsmasq et je créé un fichier /etc/dnsmasq.d/firecracker.conf pour seulement activer le serveur sur l’interface tap0.

apt install dnsmasq
cat <<EOF > /etc/dnsmasq.d/firecracker.conf
dhcp-range=172.16.0.50,172.16.0.150,12h
interface=tap0
EOF
systemctl restart dnsmasq

Avertissement

Attention de ne pas activer le DHCP sur l’interface de votre LAN, la présence d’un second DHCP pourrait avoir des répercussions sur votre réseau.

Je rédémarre la machine virtuelle et lance la commande dhclient -v pour obtenir automatiquement une adresse provenant de notre DHCP.

# dhclient -v (dans la VM)
Internet Systems Consortium DHCP Client 4.4.3-P1
Copyright 2004-2022 Internet Systems Consortium.
All rights reserved.
For info, please visit https://www.isc.org/software/dhcp/

Listening on LPF/eth0/aa:fc:00:00:00:01
Sending on   LPF/eth0/aa:fc:00:00:00:01
Sending on   Socket/fallback
DHCPDISCOVER on eth0 to 255.255.255.255 port 67 interval 6
DHCPDISCOVER on eth0 to 255.255.255.255 port 67 interval 15
DHCPDISCOVER on eth0 to 255.255.255.255 port 67 interval 16
DHCPOFFER of 172.16.0.96 from 172.16.0.1
DHCPREQUEST for 172.16.0.96 on eth0 to 255.255.255.255 port 67
DHCPACK of 172.16.0.96 from 172.16.0.1
bound to 172.16.0.96 -- renewal in 18163 seconds.
root@microvm-debian-trixie:~# ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=63 time=5.59 ms

L’idéal serait de créer le fichier /etc/network/interfaces dans le rootfs pour que la configuration soit persistante et que la machine demande automatiquement une adresse IP au démarrage.

#/etc/network/interfaces.d/eth0
auto eth0
iface eth0 inet dhcp

Parfait, non ? Mais il y reste un problème : si je lance une deuxième machine virtuelle, comment faire pour qu’elle puisse communiquer avec notre première machine virtuelle ? Surtout qu’une interface TUN/TAP ne peut être utilisée que par une seule VM à la fois.

La réponse : créer un bridge pour connecter plusieurs interfaces TUN/TAP à un même réseau.

Créer un bridge

Comme dit précédemment, une interface TUN/TAP ne peut être utilisée que par une seule machine virtuelle à la fois. Nous devrons donc créer une interface TUN/TAP par instance. Notre objectif est alors de créer un bridge pour connecter toutes ces interfaces et ainsi créer un réseau de plusieurs machines virtuelles.

Commençons par supprimer l’interface tap0 et les règles IPTables pour repartir sur une base saine.

ip link del tap0
iptables -F # Suppression des règles iptables

J’ajoute un bridge nommé br0 et lui attribue une adresse IP. Celui-ci sera notre interface réseau principale à laquelle nous connecterons toutes nos interfaces TUN/TAP.

J’en profite aussi pour mettre en place le routage des paquets ainsi que les règles IPTables pour faire du NAT sur l’interface ens18 (indispensable pour accéder à internet).

ip link add name br0 type bridge
ip addr add 172.16.0.1/24 dev br0
ip link set dev br0 up
sysctl -w net.ipv4.ip_forward=1
iptables --table nat --append POSTROUTING --out-interface ens18 -j MASQUERADE
iptables --insert FORWARD --in-interface br0 -j ACCEPT

Ainsi, nous devrions avoir notre bridge br0 avec l’adresse 172.16.0.1/24.

Cette étape terminée, nous pouvons créer nos interfaces tapX et les ajouter au bridge.

# Interface pour VM-1
ip tuntap add dev tap0 mode tap
brctl addif br0 tap0
ip link set dev tap0 up

# Interface pour VM-2
ip tuntap add dev tap1 mode tap
brctl addif br0 tap1
ip link set dev tap1 up

J’ai également modifié le fichier /etc/dnsmasq.d/firecracker.conf pour que le DHCP soit disponible sur toutes les interfaces excepté ens18 (mon LAN).

dhcp-range=172.16.0.50,172.16.0.150,12h
except-interface=ens18

Je parle de créer une deuxième VM, mais je ne vous ai pas montré comment faire. Pour cela, nous avons besoin d’un second fichier rootfs (et ça tombe bien, nous en avons un premier sous Debian, et un second sous Alpine).

Créer une deuxième VM

Si on résume ce que nous avons fait jusqu’à présent :

  • Démarrer Firecracker avec un socket dans /tmp/firecracker.socket
  • Créer un réseau NAT avec un bridge nommé br0
  • Créer une interface tap0 et l’ajouter au bridge br0
  • Configurer Firecracker pour utiliser l’interface tap0
  • Configurer Firecracker pour utiliser le rootfs debian-rootfs.ext4 et le noyau 6.6-vmlinux
  • Démarrer la VM

Une fois la VM démarrée, si je relance des requêtes curl pour initier une seconde VM, je vais avoir une erreur puisque le socket ne permet pas de créer plusieurs VM.

La solution est d’avoir autant de processus Firecracker que de VM (et donc d’interface réseau tapX).

Mais comme lancer plusieurs fois les mêmes commandes est fastidieux, j’ai créé un script tout simple pour automatiser ça.

./run_vm.sh
#!/bin/bash
###########################################################################################
# Author: Quentin JOLY                                                                    #
# Run a Firecracker VM with a rootfs, kernel and network interface                        #
# Usage: ./run_vm.sh VM_ID FILE_ROOTFS KERNEL_PATH INTERFACE                              #
# Example: ./run_vm.sh 01 debian-rootfs.ext4 6.6-vmlinux tap0                             #
# VM_ID must be a number between 01 and 99                                                #
# FILE_ROOTFS and KERNEL_PATH has to be in "/var/lib/firecracker/" or absolute paths      #
# INTERFACE is optional, default value is "tap0"                                          #
#                                                                                         #
# LICENSE: MIT License                                                                    #
###########################################################################################

set -euo pipefail

make_curl_request() {
  local unix_socket="$1"
  local base_path="$2"
  local data="$3"
  local url="http://localhost/$base_path"

  curl --unix-socket "$unix_socket" -f -i \
    -X PUT "$url" \
    -H 'Accept: application/json' \
    -H 'Content-Type: application/json' \
    -d "$data"
}

check_command_installed() {
  local cmd="$1"
  
  if command -v "$cmd" &>/dev/null; then
    echo "$cmd is installed."
  else
    echo "$cmd is not installed."
  fi
}

check_valid_path() {
  local path="$1"

  # Check if the path is an absolute path
  if [[ "$path" != /* ]]; then
    path="/var/lib/firecracker/$path"
  fi

  # Check if the path exists
  if [ ! -e "$path" ]; then
    echo "Error: Path '$path' does not exist."
    exit 1
  fi
}

if [ "$#" -ne 3 ]; then
  echo "Usage: $0 VM_ID FILE_ROOTFS KERNEL_PATH [INTERFACE]"
  echo "Example: $0 01 debian-rootfs.ext4 6.6-vmlinux [tap0]"
  exit 1
fi

VM_ID=$1
ROOTFS_PATH=$2
KERNEL_PATH=$3
NETWORK_INTERFACE="${4:-tap0}"

echo "NETWORK_INTERFACE is set to: $NETWORK_INTERFACE"

if ! [[ "$1" =~ ^[0-9]{1,2}$ && "$1" -ge 1 && "$1" -le 99 ]]; then
  echo "Error: VM_ID must be a number between 01 and 99"
  exit 1
fi

check_valid_path "$ROOTFS_PATH"
check_valid_path "$KERNEL_PATH"
check_command_installed firecracker
check_command_installed tmux

MAC_ADDRESS="AA:FC:00:00:00:$VM_ID"
TMUX_SESSION_NAME="firecracker-$VM_ID"
FIRECRACKER_SOCKET="/tmp/firecracker-$VM_ID.socket"

# Kill tmux session if exists
if tmux has-session -t "$TMUX_SESSION_NAME" 2>/dev/null; then
  echo "Tmux session '$TMUX_SESSION_NAME' already exists, killing it..."
  tmux kill-session -t "$TMUX_SESSION_NAME"
  sleep 2
fi

# Kill socket if exists
if [ -e "$FIRECRACKER_SOCKET" ]; then
  echo "Socket already exists, deleting it..."
  pkill -f "$FIRECRACKER_SOCKET" || echo ""
  rm "$FIRECRACKER_SOCKET"
  sleep 2
fi

tmux new-session -d -s "$TMUX_SESSION_NAME" "cd /var/lib/firecracker && firecracker --api-sock $FIRECRACKER_SOCKET"

echo "Configure network-interfaces"
make_curl_request "$FIRECRACKER_SOCKET" "network-interfaces/eth0" '{ "iface_id": "eth0", "guest_mac": "'"$MAC_ADDRESS"'", "host_dev_name": "'"$NETWORK_INTERFACE"'" }'

echo "Configure kernel path"
make_curl_request "$FIRECRACKER_SOCKET" "boot-source" '{"kernel_image_path": "'"$KERNEL_PATH"'", "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"}'

echo "Configure rootfs path"
make_curl_request "$FIRECRACKER_SOCKET" "drives/rootfs" '{"drive_id": "rootfs", "path_on_host": "'"$ROOTFS_PATH"'", "is_root_device": true, "is_read_only": false}'

echo "Start the VM"
make_curl_request "$FIRECRACKER_SOCKET" "actions" '{"action_type": "InstanceStart"}'

echo "You can obtain an interactive session with the command: tmux attach -t $TMUX_SESSION_NAME"

Ce script permet de créer une VM en prenant pour paramètres :

  • un identifiant (entre 01 et 99)
  • un fichier rootfs (qui doit être dans /var/lib/firecracker ou un chemin absolu)
  • un fichier kernel (qui doit être dans /var/lib/firecracker ou un chemin absolu)
  • une interface réseau (optionnel, par défaut tap0)

On peut donc lancer une première VM avec la commande suivante :

./run_vm.sh 01 debian-rootfs.ext4 6.6-vmlinux tap0

Et une seconde VM avec la commande suivante (via un autre rootfs) :

./run_vm.sh 02 alpine-rootfs.ext4 6.6-vmlinux tap1

Les seules dépendances sont firecracker et tmux. Le script va créer un socket dans /tmp/firecracker-VM_ID.socket et une session tmux nommée firecracker-VM_ID. Vous pouvez vous connecter à la session tmux avec la commande tmux attach -t firecracker-VM_ID.

L’adresse MAC est générée automatiquement en fonction de l’identifiant de la VM. La VM avec l’ID 01 aura l’adresse AA:FC:00:00:00:01.

Information

Le script ne s’occupe pas de créer l’interface réseau TUN/TAP, il faut donc la créer manuellement avant de lancer le script.

TAP_INTERFACE=tap1
BRIDGE_INTERFACE=br0
ip tuntap add dev $TAP_INTERFACE mode tap
brctl addif $BRIDGE_INTERFACE $TAP_INTERFACE
ip link set dev $TAP_INTERFACE up

Bon d’accord, ce n’est pas super propre comme solution, mais ça m’évite de taper 10 fois les mêmes commandes dans mon environnement de test et je n’ai pas (encore) besoin d’une solution tierce pour gérer mes VMs.

(Optionnel) VMs dans notre LAN

Si vous préférez que vos machines virtuelles soient directement accessibles depuis votre LAN sans passer par du NAT, vous pouvez suivre la procédure suivante:

Créons une nouvelle interface bridge nommée vmbr0, celle-ci sera reliée à mon interface ens18 (qui est connectée à mon LAN). L’adresse IP de ma machine hôte sera configurée sur vmbr0 et non plus sur ens18.

Pour cela, modifions le contenu du fichier /etc/network/interfaces pour y ajouter une interface vmbr0 et supprimer l’adressage automatique sur ens18.

allow-hotplug ens18
#iface ens18 inet dhcp <----- commenter ça 
iface ens18 inet manual # <----- ajouter cette ligne

# Créer une interface bridge nommée vmbr0
auto vmbr0
iface vmbr0 inet static
        address 192.168.1.35/24 # Ancienne IP de ens18
        gateway 192.168.1.1
        bridge-ports ens18
        bridge-stp off
        bridge-fd 0

Redémarrons la machine hôte pour que les changements soient pris en compte (ce qui implique aussi la suppression des interfaces TUN/TAP et des règles IPTables). Ensuite, créons une interface tap0 et ajoutons la au bridge vmbr0.

TAP_INTERFACE=tap1
BRIDGE_INTERFACE=vmbr0
ip tuntap add dev $TAP_INTERFACE mode tap
brctl addif $BRIDGE_INTERFACE $TAP_INTERFACE
ip link set dev $TAP_INTERFACE up

Désormais, nous pouvons créer une nouvelle VM via le script run_vm.sh dont l’interface réseau sera tap1.

./run_vm.sh 01 debian-rootfs.ext4 6.6-vmlinux tap1

La VM aura une adresse IP dans le même réseau que la machine hôte et sera accessible depuis le même LAN.

Bridge sur LAN

Exposer le socket en TCP

Lorsque nous pilotons Firecracker, nous utilisons un socket UNIX. Ce socket est opérationnel uniquement depuis la machine hôte. Si nous souhaitons contrôler Firecracker depuis une autre machine, nous devons exposer ce socket en TCP.

Pour cela, nous allons utiliser socat pour créer un tunnel TCP vers notre socket UNIX.

apt install socat
socat TCP-LISTEN:8080,reuseaddr,fork UNIX-CONNECT:/tmp/firecracker-01.socket

Nous pouvons à présent piloter notre VM depuis une autre machine (et non plus depuis la machine hôte).

# sur mon laptop
➜  ~ curl 192.168.1.35:8080
{"id":"anonymous-instance","state":"Running","vmm_version":"1.6.0","app_name":"Firecracker"}

Conclusion

Firecracker est un outil très intéressant pour créer de nombreuses machines virtuelles légères et en seulement quelques millisecondes. Je l’imagine bien utilisé dans un environnement de CI/CD pour créer des environnements de tests, de déploiement ou encore pour créer des environnements de développement à la demande. Pourquoi ne pas l’utiliser comme atelier de développement durant un hackathon ?

Il reste encore de nombreuses choses à voir avec Firecracker. Je n’ai, par exemple, pas parlé de jailer (un composant pour rajouter une couche de sécurité), de la gestion des snapshots, ou encore de l’attribution de ressources (CPU, RAM, etc.). Je vous invite donc à lire la documentation officielle pour en savoir plus.

Difficile de prévoir l’usage que je vais en faire, mais j’espère pouvoir vous faire un retour d’ici à quelques mois sur des usages plus concrets.

Information

  • J’avais prévu de vous parler d’Ignite, un outil de Weavework pour manipuler ses VMs sur Firecracker. Mais celui-ci a été abandonné au profit de Flintlock sur lequel je n’ai pas encore eu le temps de me pencher.

  • Il est possible d’utiliser Firecracker sans passer par les sockets UNIX, j’ai volontairement choisi de ne pas en parler dans cet article car je trouve que ça ruine l’intérêt même de Firecracker. Mais si vous souhaitez en savoir plus, je vous invite une nouvelle fois à consulter la documentation officielle.