Capítulo 13: Casos de Uso Reales

Por: Artiko
firecrackerserverlessci-cdsandboxcasos-usofaas

Capítulo 13: Casos de Uso Reales

Caso 1: Serverless local (FaaS)

Implementar un sistema básico de funciones-como-servicio usando Firecracker, similar a AWS Lambda pero en tu máquina local.

Arquitectura

graph LR
    A[HTTP Request] --> B[Gateway]
    B --> C{Pool de VMs}
    C -->|VM disponible| D[microVM con handler]
    C -->|Pool vacío| E[Crear VM desde snapshot]
    D -->|Resultado| B
    B -->|Response| A

Preparar snapshot base para funciones Node.js

# 1. Arrancar VM base con Node.js instalado
API_SOCKET="/tmp/fc-base.sock"
firecracker --api-sock "${API_SOCKET}" &

# Configurar y arrancar VM...
# (dentro del guest: instalar Node.js, preparar handler)

# 2. Crear snapshot cuando Node.js está listo
curl -s -X PATCH \
  --unix-socket "${API_SOCKET}" \
  --data '{"state": "Paused"}' \
  "http://localhost/vm"

curl -s -X PUT \
  --unix-socket "${API_SOCKET}" \
  --data '{
    "snapshot_type": "Full",
    "snapshot_path": "./snapshots/nodejs-ready/snapshot",
    "mem_file_path": "./snapshots/nodejs-ready/memory"
  }' \
  "http://localhost/snapshot/create"

Handler de funciones (dentro del guest)

// /opt/handler/server.js — servidor HTTP dentro del guest
const http = require('http');
const { execSync } = require('child_process');

// Leer la función a ejecutar desde MMDS
async function getFunctionCode() {
  const token = execSync(
    'curl -s -X PUT -H "X-metadata-token-ttl-seconds: 60" http://169.254.169.254/latest/api/token'
  ).toString().trim();
  
  const code = execSync(
    `curl -s -H "X-metadata-token: ${token}" http://169.254.169.254/function/code`
  ).toString();
  
  return code;
}

const server = http.createServer(async (req, res) => {
  const code = await getFunctionCode();
  const fn = new Function('event', code);
  const result = await fn(JSON.parse(req.url.slice(1)));
  res.end(JSON.stringify(result));
});

server.listen(3000);

Invocar una función

# Inyectar código de función via MMDS
curl -s -X PUT \
  --unix-socket "${API_SOCKET}" \
  --data '{
    "function": {
      "code": "return { result: event.x * event.y };"
    }
  }' \
  "http://localhost/mmds"

# Llamar la función (desde el host, via red)
curl "http://172.16.0.2:3000/$(echo '{"x":6,"y":7}' | python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))')"

Caso 2: Aislamiento para CI/CD

Cada job de CI corre en una microVM efímera — el pipeline no puede afectar al host ni a otros jobs.

#!/bin/bash
# run-ci-job.sh — ejecutar un job de CI en microVM aislada

JOB_ID="$1"
REPO_URL="$2"
COMMAND="$3"

# Crear rootfs con copy-on-write (cambios del job no afectan la base)
cp --reflink=auto /opt/ci-base.ext4 "/tmp/ci-${JOB_ID}.ext4"

# Montar e inyectar el código del repositorio
MOUNT_DIR="/tmp/ci-mount-${JOB_ID}"
mkdir -p "${MOUNT_DIR}"
sudo mount "/tmp/ci-${JOB_ID}.ext4" "${MOUNT_DIR}"
git clone --depth=1 "${REPO_URL}" "${MOUNT_DIR}/workspace"
sudo umount "${MOUNT_DIR}"

# Crear configuración de la VM
cat > "/tmp/ci-${JOB_ID}-config.json" << EOF
{
  "boot-source": {
    "kernel_image_path": "/opt/ci-kernel/vmlinux",
    "boot_args": "console=ttyS0 reboot=k panic=1 pci=off init=/opt/ci-runner/init.sh"
  },
  "drives": [{
    "drive_id": "rootfs",
    "path_on_host": "/tmp/ci-${JOB_ID}.ext4",
    "is_root_device": true,
    "is_read_only": false
  }],
  "machine-config": {
    "vcpu_count": 2,
    "mem_size_mib": 2048
  }
}
EOF

# Arrancar VM (bloqueante hasta que el job termine)
SOCKET="/tmp/ci-${JOB_ID}.sock"
firecracker --api-sock "${SOCKET}" \
  --config-file "/tmp/ci-${JOB_ID}-config.json"

# Recolectar artefactos y limpiar
sudo mount "/tmp/ci-${JOB_ID}.ext4" "${MOUNT_DIR}"
cp -r "${MOUNT_DIR}/artifacts" "/opt/ci-results/${JOB_ID}/"
sudo umount "${MOUNT_DIR}"
rm -f "/tmp/ci-${JOB_ID}.ext4" "/tmp/ci-${JOB_ID}-config.json"

Caso 3: Sandbox para ejecución de código no confiable

Ejecutar código arbitrario de usuarios de forma segura:

# sandbox.sh — ejecutar código del usuario en VM aislada
USER_CODE="$1"  # código a ejecutar
TIMEOUT=30       # máximo 30 segundos

# Inyectar código via MMDS después del arranque
run_in_sandbox() {
  local SOCKET="/tmp/sandbox-$$.sock"
  
  # Arrancar desde snapshot pre-calentado
  firecracker --api-sock "${SOCKET}" &
  FC_PID=$!
  
  sleep 0.1
  
  # Restaurar snapshot con Python listo
  curl -sf -X PUT \
    --unix-socket "${SOCKET}" \
    --data '{
      "snapshot_path": "/opt/snapshots/python-ready/snapshot",
      "mem_backend": {
        "backend_path": "/opt/snapshots/python-ready/memory",
        "backend_type": "File"
      },
      "resume_vm": true
    }' \
    "http://localhost/snapshot/load"
  
  # Inyectar código via MMDS
  curl -sf -X PUT \
    --unix-socket "${SOCKET}" \
    --data "{\"user_code\": \"${USER_CODE}\"}" \
    "http://localhost/mmds"
  
  # Esperar resultado con timeout
  timeout "${TIMEOUT}" bash -c "
    while true; do
      result=\$(curl -sf --unix-socket ${SOCKET} \
        'http://localhost/mmds' 2>/dev/null | jq -r '.result // empty')
      [ -n \"\${result}\" ] && echo \"\${result}\" && break
      sleep 0.1
    done
  "
  
  # Matar la VM
  kill "${FC_PID}" 2>/dev/null
  rm -f "${SOCKET}"
}

run_in_sandbox "${USER_CODE}"

Caso 4: Ambientes de desarrollo reproducibles

Reemplazar Docker Desktop con microVMs para desarrollo en Linux:

# devenv.sh — ambiente de desarrollo en microVM

start_devenv() {
  local PROJECT="$1"
  local SOCKET="/tmp/devenv-${PROJECT}.sock"
  
  # Montar el directorio del proyecto como drive adicional
  # (usando virtio-fs o 9p si el kernel lo soporta)
  
  firecracker --api-sock "${SOCKET}" \
    --config-file "${HOME}/.devenvs/${PROJECT}/config.json" &
  
  echo "Ambiente listo. SSH:"
  echo "  ssh -i ~/.devenvs/${PROJECT}/key [email protected].$(( $(cat /tmp/vm-counter) + 2 ))"
}

Referencia