El curso lleva cuatro módulos echando en falta lo mismo. En el Módulo 4, el release 3.3.0 no arrancó y desplegar.sh hizo un rollback automático —correcto— pero nadie llegó a diagnosticar por qué no arrancaba, porque no había dónde reproducirlo. En 07-01 practicaste la recuperación de arranque rompiendo el fstab del servidor real, con snapshot previo y cruzando los dedos. En 07-03, el último ejercicio pedía vaciar la caché de página, quitar el ionice y parar la aplicación para medir el planificador de E/S: tres cosas que no se hacen en producción. Y quedó pendiente comprobar si la CPU expone AES-NI al huésped, que es un cambio de configuración de la máquina virtual.

Todo eso apunta a la misma carencia: falta un entorno de pruebas. Esta lección lo construye, y por el camino te enseña la pila de virtualización desde dentro — que es lo que hará que la lección siguiente, contenedores, se entienda por contraste y no por analogía equivocada.

Contenido

  1. Qué es virtualizar: los tres modelos
  2. Hipervisores de tipo 1 y de tipo 2
  3. Virtualización frente a contenedores
  4. KVM, QEMU y libvirt: quién hace qué
  5. Instalación y comprobación del soporte
  6. virsh: gestionar máquinas desde la línea de comandos
  7. Crear una máquina con virt-install
  8. Aprovisionamiento desatendido con cloud-init
  9. Almacenamiento: formatos, pools y snapshots
  10. Red: NAT, aislada y puente
  11. Rendimiento: virtio y configuración de CPU
  12. Clonar y crear srv-tramontana-pruebas

Qué es virtualizar: los tres modelos

Virtualizar es ejecutar un sistema operativo completo dentro de otro, haciéndole creer que tiene su propio hardware. El problema técnico de fondo: ciertas instrucciones de la CPU son privilegiadas y solo puede ejecutarlas el kernel. Si el sistema huésped cree ser el kernel y ejecuta una de ellas, hay que interceptarla y hacer algo con ella. Las tres formas de resolverlo definen los tres modelos:

Modelo Cómo maneja las instrucciones privilegiadas Rendimiento Requiere modificar el huésped
Emulación Traduce cada instrucción por software 10-100 veces más lento No
Paravirtualización El huésped sabe que está virtualizado y pide los servicios al hipervisor por una API 90-95 % del nativo Sí
Asistida por hardware La CPU tiene un modo específico (VT-x/AMD-V) que las intercepta sola 95-99 % del nativo No

La emulación sigue teniendo su uso: es lo que permite ejecutar ARM sobre x86 (qemu-system-aarch64) o un sistema de 1985. Para virtualizar Linux sobre Linux en la misma arquitectura, es absurdamente lenta.

La paravirtualización fue la técnica dominante antes de 2006 (Xen la popularizó) y exigía un kernel modificado. Hoy sobrevive en algo mucho más importante de lo que parece: los drivers paravirtualizados, virtio, que verás más adelante y que son la diferencia entre una VM lenta y una rápida.

La virtualización asistida por hardware es lo que usa srv-tramontana y lo que se usa en cualquier sitio hoy. Intel la llama VT-x y AMD, AMD-V; ambas añaden un nivel de privilegio por debajo del kernel donde vive el hipervisor.

Hipervisores de tipo 1 y de tipo 2

Tipo 1 (nativo) Tipo 2 (alojado)
Dónde corre Directamente sobre el hardware Como aplicación sobre un SO
Ejemplos VMware ESXi, Xen, Hyper-V VirtualBox, VMware Workstation
Rendimiento Mayor: sin SO intermedio Menor, aunque poco con VT-x
Uso típico Centros de datos, nube Escritorio, desarrollo, laboratorio

Y KVM desafía esa clasificación, que es lo interesante: es un módulo del kernel de Linux, así que técnicamente hay un sistema operativo debajo (tipo 2), pero ese sistema operativo se convierte en el hipervisor al cargarlo, con acceso directo a las extensiones de virtualización (tipo 1). La respuesta honesta es que es un híbrido, y en la práctica rinde como un tipo 1.

Tu portátil está ejecutando VirtualBox, que es tipo 2. srv-tramontana es una VM dentro de él. Y en esta lección vas a crear una VM dentro de srv-tramontana, lo que se llama virtualización anidada y requiere que VirtualBox exponga las extensiones de CPU al huésped. Volveremos a ello en el apartado de instalación.

Virtualización frente a contenedores

Esta comparación es el puente hacia la lección siguiente, y conviene verla antes de tocar contenedores para no adquirir la analogía equivocada:

graph TB
    subgraph VM["Maquina virtual"]
        H1["Hardware fisico"] --> K1["Kernel anfitrion + KVM"]
        K1 --> Q1["QEMU"] & Q2["QEMU"]
        Q1 --> KG1["Kernel huesped 1"] --> A1["Bibliotecas + app"]
        Q2 --> KG2["Kernel huesped 2"] --> A2["Bibliotecas + app"]
    end
    subgraph CT["Contenedores"]
        H2["Hardware fisico"] --> K2["Kernel anfitrion UNICO"]
        K2 --> N1["namespace + cgroup"] --> B1["Bibliotecas + app"]
        K2 --> N2["namespace + cgroup"] --> B2["Bibliotecas + app"]
    end

La diferencia estructural está en una sola fila del diagrama: cada VM tiene su propio kernel; los contenedores comparten el del anfitrión. De ahí se derivan todas las demás diferencias:

Máquina virtual Contenedor
Kernel Propio Compartido con el anfitrión
Arranque 10-60 segundos Milisegundos
Peso en disco GB (sistema completo) MB (solo la aplicación y sus bibliotecas)
Sobrecarga de memoria 200 MB - 1 GB por VM Prácticamente nula
Aislamiento Fuerte: frontera de hardware virtual Más débil: frontera de kernel
Puede ejecutar otro SO Sí (Windows, BSD, otro kernel Linux) No: solo Linux, y el del anfitrión
Densidad típica Decenas por anfitrión Cientos o miles

Y las dos consecuencias que hay que retener:

  • Para aislamiento de seguridad, la VM es superior. Un escape de contenedor es un fallo del kernel compartido y da acceso al anfitrión. Un escape de VM requiere vulnerar el hipervisor, que es una superficie mucho menor. Por eso los proveedores de nube ejecutan cargas de clientes distintos en VM separadas, no en contenedores del mismo kernel.
  • Para densidad y velocidad, el contenedor gana por órdenes de magnitud. Arrancar cien contenedores es cuestión de segundos; cien VM son cien kernels.

No son alternativas: se combinan. Lo habitual hoy es ejecutar contenedores dentro de máquinas virtuales, aprovechando el aislamiento de la VM entre inquilinos y la densidad del contenedor dentro de cada uno.

KVM, QEMU y libvirt: quién hace qué

Tres piezas que se confunden constantemente. Cada una resuelve un problema distinto:

Pieza Qué es Qué resuelve
KVM Un módulo del kernel (/dev/kvm) Dar acceso a VT-x/AMD-V: ejecutar la CPU virtual a velocidad nativa
QEMU Un programa de espacio de usuario Emular todo lo demás: disco, red, teclado, gráficos, BIOS
libvirt Un demonio y una biblioteca Gestión: definir, arrancar, parar, red, almacenamiento, API estable

La división de trabajo entre las dos primeras es la clave: KVM se encarga de la CPU y la memoria, que es donde el rendimiento importa; QEMU emula los dispositivos, que se acceden mucho menos. Sin KVM, QEMU emularía también la CPU y tendrías el modelo lento del primer apartado. La combinación se suele escribir «QEMU/KVM».

Y libvirt aporta algo que no es obvio hasta que gestionas más de dos máquinas: una capa de abstracción estable. La línea de comandos de QEMU es larguísima y cambia entre versiones; libvirt define la máquina en XML, y virsh habla siempre igual. Además gestiona redes virtuales, pools de almacenamiento y permisos, y es la API que usan virt-manager, Vagrant, OpenStack y Terraform.

Instalación y comprobación del soporte

Primero, comprobar que hay soporte de hardware. Y aquí aparece la complicación de la virtualización anidada:

