Créer son cloud de MicroVM à la maison ?
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).
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.
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 bridgebr0
- Configurer Firecracker pour utiliser l’interface
tap0
- Configurer Firecracker pour utiliser le rootfs
debian-rootfs.ext4
et le noyau6.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.
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.