Capítulo 6: Storage y Block Devices

Por: Artiko
firecrackerstorageext4block-devicevirtio-blockinitrd

Capítulo 6: Storage y Block Devices

Firecracker expone almacenamiento al guest mediante VirtIO-Block devices, respaldados por archivos en el sistema de archivos del host (generalmente formato ext4).

Tipos de almacenamiento

graph TD
    A[microVM] --> B[Drive raíz<br/>is_root_device: true]
    A --> C[Drive adicional<br/>is_root_device: false]
    A --> D[initrd<br/>en boot-source]
    
    B --> E[rootfs.ext4]
    C --> F[data.ext4]
    D --> G[initrd.cpio]

Configurar el drive raíz

Ya lo vimos en el capítulo 3. Aquí con todas las opciones disponibles:

API_SOCKET="/tmp/firecracker.socket"

curl -s -X PUT \
  --unix-socket "${API_SOCKET}" \
  --header "Content-Type: application/json" \
  --data '{
    "drive_id": "rootfs",
    "path_on_host": "./rootfs.ext4",
    "is_root_device": true,
    "is_read_only": false,
    "rate_limiter": {
      "bandwidth": {
        "size": 104857600,
        "refill_time": 1000
      },
      "ops": {
        "size": 1000,
        "refill_time": 1000
      }
    }
  }' \
  "http://localhost/drives/rootfs"

Los campos del rate limiter usan token buckets:

En el ejemplo: máximo 100 MiB/s y 1000 IOPS.

Agregar un segundo drive (datos)

# Crear una imagen ext4 vacía de 2 GB
dd if=/dev/zero of=data.ext4 bs=1M count=2048
mkfs.ext4 -F data.ext4

# Configurarlo en la API
curl -s -X PUT \
  --unix-socket "${API_SOCKET}" \
  --header "Content-Type: application/json" \
  --data '{
    "drive_id": "data",
    "path_on_host": "./data.ext4",
    "is_root_device": false,
    "is_read_only": false
  }' \
  "http://localhost/drives/data"

Dentro del guest, el segundo drive aparecerá como /dev/vdb:

# Dentro del guest
mount /dev/vdb /mnt/data
df -h /mnt/data

Los drives se nombran /dev/vda, /dev/vdb, etc. en el guest según el orden de configuración.

Drive de solo lectura

Útil para compartir datos de solo lectura entre múltiples VMs (como imágenes de contenedores):

curl -s -X PUT \
  --unix-socket "${API_SOCKET}" \
  --header "Content-Type: application/json" \
  --data '{
    "drive_id": "shared-data",
    "path_on_host": "./shared.ext4",
    "is_root_device": false,
    "is_read_only": true
  }' \
  "http://localhost/drives/shared-data"

Actualizar drive en caliente

Puedes cambiar el archivo de un drive mientras la VM corre (útil para intercambiar discos):

# Solo se puede actualizar path_on_host y rate_limiter en PATCH
curl -s -X PATCH \
  --unix-socket "${API_SOCKET}" \
  --header "Content-Type: application/json" \
  --data '{
    "drive_id": "data",
    "path_on_host": "./data-v2.ext4"
  }' \
  "http://localhost/drives/data"

Crear un rootfs personalizado

Para producción deberás construir tu propio rootfs. El método más simple usa Docker:

# Extraer un sistema de archivos de una imagen Docker
docker pull alpine:3.19
CONTAINER_ID=$(docker create alpine:3.19)
IMAGE_FILE="alpine-rootfs.ext4"
IMAGE_SIZE_MB=300

# Crear imagen ext4
dd if=/dev/zero of="${IMAGE_FILE}" bs=1M count="${IMAGE_SIZE_MB}"
mkfs.ext4 -F "${IMAGE_FILE}"

# Montar y poblar
mkdir -p /tmp/rootfs-mount
sudo mount "${IMAGE_FILE}" /tmp/rootfs-mount
sudo docker export "${CONTAINER_ID}" | sudo tar -xC /tmp/rootfs-mount
sudo umount /tmp/rootfs-mount

docker rm "${CONTAINER_ID}"

Usar initrd en lugar de rootfs

Para microVMs stateless o de arranque ultrarrápido, puedes usar un initrd (archivo cpio) en lugar de una imagen de disco completa:

# Configurar boot con initrd
curl -s -X PUT \
  --unix-socket "${API_SOCKET}" \
  --header "Content-Type: application/json" \
  --data '{
    "kernel_image_path": "./vmlinux",
    "boot_args": "console=ttyS0 reboot=k panic=1 pci=off",
    "initrd_path": "./initrd.cpio"
  }' \
  "http://localhost/boot-source"

Con initrd no configures is_root_device: true en ningún drive. El initrd es el rootfs.

Ventajas del initrd:

Desventajas:

Crear un initrd mínimo

# Directorio temporal para el initrd
INITRD_DIR=$(mktemp -d)
cd "${INITRD_DIR}"

# Estructura mínima
mkdir -p bin sbin etc proc sys dev

# Copiar busybox estático como /init
cp /usr/bin/busybox bin/busybox
chmod +x bin/busybox

# Crear /init
cat > init << 'EOF'
#!/bin/busybox sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
exec /bin/busybox sh
EOF
chmod +x init

# Empaquetar como cpio
find . | cpio -H newc -o > ~/firecracker-lab/initrd.cpio
cd ~
rm -rf "${INITRD_DIR}"

Referencia