$ sudo apt install cpu-checker
$ kvm-ok
INFO: /dev/kvm does not exist
HINT:   sudo modprobe kvm_intel
INFO: Your CPU supports KVM extensions
INFO: KVM (vmx) is disabled by your BIOS

srv-tramontana es una VM de VirtualBox, y por defecto VirtualBox no expone las extensiones de virtualización al huésped. Hay que activarlo desde el anfitrión, con la máquina apagada:

# En el portatil anfitrion, con srv-tramontana APAGADA
$ VBoxManage modifyvm srv-tramontana --nested-hw-virt on
$ VBoxManage showvm srv-tramontana --machinereadable | grep -i nested
nestedHWVirt="on"

En VMware la opción equivalente es Virtualize Intel VT-x/EPT, y en un servidor físico basta con activar VT-x/AMD-V en la UEFI. Tras encender de nuevo:

$ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used

$ grep -o -m1 -E 'vmx|svm' /proc/cpuinfo
vmx
$ lsmod | grep kvm
kvm_intel             376832  0
kvm                  1146880  1 kvm_intel
irqbypass              12288  1 kvm

Si kvm-ok sigue fallando, la virtualización funcionará por emulación pura: utilizable para probar la mecánica de esta lección, inutilizable para trabajar de verdad.

$ sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients \
      virtinst bridge-utils libguestfs-tools cloud-image-utils

$ systemctl is-active libvirtd
active

# El grupo libvirt permite gestionar VM sin sudo.
# AVISO de seguridad: quien puede definir una VM puede montar cualquier disco
# del anfitrion dentro de ella y leerlo. Pertenecer a este grupo es
# equivalente a root en la practica. Es la misma advertencia que hara
# falta con el grupo docker en 07-05.
$ sudo usermod -aG libvirt operador
$ newgrp libvirt
$ virsh --connect qemu:///system version
Compiled against library: libvirt 10.0.0
Using library: libvirt 10.0.0
Using API: QEMU 10.0.0
Running hypervisor: QEMU 8.2.2

Fíjate en qemu:///system frente a qemu:///session: el primero son máquinas del sistema, gestionadas por el demonio como root, con acceso a las redes y pools compartidos. El segundo son máquinas del usuario, sin privilegios, con red limitada. Para un servidor, siempre system, y conviene fijarlo para no tener que escribirlo:

$ echo 'export LIBVIRT_DEFAULT_URI="qemu:///system"' >> ~/.bashrc
$ source ~/.bashrc

Y una comprobación importante que cierra el círculo con 07-01: al instalar libvirt aparece virbr0, la interfaz de la red NAT por defecto, y es exactamente la que costaba 35 segundos de arranque porque systemd-networkd-wait-online esperaba a que tuviera enlace:

$ ip -brief addr show virbr0
virbr0           DOWN           192.168.122.1/24
$ systemd-analyze | tail -1
Startup finished in 3.402s (kernel) + 9.118s (userspace) = 12.520s

Sigue en 12 segundos porque en el ejercicio de 07-01 limitaste systemd-networkd-wait-online a enp0s3 con un drop-in. Sin ese arreglo, instalar libvirt habría vuelto a añadir 35 segundos al arranque, y probablemente nadie habría relacionado las dos cosas.

virsh: gestionar máquinas desde la línea de comandos

virsh es la herramienta principal. Sus operaciones esenciales:

$ virsh list --all
 Id   Name   State
--------------------

$ virsh net-list --all
 Name      State    Autostart   Persistent
------------------------------------------------
 default   active   yes         yes

$ virsh pool-list --all
 Name       State    Autostart
-------------------------------
 default    active   yes
Comando Qué hace
virsh list --all Todas las máquinas definidas, con su estado
virsh dominfo <vm> CPU, memoria, autoarranque, seguridad
virsh start <vm> Arranca
virsh shutdown <vm> Apagado ordenado: pide al huésped que se apague (ACPI)
virsh destroy <vm> Corte de corriente: inmediato y sin avisar al huésped
virsh reboot <vm> Reinicio ordenado
virsh autostart <vm> Arranca al iniciar el anfitrión
virsh console <vm> Consola serie: la vía cuando no hay red
virsh edit <vm> Edita el XML con validación
virsh dumpxml <vm> Vuelca la definición
virsh undefine <vm> Borra la definición (no los discos, salvo --remove-all-storage)
virsh domifaddr <vm> Direcciones IP de la máquina

La distinción entre shutdown y destroy merece énfasis porque el nombre engaña: destroy no borra nada, es el equivalente a tirar del cable. No destruye la definición ni los discos, pero sí puede dejar el sistema de ficheros del huésped inconsistente. Es la misma lógica del shutdown frente a tirar del cable de 01-05, y el orden correcto es siempre intentar shutdown primero y esperar:

$ virsh shutdown srv-tramontana-pruebas
Domain 'srv-tramontana-pruebas' is being shutdown
$ for i in {1..30}; do
      [[ "$(virsh domstate srv-tramontana-pruebas)" == "shut off" ]] && break
      sleep 2
  done
$ virsh domstate srv-tramontana-pruebas
shut off

Es exactamente el patrón SIGTERM → esperar → verificar → SIGKILL que aprendiste en 03-06, aplicado a máquinas enteras.

Y virsh console es la herramienta que hace practicable todo lo de 07-01: da acceso a la consola serie del huésped, así que puedes ver el menú de GRUB, entrar en modo emergencia y recuperar un fstab roto sin necesidad de interfaz gráfica. Se sale con Ctrl+].

Crear una máquina con virt-install

virt-install es la forma no interactiva de definir y arrancar una máquina. El ejemplo completo, comentado línea a línea:

$ sudo virt-install \
    --name srv-tramontana-pruebas \
    --memory 2048 \
    --vcpus 2 \
    --cpu host-passthrough \
    --disk path=/var/lib/libvirt/images/pruebas.qcow2,size=20,format=qcow2,bus=virtio \
    --network network=default,model=virtio \
    --os-variant ubuntu24.04 \
    --graphics none \
    --console pty,target_type=serial \
    --location 'http://archive.ubuntu.com/ubuntu/dists/noble/main/installer-amd64/' \
    --extra-args 'console=ttyS0,115200n8'
Opción Por qué ese valor
--cpu host-passthrough Expone la CPU real al huésped, incluido AES-NI — el pendiente de 07-03
bus=virtio Driver paravirtualizado de disco: imprescindible para el rendimiento
model=virtio Lo mismo para la red
--os-variant Permite a libvirt elegir los valores óptimos para ese sistema
--graphics none Un servidor no necesita pantalla virtual
--console pty,target_type=serial Consola serie, que es lo que usa virsh console
console=ttyS0,115200n8 Le dice al kernel del huésped que hable por el puerto serie

Los dos últimos van juntos y son la pareja que hace que virsh console funcione: sin console=ttyS0 en la línea de comandos del kernel, el huésped escribiría en una pantalla virtual que nadie mira, y verías una consola en blanco. Es un fallo desconcertante y muy común.

Una instalación interactiva desde el instalador de Ubuntu tarda veinte minutos y hay que responder preguntas. Para un entorno de pruebas que quieres poder recrear, eso no sirve. La alternativa es la siguiente sección.

Aprovisionamiento desatendido con cloud-init

Las distribuciones publican imágenes de nube: discos ya instalados, con cloud-init dentro, que se configuran solos en el primer arranque a partir de unos datos que les entregas.

$ cd /var/lib/libvirt/images
$ sudo wget -q https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
$ sudo qemu-img info noble-server-cloudimg-amd64.img
image: noble-server-cloudimg-amd64.img
file format: qcow2
virtual size: 3.5 GiB (3758096384 bytes)
disk size: 588 MiB

Fíjate en la diferencia entre virtual size y disk size: 3,5 GiB declarados, 588 MiB ocupados. Es el aprovisionamiento fino de qcow2, que se explica en el apartado siguiente.

Los datos de configuración se entregan en dos ficheros YAML:

$ mkdir -p ~/cloud-init && cd ~/cloud-init
$ cat > user-data <<'EOF'
#cloud-config
hostname: srv-tramontana-pruebas
fqdn: srv-tramontana-pruebas.tramontana.example
manage_etc_hosts: true

