KubeVirt es un proyecto de la CNCF que añade a Kubernetes los recursos necesarios para ejecutar máquinas virtuales KVM junto a los contenedores. Cada VM corre dentro de un pod, así que usa las mismas redes, Services, almacenamiento y herramientas que el resto del clúster. Es útil para cargas que todavía no se pueden contenerizar (sistemas heredados, Windows, appliances) o para unificar VMs y contenedores en una sola plataforma. En este tutorial instalarás KubeVirt y CDI, crearás una VM Ubuntu 24.04 configurada con cloud-init, la publicarás con un Service y la migrarás en caliente entre nodos.
Requisitos previos
Para seguir esta guía necesitas:
- Un clúster Kubernetes en una de las versiones soportadas por la release actual de KubeVirt (normalmente las tres últimas menores), con nodos Ubuntu 24.04 LTS x86_64.
- Virtualización por hardware en los nodos que ejecutarán VMs: servidores bare metal o VPS con virtualización anidada activada, con
/dev/kvmdisponible. - Al menos 4 vCPU y 8 GB de RAM libres en esos nodos para la VM de ejemplo y los componentes de KubeVirt.
- Una StorageClass predeterminada en el clúster (por ejemplo Longhorn). Para la migración en caliente del paso 7, la StorageClass debe admitir
ReadWriteMany. kubectlcon permisos de administrador del clúster y un par de claves SSH en tu equipo (~/.ssh/id_ed25519.pub).
Paso 1: Comprobar el soporte de virtualización
En cada nodo donde vayan a correr VMs, comprueba que la CPU expone las extensiones de virtualización y que existe el dispositivo KVM:
grep -Ec '(vmx|svm)' /proc/cpuinfo
ls -l /dev/kvm
4
crw-rw---- 1 root kvm 10, 232 Sep 25 09:14 /dev/kvm
Un número mayor que 0 indica que la CPU tiene VT-x (vmx) o AMD-V (svm). Si el resultado es 0 o /dev/kvm no existe, el nodo no puede ejecutar VMs aceleradas: activa la virtualización en la BIOS o pide a tu proveedor la virtualización anidada. Para pruebas sin KVM consulta la sección de solución de problemas.
Paso 2: Instalar KubeVirt
KubeVirt se instala en dos partes: un operador y un recurso KubeVirt que le indica que despliegue los componentes. Obtén la última versión estable publicada por el proyecto:
export KUBEVIRT_VERSION=$(curl -s https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt)
echo "$KUBEVIRT_VERSION"
Aplica el operador y el recurso personalizado:
kubectl apply -f "https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-operator.yaml"
kubectl apply -f "https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-cr.yaml"
Espera a que la instalación termine. Tarda unos minutos porque despliega virt-api, virt-controller y un virt-handler por nodo:
kubectl -n kubevirt wait kv kubevirt --for condition=Available --timeout=10m
kubevirt.kubevirt.io/kubevirt condition met
Comprueba que los nodos anuncian el dispositivo KVM a Kubernetes. Sustituye worker-1 por el nombre de uno de tus nodos:
kubectl get node worker-1 -o jsonpath='{.status.allocatable.devices\.kubevirt\.io/kvm}{"\n"}'
1k
Si la salida está vacía, virt-handler no ha encontrado /dev/kvm en ese nodo.
Paso 3: Instalar CDI y virtctl
CDI (Containerized Data Importer) descarga imágenes de disco desde una URL o un registro y las vuelca en un PVC. Es lo que usarás para convertir la imagen cloud de Ubuntu en el disco de la VM:
export CDI_VERSION=$(basename $(curl -s -w '%{redirect_url}' https://github.com/kubevirt/containerized-data-importer/releases/latest))
kubectl apply -f "https://github.com/kubevirt/containerized-data-importer/releases/download/${CDI_VERSION}/cdi-operator.yaml"
kubectl apply -f "https://github.com/kubevirt/containerized-data-importer/releases/download/${CDI_VERSION}/cdi-cr.yaml"
Espera a que CDI esté disponible:
kubectl wait cdi cdi --for condition=Available --timeout=10m
virtctl es el cliente para operaciones que kubectl no cubre: consola serie, VNC, SSH, arranque y migración. Descarga la versión que coincide con KubeVirt:
curl -L -o virtctl "https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/virtctl-${KUBEVIRT_VERSION}-linux-amd64"
sudo install -m 755 virtctl /usr/local/bin/virtctl
rm virtctl
virtctl version
Paso 4: Crear una máquina virtual Ubuntu 24.04
Crea un namespace para las VMs:
kubectl create namespace vms
La VM se define con un recurso VirtualMachine. Su bloque dataVolumeTemplates pide a CDI que cree un PVC de 20 GiB y descargue en él la imagen cloud de Ubuntu 24.04; el volumen cloudinitdisk configura el hostname, tu clave SSH y el agente de invitado en el primer arranque.
nano vm-ubuntu.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: ubuntu-vm-01
namespace: vms
spec:
runStrategy: Always
dataVolumeTemplates:
- metadata:
name: ubuntu-vm-01-root
spec:
storage:
resources:
requests:
storage: 20Gi
source:
http:
url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
template:
metadata:
labels:
app: ubuntu-vm-01
spec:
domain:
cpu:
cores: 2
resources:
requests:
memory: 2Gi
devices:
disks:
- name: rootdisk
disk:
bus: virtio
- name: cloudinitdisk
disk:
bus: virtio
interfaces:
- name: default
masquerade: {}
networks:
- name: default
pod: {}
volumes:
- name: rootdisk
dataVolume:
name: ubuntu-vm-01-root
- name: cloudinitdisk
cloudInitNoCloud:
userData: |
#cloud-config
hostname: ubuntu-vm-01
ssh_authorized_keys:
- ssh-ed25519 AAAA... tu_clave_publica
package_update: true
packages:
- qemu-guest-agent
- nginx
runcmd:
- systemctl enable --now qemu-guest-agent
Sustituye ssh-ed25519 AAAA... tu_clave_publica por el contenido de tu ~/.ssh/id_ed25519.pub. Las claves de ssh_authorized_keys se añaden al usuario por defecto de la imagen, que en Ubuntu es ubuntu. Con runStrategy: Always, KubeVirt arranca la VM en cuanto el disco esté listo y la vuelve a arrancar si se para.
kubectl apply -f vm-ubuntu.yaml
Sigue la importación del disco. El DataVolume pasa por ImportInProgress y termina en Succeeded:
kubectl -n vms get datavolume -w
NAME PHASE PROGRESS RESTARTS AGE
ubuntu-vm-01-root ImportInProgress 47.12% 35s
ubuntu-vm-01-root Succeeded 100.0% 1m20s
Pulsa Ctrl+C y comprueba que la VM está en marcha. vm es la definición; vmi (VirtualMachineInstance) es la instancia que se está ejecutando, con su IP y su nodo:
kubectl -n vms get vm,vmi
NAME AGE STATUS READY
virtualmachine.kubevirt.io/ubuntu-vm-01 2m Running True
NAME AGE PHASE IP NODENAME READY
virtualmachineinstance.kubevirt.io/ubuntu-vm-01 40s Running 10.42.1.37 worker-2 True
Paso 5: Acceder a la VM
Conéctate a la consola serie para ver el arranque y el final de cloud-init. Sal con Ctrl+]:
virtctl console ubuntu-vm-01 -n vms
Para trabajar en la VM es más cómodo SSH. virtctl ssh crea un túnel a través de la API de Kubernetes, así que no necesitas exponer el puerto 22:
virtctl ssh -n vms -i ~/.ssh/id_ed25519 ubuntu@vm/ubuntu-vm-01
Una vez dentro, comprueba que cloud-init ha terminado y que nginx está activo:
cloud-init status
systemctl is-active nginx
status: done
active
Con el agente de invitado en marcha, KubeVirt obtiene del sistema invitado datos como sus interfaces de red y su versión, que puedes consultar desde fuera:
kubectl -n vms get vmi ubuntu-vm-01 -o jsonpath='{.status.guestOSInfo.prettyName}{"\n"}'
La salida debe mostrar el nombre completo de la versión de Ubuntu 24.04 instalada en la VM. Si está vacía, el agente todavía no ha arrancado.
Paso 6: Publicar la VM con un Service
Como la VM corre en un pod con las etiquetas de template.metadata.labels, un Service normal puede seleccionarla. Así, los pods del clúster pueden acceder a ella por DNS igual que a cualquier otro servicio, lo que permite combinar VMs y contenedores en una misma aplicación:
nano vm-service.yaml
apiVersion: v1
kind: Service
metadata:
name: ubuntu-vm-01-http
namespace: vms
spec:
selector:
app: ubuntu-vm-01
ports:
- name: http
port: 80
targetPort: 80
type: ClusterIP
kubectl apply -f vm-service.yaml
Prueba el acceso desde un pod temporal. El nombre ubuntu-vm-01-http.vms.svc.cluster.local es el que usaría cualquier aplicación del clúster:
kubectl run prueba --rm -it --image=curlimages/curl --restart=Never -- \
curl -sI http://ubuntu-vm-01-http.vms.svc.cluster.local
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Para acceso desde fuera del clúster, cambia type a LoadBalancer (si tienes un balanceador como MetalLB) o expón el Service con un Ingress.
Paso 7: Migrar la VM en caliente
La migración en caliente mueve una VM en ejecución a otro nodo copiando su memoria, sin reiniciar el sistema invitado. Es lo que permite vaciar un nodo para mantenimiento. Tiene dos condiciones: el disco debe estar en un PVC ReadWriteMany para que ambos nodos lo vean a la vez, y la red debe usar masquerade (o bridge sobre una red secundaria), como en este ejemplo.
Comprueba si KubeVirt considera la VM migrable:
kubectl -n vms get vmi ubuntu-vm-01 -o jsonpath='{.status.conditions[?(@.type=="LiveMigratable")].status}{"\n"}'
Si devuelve False, consulta el motivo con kubectl -n vms describe vmi ubuntu-vm-01; lo habitual es que el PVC sea ReadWriteOnce. En ese caso, crea la VM con una StorageClass que ofrezca ReadWriteMany en modo bloque o sistema de archivos.
Si devuelve True, anota el nodo actual e inicia la migración:
kubectl -n vms get vmi ubuntu-vm-01 -o jsonpath='{.status.nodeName}{"\n"}'
virtctl migrate ubuntu-vm-01 -n vms
virtctl migrate crea un recurso VirtualMachineInstanceMigration. Sigue su progreso:
kubectl -n vms get vmim -w
NAME PHASE VMI
kubevirt-migrate-vm-8xk2p Scheduling ubuntu-vm-01
kubevirt-migrate-vm-8xk2p Running ubuntu-vm-01
kubevirt-migrate-vm-8xk2p Succeeded ubuntu-vm-01
Comprueba que la VM está ahora en otro nodo y que la sesión SSH o el Service siguen respondiendo:
kubectl -n vms get vmi ubuntu-vm-01 -o jsonpath='{.status.nodeName}{"\n"}'
Para que kubectl drain migre la VM en lugar de apagarla al vaciar un nodo, añade evictionStrategy: LiveMigrate en spec.template.spec de la VirtualMachine.
Operaciones habituales con virtctl
| Acción | Comando |
|---|---|
| Parar la VM | virtctl stop ubuntu-vm-01 -n vms |
| Arrancarla | virtctl start ubuntu-vm-01 -n vms |
| Reiniciarla | virtctl restart ubuntu-vm-01 -n vms |
| Consola gráfica (requiere un visor VNC local) | virtctl vnc ubuntu-vm-01 -n vms |
| Copiar un archivo a la VM | virtctl scp -n vms -i ~/.ssh/id_ed25519 archivo.txt ubuntu@vm/ubuntu-vm-01:/tmp/ |
Ten en cuenta que con runStrategy: Always, virtctl stop cambia la estrategia a Halted para que la VM no vuelva a arrancar sola; virtctl start la pone de nuevo en marcha.
Solución de problemas
La VMI se queda en Scheduling o el pod virt-launcher en Pending. Revisa los eventos con kubectl -n vms describe vmi ubuntu-vm-01. Si mencionan devices.kubevirt.io/kvm, ningún nodo tiene KVM disponible (repite el paso 1). Si mencionan memoria o CPU, no hay nodos con recursos libres suficientes.
El DataVolume falla o no avanza. Consulta los logs del pod importador, que se llama importer-<nombre-del-datavolume>:
kubectl -n vms logs -f importer-ubuntu-vm-01-root
Los fallos habituales son una URL inaccesible desde el clúster o que no haya StorageClass predeterminada, en cuyo caso el PVC queda en Pending.
Nodos sin KVM (solo para pruebas). KubeVirt puede usar emulación por software, mucho más lenta y no apta para producción:
kubectl -n kubevirt patch kubevirt kubevirt --type=merge \
-p '{"spec":{"configuration":{"developerConfiguration":{"useEmulation":true}}}}'
No puedes entrar por SSH. Confirma en la consola serie que cloud-init terminó sin errores (cloud-init status --long) y que la clave pública del manifiesto es exactamente la de tu id_ed25519.pub. cloud-init solo aplica la configuración en el primer arranque, así que si cambias la clave tendrás que recrear la VM.
Conclusión
Tienes KubeVirt y CDI funcionando, una VM Ubuntu 24.04 creada de forma declarativa con cloud-init, accesible por consola, SSH y un Service de Kubernetes, y has visto qué necesita para migrar en caliente entre nodos. Como siguientes pasos, prueba a importar una imagen propia en un DataVolume y clonarla para crear varias VMs, añade una segunda red con Multus para conectar las VMs a una VLAN existente, o gestiona las definiciones de VMs con Argo CD como cualquier otro manifiesto.
