MicroVMFirecrackerApple SiliconInfrastructureSaaSDevOps

MicroVMs partout : l'infrastructure de build qui tourne sur Mac comme en prod

Encore a réécrit sa stack microVM pour unifier Firecracker (Linux) et Apple Silicon (macOS). Architecture, kernel, vsock — retour d'expérience technique pour les équipes SaaS qui veulent une infra locale fidèle à la prod.

21 août 2026
MicroVMs partout : l'infrastructure de build qui tourne sur Mac comme en prod

En bref

  • 1Encore a reconstruit sa stack microVM dans un projet open-source nommé crackling : une API unique qui pilote Firecracker sur Linux et Virtualization.framework sur macOS, avec les mêmes images OCI et le même agent vsock.
  • 2Les obstacles techniques étaient nombreux : désassemblage de kernels EFI zboot en Rust, conversion d'images OCI en initramfs sans outils Linux, et contournement de l'absence de snapshots VZ (Apple réserve la fonction aux apps système via une entitlement privée).
  • 3Le résultat : les ingénieurs d'Encore peuvent exécuter localement la même infrastructure de build que celle de leurs déploiements en production, sans machine partagée ni pipeline rsync fragile.

MicroVM architecture concept — virtualisation légère sur MacBook

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 kvm et docker
  • 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.conf hardcodé
  • Extraire les variables d'environnement du Docker config
  • Appeler mksquashfs pour 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és
  • shell : PTY interactif
  • cp : transfert bidirectionnel
  • forward : 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 SSHBuild local, breakpoints
docker save + rsync + mksquashfsCache APFS atomic, initramfs en Rust
Bridge Docker bricolé à la mainNAT natif VZ, tap Firecracker
Pas de snapshot possibleSnapshots 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

Questions fréquentes

Firecracker est un hyperviseur open-source écrit en Rust par AWS, qui fait tourner Lambda et Fargate. Contrairement à Docker, chaque microVM fournit une isolation mémoire et CPU au niveau du noyau Linux, avec un démarrage sous 125ms. Pour un SaaS, c'est le meilleur compromis entre sécurité (isolation d'une VM) et performance (coût d'un conteneur).

Articles connexes

Vous avez aimé cet article ?