users:
  - name: operador
    groups: [sudo, adm]
    shell: /bin/bash
    sudo: 'ALL=(ALL) NOPASSWD:ALL'
    lock_passwd: false
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... operador@portatil-alumno

# Coherente con lo aprendido en 06-02: nada de contrasenas por SSH
ssh_pwauth: false
disable_root: true

package_update: true
package_upgrade: true
packages:
  - postgresql-16
  - restic
  - ufw
  - auditd
  - libpam-pwquality
  - sysstat
  - linux-tools-generic

write_files:
  - path: /etc/tramontana/app.conf
    owner: root:root
    permissions: '0640'
    content: |
      db_host=127.0.0.1
      db_port=5432
      db_name=tramontana_reservas
      max_conexiones=80
      timeout_consulta=30
      log_nivel=debug
      escucha=127.0.0.1
  - path: /etc/sysctl.d/70-rendimiento.conf
    owner: root:root
    permissions: '0644'
    content: |
      # Copiado de produccion para que las pruebas sean representativas
      net.core.default_qdisc = fq
      net.ipv4.tcp_congestion_control = bbr
      vm.swappiness = 10
      vm.dirty_background_ratio = 5
      vm.dirty_ratio = 10

runcmd:
  - [ install, -d, -m, '0750', -o, root, -g, root, /opt/tramontana ]
  - [ install, -d, -m, '2770', /srv/tramontana/backups ]
  - [ sysctl, --system ]
  - [ systemctl, enable, --now, sysstat ]

final_message: "srv-tramontana-pruebas listo tras $UPTIME segundos"
EOF

$ cat > network-config <<'EOF'
version: 2
ethernets:
  enp1s0:
    dhcp4: true
EOF

Los dos ficheros se empaquetan en una imagen ISO minúscula que la máquina lee como si fuera un CD:

$ cloud-localds -N network-config semilla.iso user-data
$ ls -lh semilla.iso
-rw-rw-r-- 1 operador operador 366K ago 18 18:04 semilla.iso
$ sudo mv semilla.iso /var/lib/libvirt/images/

Y ahora se crea la máquina, con un detalle importante: la imagen descargada no se usa directamente, se crea un disco nuevo que la usa como respaldo:

$ cd /var/lib/libvirt/images
# El disco de la VM se respalda en la imagen base, que queda intacta
$ sudo qemu-img create -f qcow2 -F qcow2 \
      -b noble-server-cloudimg-amd64.img pruebas.qcow2 20G
Formatting 'pruebas.qcow2', fmt=qcow2 cluster_size=65536 ...

$ sudo virt-install \
    --name srv-tramontana-pruebas \
    --memory 2048 --vcpus 2 --cpu host-passthrough \
    --disk path=/var/lib/libvirt/images/pruebas.qcow2,device=disk,bus=virtio \
    --disk path=/var/lib/libvirt/images/semilla.iso,device=cdrom \
    --network network=default,model=virtio \
    --os-variant ubuntu24.04 \
    --graphics none --console pty,target_type=serial \
    --import --noautoconsole

Domain creation completed.

$ virsh list
 Id   Name                     State
----------------------------------------
 1    srv-tramontana-pruebas   running

$ sleep 60 && virsh domifaddr srv-tramontana-pruebas
 Name       MAC address          Protocol     Address
-------------------------------------------------------------------------------
 vnet0      52:54:00:8a:c1:f2    ipv4         192.168.122.104/24

$ ssh [email protected] 'hostname; sysctl -n net.ipv4.tcp_congestion_control'
srv-tramontana-pruebas
bbr

Menos de dos minutos, sin una sola pregunta, y con la configuración de producción ya aplicada. Y —esto es lo que hace valioso el método— el user-data es un fichero de texto que se versiona en git: la máquina de pruebas es reproducible. Es la misma idea de infraestructura como código que verás con Ansible en 07-06, en su forma más simple.

Almacenamiento: formatos, pools y snapshots

raw frente a qcow2

$ sudo qemu-img create -f raw prueba.raw 10G
$ sudo qemu-img create -f qcow2 prueba.qcow2 10G
$ ls -lh prueba.raw prueba.qcow2
-rw-r--r-- 1 root root  10G ago 18 18:12 prueba.raw
-rw-r--r-- 1 root root 193K ago 18 18:12 prueba.qcow2
$ du -h prueba.raw prueba.qcow2
0	prueba.raw
196K	prueba.qcow2

Fíjate en la diferencia entre ls y du para prueba.raw: ls dice 10 GB, du dice 0. Es un fichero disperso (sparse), así que raw también hace aprovisionamiento fino en sistemas de ficheros que lo soportan. La diferencia real entre los formatos está en otra parte:

raw qcow2
Rendimiento El máximo posible 95-98 % del de raw
Aprovisionamiento fino Sí, con ficheros dispersos Sí, nativo
Snapshots internos No Sí
Discos de respaldo No Sí
Compresión y cifrado No Sí
Se puede montar en el anfitrión Directamente con losetup Requiere qemu-nbd

La decisión práctica: qcow2 salvo que midas que el 2-5 % de rendimiento importa. Los snapshots y los discos de respaldo valen mucho más que ese margen en un entorno de pruebas, y para producción existen alternativas mejores que ambos (un volumen LVM directo, que es lo que usan muchos hipervisores).

$ sudo qemu-img info pruebas.qcow2
image: pruebas.qcow2
file format: qcow2
virtual size: 20 GiB (21474836480 bytes)
disk size: 1.24 GiB
backing file: noble-server-cloudimg-amd64.img
backing file format: qcow2

# Ampliar un disco: primero el fichero, DESPUES el sistema de ficheros dentro
$ sudo qemu-img resize pruebas.qcow2 +10G
Image resized.
# Y dentro del huesped, con lo aprendido en 05-04:
$ ssh [email protected] 'sudo growpart /dev/vda 1 && sudo resize2fs /dev/vda1'

# Convertir entre formatos
$ sudo qemu-img convert -f qcow2 -O raw pruebas.qcow2 pruebas.raw

El disco de respaldo (backing file) es la pieza que hace eficiente un laboratorio: la imagen base se comparte y cada VM solo guarda sus diferencias. Diez máquinas de pruebas ocupan la base más diez pequeños ficheros de cambios, no diez sistemas completos. Con una regla que hay que respetar: si modificas la imagen base, todos los discos que se respaldan en ella se corrompen. La base es de solo lectura, en la práctica.

# Aplanar un disco: incorporar el contenido de la base y romper la dependencia
$ sudo qemu-img rebase -p -b "" pruebas.qcow2

Pools de almacenamiento

libvirt organiza el almacenamiento en pools, que abstraen dónde viven los discos:

$ virsh pool-list --all --details
 Name      State    Autostart  Persistent  Capacity  Allocation  Available
-------------------------------------------------------------------------------
 default   running  yes        yes         14.51 GiB    1.31 GiB   13.20 GiB

$ virsh pool-dumpxml default | grep -E '<path>|<type|name>'
<pool type='dir'>
  <name>default</name>
      <path>/var/lib/libvirt/images</path>

# Crear un pool en el volumen de datos, que tiene mas sitio
$ virsh pool-define-as pruebas dir --target /srv/tramontana/vm
$ virsh pool-build pruebas && virsh pool-start pruebas
$ virsh pool-autostart pruebas
$ virsh vol-list pruebas --details

Un pool puede ser un directorio, un grupo de volúmenes LVM, un dispositivo iSCSI, NFS o Ceph. El de tipo logical sobre LVM es interesante aquí: vg-datos ya existe desde 05-04 y usar volúmenes lógicos directos como discos evita la capa de fichero.

Snapshots, y la advertencia

$ virsh snapshot-create-as srv-tramontana-pruebas \
    --name limpia-24.04 \
    --description "Recien aprovisionada por cloud-init, antes de pruebas" \
    --atomic
Domain snapshot limpia-24.04 created

$ virsh snapshot-list srv-tramontana-pruebas
 Name           Creation Time               State
-----------------------------------------------------
 limpia-24.04   2026-08-18 18:22:41 +0200   shutoff

