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/kvm disponible.
  • 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.
  • kubectl con 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ónComando
Parar la VMvirtctl stop ubuntu-vm-01 -n vms
Arrancarlavirtctl start ubuntu-vm-01 -n vms
Reiniciarlavirtctl 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 VMvirtctl 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.