El Problema del Aislamiento
Empecemos con por qué importa. En una plataforma serverless, se está corriendo código no confiable de diferentes tenants en hardware compartido. El mecanismo de aislamiento debe garantizar que:
- Tenant A no puede leer la memoria de Tenant B
- Tenant A no puede acceder al filesystem de Tenant B
- Tenant A no puede consumir más CPU/memoria de lo asignado
- Un bug o exploit en el código de Tenant A no puede comprometer el host
Los containers proveen propiedad 2 (vía mount namespaces), propiedad 3 (vía cgroups), y parcialmente propiedad 1 (vía PID/user namespaces). Pero fallan críticamente en la propiedad 4: todos los containers comparten el kernel del host, y el kernel Linux tiene una superficie de ataque masiva.
Firecracker: Un VMM Escrito en Rust
Firecracker fue construido por AWS específicamente para Lambda y Fargate. Es un Virtual Machine Monitor que usa KVM (el hypervisor built-in de Linux) para crear máquinas virtuales livianas. Cada VM obtiene su propio kernel Linux, su propio espacio de memoria, y sus propios dispositivos virtuales.
┌─────────────────────────────────────────┐
│ HOST OS │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │Firecracker│ │Firecracker│ │ ... │ │
│ │ VMM │ │ VMM │ │ │ │
│ │ (Rust) │ │ (Rust) │ │ │ │
│ ├──────────┤ ├──────────┤ ├────────┤ │
│ │ KVM │ │ KVM │ │ KVM │ │
│ ├──────────┤ ├──────────┤ ├────────┤ │
│ │Guest │ │Guest │ │Guest │ │
│ │Linux │ │Linux │ │Linux │ │
│ │Kernel │ │Kernel │ │Kernel │ │
│ ├──────────┤ ├──────────┤ ├────────┤ │
│ │Function A│ │Function B│ │Func C │ │
│ └──────────┘ └──────────┘ └────────┘ │
└─────────────────────────────────────────┘
Lo que hace "micro" a Firecracker:
Modelo de dispositivos mínimo: Los VMMs tradicionales (QEMU) emulan cientos de dispositivos, controladores USB, tarjetas de sonido, buses ISA legacy. Firecracker emula exactamente 5 tipos de dispositivo: virtio-net, virtio-block, consola serial, un controlador de teclado i8042 mínimo (para shutdown), y un reloj KVM. Eso es todo. Menos dispositivos = menor superficie de ataque = menos código que auditar.
Sin BIOS, sin UEFI: Firecracker carga el kernel Linux directo en la memoria del guest y configura los parámetros de boot manualmente. Esto saltea toda la secuencia de boot de firmware.
Esto es lo que Firecracker hace al arrancar:
// Secuencia de boot simplificada de Firecracker
fn boot_microvm(config: &VmConfig) -> Result<Vm> {
// 1. Abrir /dev/kvm y crear una VM
let kvm_fd = open("/dev/kvm", O_RDWR)?;
let vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0)?;
// 2. Configurar memoria del guest (típicamente 128MB-2GB)
let guest_mem = GuestMemory::new(config.mem_size_mib)?;
ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &guest_mem.region())?;
// 3. Cargar el kernel directo en memoria del guest
let kernel = load_kernel(&config.kernel_path, &guest_mem)?;
// 4. Configurar parámetros de boot
let boot_params = configure_boot_params(kernel.entry_point, &config)?;
// 5. Crear vCPUs y configurar registros
let vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0)?;
set_initial_registers(vcpu_fd, &boot_params)?;
// 6. Configurar dispositivos virtio (net, block)
let mmio_bus = setup_virtio_devices(&config, &guest_mem)?;
// 7. Entrar al loop de VM (ejecuta código del guest vía KVM_RUN)
Ok(Vm { vm_fd, vcpu_fd, guest_mem, mmio_bus })
}
Tiempos de Boot: Números Reales
Los tiempos de boot de Firecracker medidos en una instancia m5.metal (bare-metal, sin virtualización anidada):
Fase Tiempo
─────────────────────────────────────
ioctl KVM_CREATE_VM: 0.2ms
Setup de memoria (256MB): 1.8ms
Carga del kernel (5.10): 3.1ms
Setup vCPU + init registros: 0.4ms
Setup dispositivos virtio: 0.8ms
─────────────────────────────────────
VMM listo para KVM_RUN: 6.3ms
Boot kernel guest hasta init: ~125ms
Init Alpine Linux hasta shell: ~30ms
─────────────────────────────────────
Total hasta userspace corriendo: ~160ms
160ms de la nada a un userspace Linux corriendo. Es notable, pero para funciones serverless que necesitan responder en <200ms, ese tiempo de boot sigue siendo una tajada significativa del presupuesto de latencia. Aquí es donde entran los snapshots.
Snapshots de Firecracker
Firecracker puede tomar un snapshot del estado completo de una VM corriendo y restaurarlo en ~5ms:
# Crear snapshot de VM corriendo
curl --unix-socket /tmp/firecracker.socket -X PUT \
http://localhost/snapshot/create \
-d '{
"snapshot_type": "Full",
"snapshot_path": "/snapshots/function-v1.snap",
"mem_file_path": "/snapshots/function-v1.mem"
}'
# Restaurar VM desde snapshot (~5ms)
curl --unix-socket /tmp/firecracker.socket -X PUT \
http://localhost/snapshot/load \
-d '{
"snapshot_path": "/snapshots/function-v1.snap",
"mem_file_path": "/snapshots/function-v1.mem"
}'
AWS Lambda usa exactamente este mecanismo: cuando despliega una función, Lambda la bootea una vez, snapshottea el estado inicializado, y restaura desde snapshot en cold starts subsiguientes.
Unikraft: Library Operating Systems
Los unikernels toman un approach fundamentalmente diferente. En vez de correr un kernel Linux completo con miles de syscalls, drivers, y subsistemas, un unikernel incluye solo los componentes del OS que la aplicación realmente usa.
VM Tradicional:
Aplicación → libc → Kernel Linux (25M+ líneas de código) → Hardware
Unikernel Unikraft:
Aplicación → lib/nolibc → lib/vfscore → lib/9pfs → driver KVM
→ lib/uknetdev → driver virtio-net
→ lib/uksched → scheduler cooperativo
(total: ~100K líneas de código)
# Buildear el unikernel
kraft build --target firecracker/x86_64 --log-level debug
# Correr con Firecracker
kraft run --target firecracker/x86_64 \
--memory 64 \
--network bridge:kraft0 \
-- /my-http-server --port 8080
Comparación Cara a Cara
Todas las mediciones en el mismo hardware (c5.metal, bare-metal):
Firecracker Unikraft Docker
(guest Alpine) (servidor HTTP) (Alpine)
────────────────────────────────────────────────────────────────────────────
Boot a primer HTTP response: 160ms 8ms ~800ms*
Memoria (idle): 32MB 2MB ~15MB
Tamaño de imagen: ~50MB ~1.2MB ~8MB
Syscalls soportadas: ~400 ~160 ~400
Boundary de seguridad: KVM + VMM KVM Namespace
Experiencia de debugging: Buena (gdb) Dolorosa Excelente
Compatibilidad ecosistema: Binarios Linux Recompilación Todo
────────────────────────────────────────────────────────────────────────────
Ese es 8ms de boot time de Unikraft no son un typo. Un unikernel no tiene BIOS, no tiene bootloader, no carga módulos del kernel, no tiene systemd, no tiene proceso init. Va directo de "VM arrancada" a "main() de la aplicación corriendo."
Pero observar la fila de compatibilidad del ecosistema. Firecracker corre cualquier binario Linux, Node.js, Python, Go, Java, cualquier cosa con un build para Linux. Unikraft requiere recompilación contra su libc (basada en musl) y puede no soportar todas las syscalls que tu aplicación usa. En un caso, se pasaron dos días tratando de hacer andar una app de Flask en Python sobre Unikraft antes de descubrir que usaba sendfile() que no estaba implementada en el stack de red de Unikraft en ese momento.
Elecciones Prácticas para Producción
Para aislamiento de funciones serverless: Firecracker. La compatibilidad con el ecosistema vale la memoria extra y el tiempo de boot. Con snapshot-restore, los cold starts son ~5ms de todas formas.
Para edge computing con workloads conocidos y controlados: Unikraft. Cuando el comportamiento de la aplicación está bien caracterizado (servir archivos estáticos, correr módulos WASM, proxear HTTP), un unikernel de 2MB que bootea en 8ms es irresistible. El overhead operativo es más alto porque debuggear es más complico, pero para workloads bien caracterizados, vale la pena.
Para todo lo demás: Firecracker con Kata Containers. Esto te da la ergonomía de containers (docker build, docker push, CI/CD estándar) con aislamiento de VM. El overhead de boot time es ~150ms, que es aceptable para la mayoría de los workloads que no son funciones serverless críticas en latencia.
La verdad honesta es que los unikernels vienen siendo "el futuro" hace una década, y todavía no son mainstream. Firecracker triunfó donde los unikernels no porque hizo un tradeoff pragmático: aceptar un kernel Linux completo con su overhead y superficie de ataque, pero hacerlo lo suficientemente rápido y aislado para que no importe. A veces "suficientemente bueno", despachado y probado en batalla, supera a perfecto pero todavía madurando.