# Romper algo, probar, y volver atras
$ virsh snapshot-revert srv-tramontana-pruebas --snapshotname limpia-24.04
$ virsh snapshot-delete srv-tramontana-pruebas --snapshotname limpia-24.04

Y la advertencia, con el mismo peso que la de 05-04 sobre RAID:

Un snapshot no es una copia de seguridad. Vive en el mismo fichero (o junto a él), en el mismo disco, en la misma máquina. Si el disco falla, si el fichero se corrompe o si alguien borra el directorio, se van el snapshot y el original juntos. No cumple ninguno de los tres requisitos de la regla 3-2-1 de 05-08.

Lo que un snapshot es: un punto de retorno rápido para una operación arriesgada. Exactamente lo que necesitas antes de probar un cambio de kernel. Y tiene dos costes que hay que conocer: cada snapshot activo degrada el rendimiento de escritura, porque cada bloque modificado exige copiar el original primero; y acumular decenas de snapshots encadenados puede hacer el disco inmanejable.

Los snapshots con la máquina en marcha (--live) capturan también la memoria y son mucho más delicados: si la aplicación tiene datos a medio escribir, el estado restaurado puede ser inconsistente. La opción --quiesce pide al agente del huésped que vacíe los sistemas de ficheros antes, y requiere qemu-guest-agent instalado dentro. Para un entorno de pruebas, apagar la máquina y hacer el snapshot en frío es más simple y más fiable.

Red: NAT, aislada y puente

Los tres modos, y cuándo cada uno:

Modo La VM ve La red ve la VM Entre VM Cuándo
NAT (default) Internet, sí No Sí Por defecto; laboratorio
Aislada Nada fuera No Sí Pruebas sin salida; malware
Puente Todo Sí, con IP propia Sí Servidores que deben ser accesibles
$ virsh net-dumpxml default
<network>
  <name>default</name>
  <forward mode='nat'/>
  <bridge name='virbr0' stp='on' delay='0'/>
  <ip address='192.168.122.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='192.168.122.2' end='192.168.122.254'/>
    </dhcp>
  </ip>
</network>

En NAT, libvirt monta un puente (virbr0), un servidor DHCP y DNS (dnsmasq) y las reglas de traducción de direcciones. La VM sale a Internet con la IP del anfitrión, y desde la red nadie puede iniciar una conexión hacia ella. Para un entorno de pruebas es lo correcto: aislamiento por defecto sin configurar nada.

Y aquí conviene cerrar el asunto de virbr0 y el arranque, porque ahora se ve el mecanismo completo: la interfaz existe desde que libvirt arranca, pero no tiene enlace hasta que una VM se conecta a ella. systemd-networkd-wait-online, en su configuración por defecto, esperaba a que todas las interfaces gestionadas estuvieran en línea, y esa nunca lo estaría con las máquinas apagadas. De ahí los 35 segundos.

$ virsh net-define /dev/stdin <<'EOF'
<network>
  <name>aislada</name>
  <bridge name='virbr1' stp='on' delay='0'/>
  <ip address='192.168.200.1' netmask='255.255.255.0'>
    <dhcp><range start='192.168.200.10' end='192.168.200.100'/></dhcp>
  </ip>
</network>
EOF
$ virsh net-start aislada && virsh net-autostart aislada

Sin elemento <forward>, la red no tiene salida: las máquinas se ven entre sí y con el anfitrión, y nada más. Es lo que se usa para probar una configuración de firewall o analizar algo sospechoso.

El puente es el modo que hace falta cuando la VM debe ser un servidor accesible. Requiere reconfigurar la red del anfitrión con netplan, aplicando 06-01 — y con el mismo cuidado, porque te puede dejar sin acceso a la máquina:

$ sudo cp -p /etc/netplan/50-cloud-init.yaml \
      /etc/netplan/50-cloud-init.yaml.bak-$(date +%F)
$ sudo tee /etc/netplan/60-puente.yaml >/dev/null <<'EOF'
network:
  version: 2
  ethernets:
    enp0s3:
      dhcp4: false
      dhcp6: false
  bridges:
    br0:
      interfaces: [enp0s3]
      addresses: [10.0.2.15/24]
      routes:
        - to: default
          via: 10.0.2.2
      nameservers:
        addresses: [10.0.2.2, 1.1.1.1]
      parameters:
        stp: false
        forward-delay: 0
EOF
$ sudo chmod 600 /etc/netplan/60-puente.yaml

# netplan try con reversion automatica a los 120 s: la red de seguridad de 06-01
$ sudo netplan try
Do you want to keep these settings?
Press ENTER before the timeout to accept the new configuration

Ese netplan try no es opcional. Configurar un puente por SSH es exactamente el caso para el que existe: si el puente sale mal, pierdes la sesión, y la reversión automática a los 120 segundos te devuelve la máquina.

Y una advertencia específica: el puente reasigna la IP de la interfaz física al puente. Durante la transición hay un corte de red de unos segundos, y si la configuración es incorrecta el corte es permanente. En un servidor remoto sin consola, esto se hace con muchísimo cuidado.

$ virsh attach-interface srv-tramontana-pruebas bridge br0 \
      --model virtio --persistent

Rendimiento: virtio y configuración de CPU

virtio: la diferencia entre lenta y rápida

Los dispositivos que QEMU presenta al huésped pueden ser emulados (imita un dispositivo real, como una tarjeta Intel e1000) o paravirtualizados (virtio: un dispositivo que no existe en el mundo real, diseñado para hablar directamente con el hipervisor).

La diferencia es grande y merece verse medida:

Dispositivo Emulado virtio
Disco ~40 % del rendimiento nativo 95-98 %
Red ~1 Gbit/s con mucha CPU 10 Gbit/s+ con poca CPU
$ virsh dumpxml srv-tramontana-pruebas | grep -A2 -E "<disk|<interface"
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' cache='none' io='native' discard='unmap'/>
      <source file='/var/lib/libvirt/images/pruebas.qcow2'/>
      <target dev='vda' bus='virtio'/>
    <interface type='network'>
      <mac address='52:54:00:8a:c1:f2'/>
      <source network='default'/>
      <model type='virtio'/>

Las pistas de que virtio está activo: el disco se llama vda (no sda) y el bus es virtio. Si ves sda con bus sata, estás usando emulación y perdiendo más de la mitad del rendimiento de disco.

Los tres atributos del <driver> también importan:

Atributo Valor Por qué
cache none Evita la doble caché anfitrión/huésped; más seguro ante cortes
io native E/S asíncrona del kernel; mejor con cache=none
discard unmap Propaga el TRIM del huésped: el fichero qcow2 se reduce al borrar

discard=unmap es el que evita que un disco qcow2 crezca indefinidamente: sin él, borrar datos dentro del huésped no libera espacio en el anfitrión.

Configuración de CPU

$ virsh dumpxml srv-tramontana-pruebas | grep -A3 '<cpu'
  <cpu mode='host-passthrough' check='none' migratable='on'/>

Los tres modos:

Modo Qué expone Migración en vivo
host-passthrough La CPU real, todas sus extensiones Solo a anfitriones idénticos
host-model Un modelo equivalente al del anfitrión A anfitriones similares
custom Un modelo concreto que tú eliges A cualquier anfitrión con ese modelo o superior

Y aquí se resuelve el pendiente de 07-03, el de AES-NI:

$ ssh [email protected] 'grep -o -m1 aes /proc/cpuinfo'
aes

$ ssh [email protected] 'openssl speed -evp aes-256-cbc 2>&1 | tail -2'
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes
aes-256-cbc    1284412.10k  3841204.88k  4218844.12k  4402118.42k  4441204.88k

4,4 GB/s de cifrado AES: eso solo es posible con aceleración por hardware. Con host-model en lugar de host-passthrough, o con una CPU virtual genérica, el mismo openssl speed daría entre 200 y 600 MB/s, y ahí está el factor de cinco a diez que se planteaba como hipótesis en el último ejercicio de 07-03 para la lentitud de la copia cifrada.

