
MicroVMs partout : l'infrastructure de build qui tourne sur Mac comme en prod
Pourquoi votre build système ne devrait pas dépendre d'une machine distante
C'est un scénario que toute équipe SaaS connaît : votre infrastructure de production tourne sur des microVMs isolées, performantes, sécurisées — mais pour les faire tourner en local, il faut un SSH vers une machine partagée, un pipeline de build à distance, et des logs qui arrivent avec 500ms de latence.
Encore, la plateforme d'infrastructure automatisée, a vécu ça pendant quatre ans. Leur système de build utilisait Firecracker — l'hyperviseur open-source d'AWS qui fait tourner Lambda et Fargate — mais Firecracker nécessite /dev/kvm, ce qu'aucun Mac ne fournit. Résultat : chaque ingénieur devait passer par une machine Linux partagée, avec tout ce que ça implique de friction.
En 2026, ils ont résolu le problème. Leur solution s'appelle crackling : une API unifiée qui fait tourner les mêmes images OCI sur Firecracker (Linux) comme sur Apple Virtualization.framework (macOS). Voici ce qu'ils ont appris.
Le problème : quatre ans de build à distance
Le setup initial d'Encore fonctionnait, mais au prix d'une complexité qui allait à l'encontre de leur promesse : une infrastructure qui suit le code, pas une pile de YAML.
Chaque nouvel ingénieur exécutait un script qui :
- SSH sur une machine partagée en tant que root
- Récupérait sa clé publique depuis GitHub
- Le créait comme utilisateur dans les groupes
kvmetdocker - Copiait les images et liait le binaire Firecracker
Le déploiement d'un changement passait par un second script, qui lisait le username et le port depuis une config CUE, dans un fichier gitignoré par ingénieur — parce qu'ils partageaient tous le même host et devaient éviter les collisions.
Le pipeline d'images : un enchaînement fragile
Le vrai problème, c'était la gestion des images. Firecracker boote un block device, mais Docker produit des layers. Aucun outil existant ne convertissait les layers Docker en block device bootable par Firecracker.
# Encore's original deploy pipeline (simplified)
docker save -o "$imagesdir/$name.tar" "$docker_image"
tar -C "$layersdir" -xf "$imagesdir/$name.tar"
tar -C "$dst/" -xf "$imagesdir/$name.tar" manifest.json
rsync -azP $layersdir ${username}@builder:~/images/
ssh ${username}@builder -- \
"bash -l -s squash_layers" < $scriptpath
La dernière ligne pipe une fonction shell dans un login shell distant. La logique de squash_layers devait :
- Réextraire chaque layer dans l'ordre du manifest
- Supprimer les marqueurs
.wh..wh..opq(whiteout) - Écrire un
/etc/resolv.confhardcodé - Extraire les variables d'environnement du Docker config
- Appeler
mksquashfspour produire un block device bootable
Et à chaque redémarrage, il fallait un troisième SSH pour lancer le conteneur :
docker run --privileged \
-v ~/socks:/var/lib/buildsvc/socks:rw \
-v ~/images:/usr/lib/buildsvc/images:ro \
--device /dev/kvm --device /dev/net/tun \
-p $port:9060 --name "${username}-builder" -d -t buildsvc-tester
Firecracker tournait à l'intérieur de Docker, donc il fallait passer --privileged, /dev/kvm et /dev/net/tun. Et comme Docker n'a pas de bridge host, ils ont dû en construire un — dans un script shell, avant que le service de build démarre.
Le coût caché du remote-first
Ce setup a tenu de 2022 à 2026. Mais le coût était bien réel :
- Débuggage impossible : un breakpoint local ne se déclenche jamais sur une machine distante
- Logs lents : tailer un fichier par SSH, c'est 500ms de latence à chaque ligne
- Profiling compliqué : copier un profileur sur la box distante avant de pouvoir l'utiliser
- Boucle de feedback lente : chaque changement guest-side passait par
docker save,rsync, extraction,mksquashfs— sur le même host que les autres ingénieurs
Le résultat ? Vous réfléchissiez à deux fois avant d'essayer quelque chose de spéculatif. Et dans un SaaS, la spéculation, c'est souvent là que naissent les meilleures optimisations.
La solution : crackling, une API unique pour deux hyperviseurs
L'équipe d'Encore a regardé les alternatives existantes : Apple's container (1.0 en juin 2026), Lima, Tart, podman via libkrun. Mais aucune ne span les deux hosts. Adopter l'une d'elles aurait créé un second chemin de build, différent, uniquement pour les laptops.
Ils ont donc construit crackling : un daemon et CLI qui boote des images OCI comme VMs Linux légères sur les deux plateformes.
Architecture
┌──────────────┐ gRPC ┌──────────────┐
│ crackling CLI │◄───────────►│ cracklingd │
└──────────────┘ └──────┬───────┘
│
compile-time backend
┌──────────┴──────────┐
▼ ▼
┌──────────┐ ┌──────────────────┐
│Firecracker│ │Virtualization.fw │
│ (Linux) │ │ (macOS) │
└─────┬────┘ └────────┬─────────┘
│ vsock │
└──────┬─────────────┘
▼
┌──────────────┐
│ Guest Agent │
│ (musl/rust) │
└──────────────┘
Le coeur de crackling est un trait Rust MachineBackend que chaque backend implémente : start, shutdown, pause, resume, snapshot, wait, dispose, connect_vsock.
pub enum Feature {
Snapshot,
SnapshotRestore,
Mmds, VirtioFs, Rosetta,
TapNetwork, // Firecracker only
NatNetwork, // VZ only
MemoryBalloon, Entropy, Vsock,
Adoption, // Firecracker only
}
Chaque backend rapporte ce qu'il supporte avant qu'aucune machine n'existe, permettant au daemon d'ajuster la spec avant create.
Spécificités Linux
Sur Linux, chaque VM est un processus firecracker enfant, piloté via son API REST sur un socket Unix. Ces processus peuvent survivre au daemon. Un scope systemd transitoire empêche le service manager de les récolter, et le PID est persisté avec son start time pour éviter les collisions.
Spécificités macOS
Le backend VZ a imposé une contrainte stricte : VZVirtualMachine est !Send + !Sync. Toutes les opérations doivent s'exécuter sur la queue de dispatch qui a créé la VM. La solution : une queue DispatchQueue globale, avec un handle async qui dispatche des closures contenant uniquement des données Send.
reactor().queue.exec_async(move || {
match build_configuration(&cfg) {
Ok(vm_cfg) => {
let vm = unsafe {
VZVirtualMachine::initWithConfiguration_queue(
VZVirtualMachine::alloc(), &vm_cfg, &reactor().queue)
};
// stocker le VM dans le registre
}
Err(e) => { /* reporter l'erreur */ }
}
});
Booter un kernel Linux sans Linux : le défi du pipeline d'images
C'est là que le bât blesse. Le backend VZ peut créer et contrôler une VM, mais la faire booter nécessite un pipeline d'images qui, normalement, tourne uniquement sur Linux.
Désassemblage des kernels EFI zboot
Les kernels arm64 distribués par les distributions sont généralement des fichiers EFI zboot : un petit exécutable EFI qui wrappe un payload compressé que le firmware décomprimerait au boot. Il n'y a pas de firmware ici, donc il faut désemballer le payload soi-même.
// Detection EFI zboot: "MZ" à offset 0, "zimg" à offset 4
if &bytes[0..2] == b"MZ" && &bytes[4..8] == b"zimg" {
let payload_offset = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
let payload_size = u32::from_le_bytes(bytes[12..16].try_into().unwrap());
// décompresser gzip...
}
Virtualization.framework rejette un kernel compressé avec une erreur générique "internal error". L'équipe a perdu un après-midi à re-signer des binaires avant de réaliser que le problème venait du kernel.
Conversion OCI → initramfs sans root
Générer un rootfs bootable depuis une image OCI nécessite normalement root et un loop mount. Sur macOS, rien de tout ça n'existe.
La solution d'Encore : appliquer les layers OCI entièrement en userspace (Rust), en honorant les entrées whiteout .wh., puis générer un initramfs (archive cpio newc dans un stream gzip) — le tout en pure Rust, sans appeler cpio ou mksquashfs.
// Extraction pure Rust des layers OCI
// Pas de root, pas de loop mount, pas de cpio binaire
// Résultat : un initramfs bootable par Virtualization.framework
Le cache est clé : chaque image unpackée est normalisée une fois, un sentinel .built est écrit, et le résultat est publié par atomic rename. Chaque VM clone le rootfs caché via clonefile (APFS) ou FICLONE (Linux).
L'agent guest : un binaire musl statique
Le guest agent est un binaire statique musl compilé pour aarch64-unknown-linux-musl (sur Mac) et x86_64-unknown-linux-musl (sur Linux). Il écoute sur AF_VSOCK avec un protocole framé : header 8 bytes, puis données brutes.
Le protocole supporte :
exec: stdout/stderr streamésshell: PTY interactifcp: transfert bidirectionnelforward: tunnel vers un port dans le guest
Les modules VSOCK doivent charger exactement la bonne version du kernel — un mismatch et le VM boote mais l'agent ne répond jamais.
Le mur : Apple ne laisse personne snapshotter une VM
Firecracker capture l'état mémoire et device nativement via PUT /snapshot/create. La restauration vérifie l'empreinte host, charge le snapshot en pause, et reprend sans rebooter. C'est ce qui permet de suspendre un sandbox inactif et le reprendre sur un autre host.
Apple Virtualization.framework expose saveMachineStateToURL et validateSaveRestoreSupport. L'implémentation d'Encore a passé la validation — mais l'appel à save a échoué avec VZErrorInternal.
La raison : faire tourner une VM nécessite com.apple.security.virtualization (accessible à tout développeur), mais la sauvegarder nécessite com.apple.private.virtualization, une entitlement qu'Apple n'accorde pas aux applications tierces.
Et le validateur ne vérifie pas la seconde entitlement. Donc le framework dit "oui, c'est supporté", puis échoue avec la même erreur générique qu'un kernel corrompu.
Lesson pour les équipes SaaS : si vous construisez sur Apple Virtualization.framework, ne comptez pas sur les snapshots. Prévoyez un cache de reconstruction, pas une restauration d'état.
Ce qui tourne sur un Mac aujourd'hui
Le résultat final : un ingénieur clone le système de build, le lance sur son laptop, et boote les mêmes images OCI utilisées pour les builds et déploiements de la prod — via le même agent et le même chemin vsock.
$ crackling run --image alpine:3.20
0c1f8f3c-7b21-4a5e-9a10-2b4c6d8e0f11 running alpine:3.20
$ crackling exec 0c1f8f3c-7b21-4a5e-9a10-2b4c6d8e0f11 uname -a
Linux (none) 6.6.142-0-virt ... aarch64 Linux
$ crackling shell 0c1f8f3c-7b21-4a5e-9a10-2b4c6d8e0f11
~ # cat /etc/alpine-release
3.20.10
La machine partagée a disparu. Le docker save pipé dans rsync et le bridge construit à la main dans un conteneur privilégié aussi. Et vous pouvez poser un breakpoint dans le système de build — et l'atteindre.
En résumé : ce que ça change pour un SaaS
| Avant (remote-first) | Après (crackling) |
|---|---|
| Machine partagée, latence SSH | Build local, breakpoints |
| docker save + rsync + mksquashfs | Cache APFS atomic, initramfs en Rust |
| Bridge Docker bricolé à la main | NAT natif VZ, tap Firecracker |
| Pas de snapshot possible | Snapshots Firecracker (Linux) |
| Un seul OS supporté (Linux) | Linux + macOS, même API |
Pour une équipe SaaS qui construit son infrastructure de A à Z, la leçon d'Encore est claire : l'isolation par microVM n'est plus réservée à la production. Avec les bons outils, elle devient aussi accessible sur un MacBook que sur un serveur Linux.
Le code de crackling est open-source. Si vous construisez une plateforme qui a besoin d'isolation forte et de portabilité, c'est une base technique solide — et un rappel que les vrais problèmes d'infrastructure sont rarement là où on les attend.
Pour aller plus loin
- Firecracker MicroVM (GitHub) — l'hyperviseur open-source d'AWS
- Apple Virtualization.framework — la doc officielle
- Encore blog — plus de détails sur crackling et l'infrastructure microVM
- Alpine Linux virt kernel — modules VSOCK pré-packagés