La contrapartida de host-passthrough es la migración en vivo: una máquina que ve las extensiones exactas de un procesador no puede migrarse a otro anfitrión que no las tenga, porque el huésped las está usando. Para un entorno de pruebas en un único anfitrión, host-passthrough es la elección correcta.

Dos ajustes más de memoria, mencionados por completitud:

$ virsh dumpxml srv-tramontana-pruebas | grep -A3 -E '<memballoon|<memoryBacking'
    <memballoon model='virtio'>

El ballooning permite al anfitrión recuperar memoria de un huésped que no la está usando, mediante un driver dentro del huésped que la «infla». Es útil para densidad, y contraproducente para una base de datos, que reserva memoria compartida y no espera perderla. Las hugepages (<memoryBacking><hugepages/></memoryBacking>) reducen los fallos de TLB del huésped, con la misma lógica que las THP de 07-03, y son habituales en máquinas grandes de base de datos.

Clonar y crear srv-tramontana-pruebas

La máquina de cloud-init es limpia y reproducible, pero para reproducir un problema de producción a veces hace falta una copia del estado real. virt-clone la hace:

# La maquina origen debe estar APAGADA
$ virsh shutdown srv-tramontana-pruebas
$ virt-clone --original srv-tramontana-pruebas \
             --name srv-tramontana-pruebas-2 \
             --auto-clone
Allocating 'pruebas-2.qcow2'   |  20 GB  00:00:04
Clone 'srv-tramontana-pruebas-2' created successfully.

virt-clone copia los discos y genera direcciones MAC y UUID nuevos, que es lo que evita el conflicto que produce copiar el fichero a mano. Pero deja intacto el interior del huésped: mismo nombre de máquina, mismas claves de host de SSH, mismo machine-id. Y eso da problemas reales:

$ sudo virt-sysprep -d srv-tramontana-pruebas-2 \
    --hostname pruebas-2 \
    --operations defaults,-ssh-userdir
[   0.0] Examining the guest ...
[   4.2] Performing "abrt-data" ...
[   4.2] Performing "bash-history" ...
[   4.3] Performing "machine-id" ...
[   4.4] Performing "ssh-hostkeys" ...
[   4.5] Performing "logfiles" ...
[   4.8] Setting a random seed
[   4.9] Setting the machine ID in /etc/machine-id
[   5.0] Setting the hostname: pruebas-2

virt-sysprep limpia lo que hace única a una máquina: machine-id —que systemd y el journal usan como identificador—, claves de host de SSH, registros, historial de shell, y configuración de red asociada a la MAC anterior. Sin este paso, dos máquinas con el mismo machine-id producen entradas de journal mezcladas si se centralizan, y dos con las mismas claves de host provocan el aviso de huella cambiada de 06-02.

Y libguestfs permite además trabajar con el disco de una máquina apagada sin arrancarla, lo que resuelve el escenario de 07-01 sin necesidad de un medio de rescate:

# Inspeccionar un disco sin arrancar la maquina
$ sudo virt-ls -d srv-tramontana-pruebas /etc/tramontana/
app.conf

# Leer un fichero
$ sudo virt-cat -d srv-tramontana-pruebas /etc/fstab

# Y ARREGLAR un fstab roto sin arrancar ni usar un Live CD
$ sudo guestfish -d srv-tramontana-pruebas -i edit /etc/fstab

Eso es lo que convierte la recuperación de 07-01 en una operación de dos minutos cuando la máquina es virtual: en lugar de arrancar desde un ISO, montar y hacer chroot, se edita el fichero directamente en el disco apagado.

El entorno de pruebas, y para qué sirve

Con srv-tramontana-pruebas en marcha, las cuatro deudas del principio de la lección se pueden liquidar:

# 1. El release 3.3.0 que no arrancaba (deuda del Modulo 4)
$ virsh snapshot-create-as srv-tramontana-pruebas --name antes-de-3.3.0 --atomic
$ scp /srv/tramontana/backups/envios/tramontana-3.3.0.tar.gz \
      [email protected]:/tmp/
$ ssh [email protected] \
      'sudo ~/scripts/desplegar.sh 3.3.0; sudo journalctl -u tramontana -n 40'
# Y con el log completo delante, por fin se puede diagnosticar

# 2. El planificador de E/S (ejercicio 3 de 07-03), sin degradar produccion
$ ssh [email protected] \
      'sync; echo 3 | sudo tee /proc/sys/vm/drop_caches; \
       echo mq-deadline | sudo tee /sys/block/vda/queue/scheduler'

# 3. La recuperacion de arranque de 07-01, con virsh console
$ virsh console srv-tramontana-pruebas
# (Ctrl+] para salir)

# 4. Y AES-NI, ya comprobado
$ ssh [email protected] 'grep -c -m1 aes /proc/cpuinfo'
1

# Volver al punto de partida cuando se acabe
$ virsh snapshot-revert srv-tramontana-pruebas --snapshotname antes-de-3.3.0

Sobre la migración en vivo, para cerrar: libvirt permite mover una máquina en ejecución de un anfitrión a otro sin interrumpirla, copiando la memoria en varias pasadas hasta que queda tan poco por transferir que se puede pausar unos milisegundos y completar el cambio.

$ virsh migrate --live srv-tramontana-pruebas qemu+ssh://anfitrion2/system

Requiere almacenamiento compartido (o --copy-storage-all, mucho más lento) y CPU compatibles —de ahí la contrapartida de host-passthrough—. Es la base del mantenimiento sin cortes: se vacían los anfitriones uno a uno para actualizarlos. Y es un adelanto de 07-07: mover una máquina no es lo mismo que tener alta disponibilidad, porque la migración requiere que el anfitrión origen siga funcionando. Frente a un fallo repentino, no sirve.

Herramientas gráficas y de más alto nivel, mencionadas: virt-manager es la interfaz gráfica de libvirt, cómoda para explorar y para acceder a la consola de una máquina con entorno gráfico; Vagrant define máquinas de desarrollo en un Vagrantfile versionable y funciona sobre libvirt, VirtualBox o VMware, y es la herramienta indicada cuando un equipo entero necesita entornos idénticos; y Terraform y OpenStack operan a escala de infraestructura, hablando también con libvirt por debajo.

Errores Comunes y Consejos

  • No activar la virtualización anidada. kvm-ok dice KVM is disabled by your BIOS y todo funciona por emulación, diez veces más lento. En VirtualBox, VBoxManage modifyvm <vm> --nested-hw-virt on con la máquina apagada.
  • Olvidar console=ttyS0 en la línea de comandos del kernel. virsh console muestra una pantalla en blanco y parece que la máquina no arranca. Va emparejado con --console pty,target_type=serial.
  • Usar dispositivos emulados en vez de virtio. Si el disco del huésped se llama sda y no vda, estás perdiendo más de la mitad del rendimiento de disco. Comprueba bus='virtio' en el XML.
  • Confundir destroy con borrar. virsh destroy es tirar del cable: no borra nada, pero puede dejar el sistema de ficheros del huésped inconsistente. Usa shutdown y espera.
  • Modificar una imagen base que tiene discos respaldándose en ella. Corrompe todos los discos derivados. La base es de solo lectura en la práctica; usa qemu-img rebase si necesitas independizar uno.
  • Confiar en un snapshot como copia de seguridad. Vive en el mismo disco y en la misma máquina. No cumple ninguno de los tres requisitos de la regla 3-2-1.
  • Acumular snapshots encadenados. Cada uno activo degrada la escritura, y una cadena larga hace el disco inmanejable. Consolida o borra.
  • Clonar sin virt-sysprep. Dos máquinas con el mismo machine-id y las mismas claves de host de SSH provocan journals mezclados y avisos de huella cambiada.
  • Configurar un puente por SSH sin netplan try. Un puente mal definido te deja fuera de la máquina de forma permanente. La reversión automática a los 120 segundos existe exactamente para esto.
  • Omitir discard=unmap. El fichero qcow2 crece indefinidamente porque borrar dentro del huésped no libera espacio en el anfitrión.
  • Activar ballooning en una máquina de base de datos. La base de datos reserva memoria compartida y no espera que se la quiten. Provoca degradación difícil de diagnosticar.
  • Consejo de método. Versiona el user-data de cloud-init en git junto a los scripts. Una máquina de pruebas que se recrea con un comando en dos minutos se usa; una que hay que reinstalar a mano se abandona en cuanto se desconfigura.

Ejercicios

Ejercicio 1

Diseña el user-data de cloud-init para una máquina srv-tramontana-pruebas que reproduzca la configuración de seguridad de producción lo más fielmente posible, y explica qué no se puede reproducir y por qué. Justifica cada decisión.

Ejercicio 2

El release 3.3.0 no arranca. Diseña el procedimiento completo para diagnosticarlo en el entorno de pruebas, usando lo aprendido en 07-01, 07-02 y esta lección, de forma que puedas repetir el intento tantas veces como haga falta.

Ejercicio 3

Marta pregunta si conviene mover Tramontana Reservas a máquinas virtuales gestionadas por KVM en un servidor propio, en lugar del VPS actual. Redacta el análisis: qué se gana, qué se pierde, y qué recomiendas.

Soluciones

Solución 1

#cloud-config
# user-data para srv-tramontana-pruebas
# Objetivo: reproducir produccion lo suficiente para que las pruebas sean
# validas, SIN copiar nada que sea un secreto real.
hostname: srv-tramontana-pruebas
fqdn: srv-tramontana-pruebas.tramontana.example
manage_etc_hosts: true

# --- Identidades: mismos uid/gid que produccion ---
# Motivo: los permisos, ACL y SGID solo son representativos si los
# identificadores numericos coinciden. Un 2770 sobre el gid 1002 solo se
# comporta igual si el grupo tramontana ES el 1002.
groups:
  - tramontana: [operador]

users:
  - name: operador
    uid: 1000
    groups: [sudo, adm, tramontana]
    shell: /bin/bash
    lock_passwd: false
    sudo: 'ALL=(ALL) NOPASSWD:ALL'
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... operador@portatil-alumno
  - name: luis
    uid: 1001
    groups: [tramontana]
    shell: /bin/bash
    lock_passwd: true
  - name: svc-tramontana
    uid: 997
    system: true
    shell: /usr/sbin/nologin
    homedir: /opt/tramontana
    create_groups: false
    no_create_home: true

ssh_pwauth: false
disable_root: true

# --- Paquetes: los mismos que produccion, mas los de diagnostico ---
package_update: true
package_upgrade: true
packages:
  - postgresql-16
  - restic
  - ufw
  - fail2ban
  - auditd
  - aide
  - apparmor-utils
  - libpam-pwquality
  - needrestart
  - sysstat
  # Herramientas de diagnostico: en pruebas SI, en produccion no hacen falta
  - linux-tools-generic
  - strace
  - bpfcc-tools
  - bpftrace
  - qemu-guest-agent

write_files:
  # Configuracion de la aplicacion, con log_nivel=debug (unica diferencia)
  - path: /etc/tramontana/app.conf
    owner: root:tramontana
    permissions: '0640'
    content: |
      db_host=127.0.0.1
      db_port=5432
      db_name=tramontana_reservas
      max_conexiones=80
      timeout_consulta=30
      log_nivel=debug
      escucha=127.0.0.1

  # sysctl de rendimiento: identico a produccion. Sin esto, cualquier
  # medicion comparativa seria invalida.
  - path: /etc/sysctl.d/70-rendimiento.conf
    owner: root:root
    permissions: '0644'
    content: |
      net.core.default_qdisc = fq
      net.ipv4.tcp_congestion_control = bbr
      vm.swappiness = 10
      vm.dirty_background_ratio = 5
      vm.dirty_ratio = 10
      fs.inotify.max_user_watches = 524288

  # sysctl de seguridad: identico, INCLUIDO ptrace_scope.
  # Decision deliberada: si en pruebas relajamos ptrace_scope, no
  # reproduciriamos el "Operation not permitted" de 07-02, y las pruebas
  # de diagnostico no serian representativas.
  - path: /etc/sysctl.d/60-endurecimiento.conf
    owner: root:root
    permissions: '0644'
    content: |
      kernel.dmesg_restrict = 1
      kernel.kptr_restrict = 2
      kernel.randomize_va_space = 2
      fs.protected_hardlinks = 1
      fs.protected_symlinks = 1
      fs.protected_fifos = 2
      fs.protected_regular = 2
      kernel.unprivileged_bpf_disabled = 1
      kernel.yama.ptrace_scope = 1

  - path: /etc/modprobe.d/blacklist-tramontana.conf
    owner: root:root
    permissions: '0644'
    content: |
      install cramfs /bin/true
      install freevxfs /bin/true
      install jffs2 /bin/true
      install udf /bin/true
      install dccp /bin/true
      install sctp /bin/true

  # Datos de prueba SINTETICOS, generados, nunca copiados de produccion
  - path: /home/operador/datos/casas.txt
    owner: operador:operador
    permissions: '0640'
    content: |
      mas-figueres
      can-ventos
      la-solana
      cal-ferrer
      el-moli

runcmd:
  # Rutas con los mismos permisos y SGID que produccion
  - [ install, -d, -m, '0755', -o, root, -g, root, /opt/tramontana ]
  - [ install, -d, -m, '2770', -o, svc-tramontana, -g, tramontana,
      /opt/tramontana/shared/uploads ]
  - [ install, -d, -m, '0750', -o, svc-tramontana, -g, adm,
      /var/log/tramontana ]
  - [ install, -d, -m, '2770', -o, operador, -g, tramontana,
      /srv/tramontana/backups ]
  # Firewall con la misma politica de lista blanca
  - [ ufw, --force, default, deny, incoming ]
  - [ ufw, --force, default, allow, outgoing ]
  - [ ufw, limit, '22/tcp' ]
  - [ ufw, allow, from, 192.168.122.0/24, to, any, port, '5432', proto, tcp ]
  - [ ufw, --force, enable ]
  # Aplicar y activar
  - [ sysctl, --system ]
  - [ update-initramfs, -u, -k, all ]
  - [ systemctl, enable, --now, sysstat ]
  - [ systemctl, enable, --now, qemu-guest-agent ]
  # Generar reservas.csv sinteticas con la misma estructura
  - [ bash, -c, 'printf "id;fecha;casa;huesped;noches;importe\n" > /home/operador/datos/reservas.csv' ]
  - [ bash, -c, 'for i in $(seq 1001 1025); do printf "%s;2026-08-%02d;mas-figueres;Huesped %s;2;250,00\n" "$i" "$((i-1000))" "$i"; done >> /home/operador/datos/reservas.csv' ]
  - [ chown, 'operador:operador', /home/operador/datos/reservas.csv ]

final_message: "srv-tramontana-pruebas listo tras $UPTIME segundos"

Qué NO se puede reproducir, y por qué. Esta es la parte importante del ejercicio, porque determina qué pruebas son válidas:

No reproducible Por qué Consecuencia para las pruebas
Los secretos reales (db_password, contraseña de restic, clave GPG de pass) Copiarlos multiplicaría por dos su exposición y violaría la regla de 06-05 Las pruebas usan credenciales propias. No se puede probar la rotación real
El fichero .cred de systemd-creds Está cifrado con una clave derivada de la máquina de producción; es indescifrable aquí Hay que generar uno nuevo. Es la limitación que ya se documentó en 06-05
El certificado de Let's Encrypt Requiere el dominio público y el desafío ACME. Además, cada emisión consume cuota Se usa un autofirmado. No se puede probar la renovación real
Los datos personales de huéspedes RGPD: un entorno de pruebas tiene menos controles, más accesos y más copias. Copiarlos sería una infracción Datos sintéticos con la misma estructura y volumen
El volumen LUKS con su clave La clave es un secreto y el disco es físico Se puede crear un LUKS nuevo con clave de pruebas. Válido para medir rendimiento, no para restaurar copias reales
La base de datos de AIDE Depende de las sumas de los ficheros reales de esta máquina Se inicializa aquí. Válido para probar el mecanismo, no para comparar con producción
El hardware exacto Es una VM anidada, con dos capas de hipervisor Las mediciones absolutas de rendimiento no son comparables; solo las relativas antes/después dentro de la propia máquina

Y las tres decisiones que conviene justificar porque son contraintuitivas:

  1. Mantener ptrace_scope = 1 en pruebas. La tentación es relajarlo para diagnosticar cómodamente. Sería un error: entonces no reproduciríamos el Operation not permitted de 07-02 y las pruebas de diagnóstico no valdrían. Un entorno de pruebas que difiere en seguridad no prueba la seguridad.
  2. Mismos UID y GID numéricos. Es fácil dejar que cloud-init asigne los que quiera, y entonces el 2770 sobre el grupo tramontana no se comporta igual, y las pruebas de permisos son inválidas. Los identificadores numéricos son parte de la configuración.
  3. log_nivel=debug como única diferencia intencionada, más las herramientas de diagnóstico. Ambas cosas son más de lo que hay en producción, no menos, así que no invalidan nada — y hay que anotarlas, porque el volumen de logs sí difiere.

La última advertencia va al runbook: este user-data contiene la clave pública de SSH y los identificadores de la organización, así que se versiona en el repositorio interno, no en uno público. Y nunca debe contener un secreto real: para eso está pass y el aprovisionamiento posterior.

Solución 2

La clave del diseño es que el diagnóstico debe ser repetible: cada intento parte del mismo estado, para que las diferencias observadas se deban al cambio y no a residuos del intento anterior.

# --- Fase 0: punto de retorno limpio y repetible ---
$ virsh shutdown srv-tramontana-pruebas
$ for i in {1..30}; do
      [[ "$(virsh domstate srv-tramontana-pruebas)" == "shut off" ]] && break
      sleep 2
  done
$ virsh snapshot-create-as srv-tramontana-pruebas \
    --name base-3.2.1 \
    --description "3.2.1 activa y verificada, antes de intentar 3.3.0" \
    --atomic
$ virsh start srv-tramontana-pruebas

El snapshot en frío es deliberado: un --live capturaría la memoria y, sin --quiesce, podría dejar PostgreSQL inconsistente. Para un punto de retorno, apagado es más simple y más fiable.

# --- Fase 1: verificar el estado de partida ---
$ VM=192.168.122.104
$ ssh operador@$VM '~/scripts/revision_salud.sh; echo "estado: $?"'
estado: 0
$ ssh operador@$VM 'readlink /opt/tramontana/app'
releases/3.2.1

Sin esta verificación, un fallo de 3.3.0 podría confundirse con un entorno de pruebas que ya estaba roto.

# --- Fase 2: preparar la observacion ANTES de provocar el fallo ---
# El error puede durar milisegundos: hay que estar mirando cuando ocurra.
$ ssh operador@$VM 'sudo journalctl -f -u tramontana' > /tmp/journal-3.3.0.log &
$ ssh operador@$VM 'sudo execsnoop-bpfcc -T' > /tmp/procesos-3.3.0.log &
$ ssh operador@$VM 'sudo journalctl -f -k | grep -i apparmor' > /tmp/apparmor.log &

execsnoop-bpfcc es la elección informada aquí: si el binario arranca y muere en 200 ms, ps no lo verá nunca, y esta herramienta captura cada execve con su código de salida. Es exactamente el caso de uso de la tabla de 07-02.

# --- Fase 3: el intento, con el modo de simulacion primero ---
$ scp /srv/tramontana/backups/envios/tramontana-3.3.0.tar.gz \
      /srv/tramontana/backups/envios/tramontana-3.3.0.tar.gz.sha256 \
      operador@$VM:/tmp/
$ ssh operador@$VM 'cd /tmp && sha256sum -c tramontana-3.3.0.tar.gz.sha256'
tramontana-3.3.0.tar.gz: La suma coincide

# --dry-run primero: convencion del curso, y descarta problemas del script
$ ssh operador@$VM 'sudo ~/scripts/desplegar.sh --dry-run 3.3.0'
$ ssh operador@$VM 'sudo ~/scripts/desplegar.sh 3.3.0'
# --- Fase 4: recoger la evidencia, de lo general a lo especifico ---

# 4a. El journal. La causa esta aqui el 70% de las veces.
$ ssh operador@$VM 'sudo journalctl -u tramontana -n 60 --no-pager -o short-precise'

# 4b. Como murio? El codigo de salida y la senal lo dicen todo.
$ ssh operador@$VM 'systemctl show tramontana -p ExecMainStatus -p ExecMainCode \
    -p Result -p StatusErrno'

Y aquí está el árbol de decisión que hace útil este procedimiento, porque cada síntoma tiene una herramienta distinta:

Evidencia Hipótesis Herramienta que la confirma
signal=SYS Filtro seccomp de SystemCallFilter ausyscall <n> sobre el syscall= del journal
apparmor="DENIED" El perfil de AppArmor no contempla una ruta nueva journalctl -k | grep DENIED
status=127 o not found Falta una biblioteca compartida ldd sobre el binario nuevo
status=1 con mensaje propio Configuración: falta una clave en app.conf strace -e trace=%file | grep ENOENT
Killed / oom-kill Excede MemoryMax=512M journalctl -k | grep -i oom
Arranca y muere sin mensaje Cualquiera de las anteriores, silenciada execsnoop + strace -f desde el arranque
# 4c. Bibliotecas: la causa mas comun de un binario nuevo que no arranca
$ ssh operador@$VM 'ldd /opt/tramontana/releases/3.3.0/tramontana | grep -i "not found"'
	libpq.so.6 => not found

# Y si aparece, la confirmacion y la causa raiz
$ ssh operador@$VM 'apt-cache policy libpq5; dpkg -l | grep libpq'
# 4d. Si el journal no dice nada util: trazar el arranque desde el principio
$ ssh operador@$VM 'sudo -u svc-tramontana strace -f -o /tmp/traza.txt \
    /opt/tramontana/releases/3.3.0/tramontana --config /etc/tramontana/app.conf; \
    grep -E "ENOENT|EACCES|EPERM" /tmp/traza.txt | grep -v "lib\|locale" | tail -20'

# 4e. Comparar 3.2.1 con 3.3.0: que ha cambiado de verdad
$ ssh operador@$VM 'diff <(ldd /opt/tramontana/releases/3.2.1/tramontana | sort) \
                         <(ldd /opt/tramontana/releases/3.3.0/tramontana | sort)'
$ ssh operador@$VM 'sudo systemd-analyze security tramontana.service | tail -1'

El paso 4e es el que más rendimiento da y el que suele olvidarse: comparar la versión que funciona con la que no. Si la única diferencia es una biblioteca nueva, ya tienes la respuesta sin trazar nada.

# --- Fase 5: volver al punto de partida y repetir ---
$ kill %1 %2 %3 2>/dev/null    # cerrar los observadores
$ virsh shutdown srv-tramontana-pruebas
$ virsh snapshot-revert srv-tramontana-pruebas --snapshotname base-3.2.1
$ virsh start srv-tramontana-pruebas
$ ssh operador@$VM '~/scripts/revision_salud.sh; echo "estado: $?"'
estado: 0

Tres decisiones de diseño que justifican el procedimiento:

  1. Los observadores se lanzan antes del intento. Un binario que muere en 200 ms no se puede observar a posteriori. Es la diferencia entre tener la evidencia y tener que repetir.
  2. El árbol de decisión está escrito antes de empezar. Bajo presión, la tentación es lanzar strace a todo y ahogarse en salida. Con la tabla delante, cada síntoma lleva a una herramienta concreta.
  3. El snapshot permite intentos ilimitados. Y esa es la razón de ser de la lección: este diagnóstico no se puede hacer en producción. Cada intento fallido deja un release a medio desplegar, un servicio caído y un rollback. En pruebas, snapshot-revert y otra vez.

Nota final: cuando se encuentre la causa, la corrección se aplica y se prueba en pruebas hasta que revision_salud.sh devuelva 0, y solo entonces se despliega en producción. Que desplegar.sh tuviera rollback automático desde 04-07 evitó el desastre en su momento; tener dónde diagnosticar es lo que permite arreglarlo.

Solución 3

Análisis: migrar Tramontana Reservas a virtualización propia con KVM Para: Marta Vidal · De: Operaciones de sistemas · 18 de agosto de 2026

Recomendación breve: no ahora. Mantener el VPS y usar KVM únicamente para el entorno de pruebas, que es donde el beneficio es inmediato y el riesgo nulo. Detallo el razonamiento.


Qué ganaríamos

  1. Control completo de la pila. Podríamos ajustar la CPU de la máquina (host-passthrough), la configuración de discos y el planificador de E/S. Recuerda que en las pruebas de rendimiento de esta semana no pudimos determinar si el cifrado de las copias iba acelerado por hardware, precisamente porque no controlamos esa capa.
  2. Aislamiento fuerte entre servicios. Podríamos separar la base de datos de la aplicación en máquinas distintas, con una frontera de seguridad real entre ellas. Hoy comparten sistema operativo: un compromiso de la aplicación alcanza directamente a la base de datos.
  3. Snapshots antes de cada cambio. Un punto de retorno de segundos, en lugar de depender de la restauración de copias con su RTO de 8 horas.
  4. Coste predecible a partir de cierto volumen. Un servidor propio con capacidad para varias máquinas puede salir más barato que cuatro o cinco VPS.
  5. Sin dependencia del proveedor en cuanto a versiones de kernel, características disponibles o cambios de precio.

Qué perderíamos, y esto es lo determinante

  1. El hardware pasa a ser nuestro problema. Discos, fuentes, memoria, ventiladores. Un disco que falla a las tres de la madrugada hoy lo resuelve el proveedor; con servidor propio lo resolvemos nosotros, y necesitaríamos discos en RAID —recordando que RAID no es una copia de seguridad— y repuestos.
  2. La energía, la refrigeración y la red dejan de estar garantizadas. Un servidor en una oficina depende de un solo circuito eléctrico y de una sola línea de datos. Un corte de luz de dos horas es una caída de dos horas. Un alojamiento con SAI redundante y doble acometida cuesta dinero.
  3. Un único punto de fallo, y uno nuevo: el anfitrión. Hoy tenemos un servidor que puede fallar. Con virtualización propia tendríamos las máquinas virtuales más el anfitrión, y si el anfitrión cae, caen todas a la vez. Esto es importante: la virtualización no da alta disponibilidad, y creer lo contrario es el error de diseño más común del área. Con un solo anfitrión, el riesgo de caída total aumenta.
  4. Aumenta la carga de trabajo, no disminuye. Hoy administramos un sistema operativo. Con virtualización propia administraríamos el anfitrión, el hipervisor, la red virtual, el almacenamiento y cada máquina huésped. Con la plantilla actual, eso es tiempo que sale de otro sitio.
  5. Coste inicial real. Servidor con capacidad y redundancia de discos, más alojamiento adecuado, más los repuestos. No es despreciable, y hay que compararlo con el coste anual del VPS a varios años, no a uno.
  6. Implicaciones de cumplimiento. Tratamos datos personales de huéspedes. Un servidor propio nos convierte en responsables de la seguridad física del equipo, que hoy es del proveedor y que en nuestro modelo de amenazas figura explícitamente como fuera de alcance. Eso habría que revisarlo con el responsable de protección de datos, y probablemente exigiría cifrado completo del disco raíz y control de acceso al espacio físico.

Lo que sí propongo, y ya está en marcha

Usar KVM para el entorno de pruebas, en la propia máquina de administración o en el VPS actual. El beneficio es inmediato y el riesgo, ninguno:

  • Esta semana ha permitido, por primera vez, poder diagnosticar el release 3.3.0 que falló en julio y que quedó sin explicación.
  • Permite probar cambios de configuración y actualizaciones de kernel antes de aplicarlos en producción.
  • Permite ensayar el procedimiento de recuperación del runbook sin tocar el servidor real.
  • Se recrea desde cero en dos minutos con un fichero de configuración versionado en git.

Cuándo reconsiderarlo. El análisis cambia si se dan al menos dos de estas condiciones:

Condición Por qué cambia la decisión
Necesitamos cuatro o más servidores El coste por máquina del servidor propio baja mucho
Hay presupuesto para dos anfitriones Solo entonces la virtualización aporta disponibilidad, no la resta
Hay alojamiento profesional disponible Resuelve energía, refrigeración y red
Hay una persona dedicada a infraestructura La carga de trabajo deja de salir de otro sitio
El coste del VPS supera claramente la alternativa a tres años Es cuando la inversión se amortiza de verdad

Y una alternativa intermedia que merece considerarse antes. Si el objetivo real es separar la base de datos de la aplicación por seguridad, o tener un entorno de preproducción permanente, contratar un segundo VPS consigue eso mismo por una fracción del coste y sin asumir la responsabilidad del hardware. Es un paso reversible; comprar un servidor no lo es.

Resumen para decidir. La virtualización propia es la respuesta correcta cuando el problema es escala y hay recursos para hacerla bien. Nuestro problema hoy no es la escala: es que no teníamos entorno de pruebas —resuelto— y que tenemos un único punto de fallo, que es un problema de redundancia y no de virtualización. Ese segundo asunto lo estoy estudiando y te traeré una propuesta con números.

Conclusión

Ya tienes el entorno de pruebas que el curso llevaba cuatro módulos echando en falta, y entiendes la pila que lo sostiene. Sabes qué virtualiza cada pieza: KVM da acceso a las extensiones de la CPU, QEMU emula los dispositivos, libvirt gestiona el conjunto con una API estable, y virsh es la herramienta del día a día. Sabes que destroy es tirar del cable y no borrar, que virsh console es la vía para recuperar un arranque roto sin interfaz gráfica, y que virtio en el disco y en la red es la diferencia entre una máquina lenta y una rápida. Aprovisionas sin intervención con cloud-init a partir de un fichero versionado en git, eliges qcow2 por sus snapshots y sus discos de respaldo sabiendo lo que cuesta ese 2-5 % de rendimiento, y tienes claro que un snapshot no es una copia de seguridad.

Y de paso has cerrado tres asuntos pendientes. El virbr0 que costaba 35 segundos de arranque en 07-01 tiene ahora una explicación completa: una interfaz que existe pero no tiene enlace hasta que arranca una máquina. La duda de 07-03 sobre AES-NI está resuelta: con host-passthrough, el huésped ve las extensiones de la CPU y cifra a 4,4 GB/s, lo que confirma el factor de cinco a diez que se planteaba como hipótesis. Y el release 3.3.0 que falló en el Módulo 4 tiene por fin un sitio donde reproducirse tantas veces como haga falta, con un snapshot como punto de retorno y un árbol de decisión escrito antes de empezar.

Fíjate ahora en el precio que has pagado por el aislamiento fuerte de las máquinas virtuales. Cada VM lleva su propio kernel, tarda medio minuto en arrancar, ocupa gigabytes en disco y reserva cientos de megabytes de memoria solo por existir. Para un entorno de pruebas eso es perfectamente razonable. Para desplegar una aplicación y su base de datos, y poder recrearlas en segundos, es carísimo. Y en el diagrama del tercer apartado ya viste la alternativa: compartir el kernel del anfitrión y aislar solo lo necesario.

En la lección 07-05: Contenedores de Linux y Docker se construye eso, y empieza por la idea que hay que tener clara antes de escribir el primer docker run: un contenedor no es una máquina pequeña, es un proceso aislado. Verás las tres primitivas del kernel que lo hacen posible —los namespaces, que aíslan lo que un proceso ve; los cgroups v2, que son literalmente la misma tecnología del MemoryMax=512M que pusiste en 05-05; y las capabilities de 05-02—, y reconocerás en los namespaces de montaje al chroot con el que reparaste GRUB en 07-01. Después llega Docker: imágenes y capas, un Dockerfile con construcción en varias etapas para no enviar el compilador a producción, volúmenes frente a bind mounts, redes con resolución por nombre, y un compose.yaml que levanta la aplicación y PostgreSQL juntos con comprobaciones de salud. Con dos avisos que la lección se toma en serio: pertenecer al grupo docker equivale en la práctica a tener root, y aquel kernel.unprivileged_userns_clone que dejaste comentado en 06-06 con una nota explicando por qué — ha llegado el momento de entenderla del todo.

Curso de Linux: De Principiante a Administrador de Sistemas

Módulo 1: Introducción a Linux

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados