Packer es una herramienta de HashiCorp que construye imágenes de máquina a partir de una plantilla en código: arranca una máquina temporal, la configura con scripts o herramientas como Ansible y guarda el resultado como imagen reutilizable. Así cada servidor nuevo parte de una base idéntica, ya actualizada y con tu software instalado, en lugar de configurarse a mano. En este tutorial usarás Packer con el builder QEMU para generar una imagen qcow2 de Ubuntu 24.04 a partir de la cloud image oficial de Canonical, y la arrancarás para comprobar que funciona.

Requisitos previos

Para seguir esta guía necesitas:

  • Un equipo o servidor con Ubuntu 24.04 LTS y un usuario no root con privilegios sudo.
  • Virtualización por hardware disponible (KVM). En un equipo físico suele bastar con tenerla activada en la BIOS; en un VPS depende de que el proveedor ofrezca virtualización anidada. El paso 1 te enseña a comprobarlo.
  • Al menos 4 GB de RAM y 15 GB libres en disco.
  • Conocimientos básicos de cloud-init, que se usa para dar acceso SSH a Packer durante la construcción.

Paso 1: Comprobar KVM e instalar QEMU

Packer arranca una máquina virtual durante la construcción. Sin KVM funcionaría con emulación por software, pero sería decenas de veces más lento. Instala cpu-checker y comprueba la aceleración:

sudo apt update
sudo apt install cpu-checker
kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used

Si ves KVM acceleration can NOT be used, activa la virtualización en la BIOS o usa otra máquina antes de continuar.

Instala QEMU, las utilidades de discos y xorriso, que Packer usa para generar la ISO de configuración de cloud-init:

sudo apt install qemu-system-x86 qemu-utils xorriso

Añade tu usuario al grupo kvm para usar /dev/kvm sin sudo y vuelve a abrir la sesión:

sudo usermod -aG kvm "$USER"

Tras volver a entrar, comprueba que perteneces al grupo:

id -nG | grep -w kvm

Paso 2: Instalar Packer

Instala Packer desde el repositorio oficial de HashiCorp. Descarga la clave del repositorio:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg

Añade el repositorio:

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(. /etc/os-release && echo "$VERSION_CODENAME") main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

Instala Packer:

sudo apt update
sudo apt install packer

Comprueba la instalación:

packer version
Packer v1.14.2

Paso 3: Preparar el proyecto

Crea el directorio del proyecto con una carpeta para los scripts de aprovisionamiento:

mkdir -p ~/packer-ubuntu/scripts && cd ~/packer-ubuntu

La cloud image de Ubuntu no tiene contraseña para ningún usuario: espera que cloud-init le inyecte claves o credenciales en el primer arranque. Packer aprovecha esto adjuntando un pequeño CD con etiqueta cidata (el formato NoCloud de cloud-init) que fija una contraseña temporal para el usuario ubuntu. Crea la plantilla de ese user-data:

nano user-data.pkrtpl
#cloud-config
password: ${password}
chpasswd:
  expire: false
ssh_pwauth: true

${password} se sustituye en tiempo de construcción con el valor de una variable de Packer, así la contraseña no queda escrita en ningún archivo del proyecto. Al final de la construcción se bloquea esa contraseña, de modo que la imagen final no permite entrar con ella.

Paso 4: Escribir la plantilla de Packer

Crea la plantilla principal:

nano ubuntu.pkr.hcl
packer {
  required_plugins {
    qemu = {
      source  = "github.com/hashicorp/qemu"
      version = ">= 1.1.0"
    }
  }
}

variable "build_password" {
  type      = string
  sensitive = true
}

variable "image_name" {
  type    = string
  default = "ubuntu-2404-base"
}

source "qemu" "ubuntu" {
  iso_url      = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
  iso_checksum = "file:https://cloud-images.ubuntu.com/noble/current/SHA256SUMS"
  disk_image   = true

  disk_size        = "10G"
  format           = "qcow2"
  disk_compression = true
  accelerator      = "kvm"
  cpus             = 2
  memory           = 2048
  headless         = true

  output_directory = "output-${var.image_name}"
  vm_name          = "${var.image_name}.qcow2"

  cd_label = "cidata"
  cd_content = {
    "meta-data" = "instance-id: packer-build\nlocal-hostname: packer\n"
    "user-data" = templatefile("${path.root}/user-data.pkrtpl", { password = var.build_password })
  }

  ssh_username     = "ubuntu"
  ssh_password     = var.build_password
  ssh_timeout      = "15m"
  shutdown_command = "sudo shutdown -P now"
}

build {
  sources = ["source.qemu.ubuntu"]

  provisioner "shell" {
    execute_command  = "chmod +x {{ .Path }}; sudo env {{ .Vars }} {{ .Path }}"
    environment_vars = ["DEBIAN_FRONTEND=noninteractive"]
    scripts = [
      "scripts/base.sh",
      "scripts/cleanup.sh",
    ]
  }
}

Las partes importantes:

  • disk_image = true indica que iso_url apunta a un disco ya instalado (la cloud image) y no a una ISO de instalación, así que no hace falta automatizar ningún instalador.
  • iso_checksum = "file:..." hace que Packer descargue el archivo SHA256SUMS oficial y verifique la imagen.
  • disk_size amplía el disco a 10 GB. cloud-init ampliará la partición raíz en el primer arranque de cada servidor.
  • cd_content genera la ISO cidata con meta-data y user-data; es lo que requiere xorriso.
  • execute_command ejecuta cada script como root con las variables de environment_vars, de modo que los scripts no necesitan sudo en cada línea.

Paso 5: Escribir los scripts de aprovisionamiento

El primer script espera a que cloud-init termine (si no, apt puede estar bloqueado), actualiza el sistema e instala los paquetes que quieres en todos tus servidores:

nano scripts/base.sh
#!/usr/bin/env bash
set -euo pipefail

cloud-init status --wait

apt-get update
apt-get -y full-upgrade
apt-get -y install qemu-guest-agent ufw fail2ban unattended-upgrades

systemctl enable qemu-guest-agent

ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH

Las reglas de UFW quedan guardadas pero el cortafuegos no se activa aquí, para no cortar la conexión SSH de Packer. Actívalo en cada servidor con sudo ufw enable o desde tu herramienta de configuración.

El segundo script deja la imagen limpia para que cada servidor creado a partir de ella se comporte como una instancia nueva:

nano scripts/cleanup.sh
#!/usr/bin/env bash
set -euo pipefail

# Bloquear la contraseña temporal de la construcción
passwd -l ubuntu

# Quitar la configuración SSH con contraseña que generó cloud-init
rm -f /etc/ssh/sshd_config.d/50-cloud-init.conf

# Borrar claves de host SSH: cada servidor generará las suyas
rm -f /etc/ssh/ssh_host_*

# Vaciar el machine-id para que no se repita entre servidores
truncate -s 0 /etc/machine-id

# Reiniciar cloud-init para que vuelva a ejecutarse en el primer arranque
cloud-init clean --logs --seed

apt-get -y autoremove --purge
apt-get clean

Borrar machine-id y las claves de host es imprescindible: si no, todos los servidores de la imagen tendrían la misma identidad y las mismas claves SSH. En Ubuntu 24.04, cloud-init regenera las claves de host y systemd un machine-id nuevo en el primer arranque.

Paso 6: Inicializar y validar la plantilla

Descarga el plugin de QEMU declarado en el bloque packer:

packer init .
Installed plugin github.com/hashicorp/qemu v1.1.4 in "/home/tu_usuario/.config/packer/plugins/github.com/hashicorp/qemu/packer-plugin-qemu_v1.1.4_x5.0_linux_amd64"

Da formato a los archivos y valida la sintaxis:

packer fmt .
packer validate -var "build_password=temporal" .
The configuration is valid.

Paso 7: Construir la imagen

Genera una contraseña aleatoria para la construcción y pásala como variable de entorno. Packer lee automáticamente las variables PKR_VAR_<nombre>:

export PKR_VAR_build_password="$(openssl rand -base64 18)"
packer build .

La construcción descarga la cloud image (unos 600 MB), arranca la máquina, espera a SSH, ejecuta los scripts y apaga la máquina. Tarda entre 5 y 15 minutos según tu conexión y disco:

qemu.ubuntu: output will be in this color.

==> qemu.ubuntu: Retrieving ISO
==> qemu.ubuntu: Starting VM, booting disk image
==> qemu.ubuntu: Waiting for SSH to become available...
==> qemu.ubuntu: Connected to SSH!
==> qemu.ubuntu: Provisioning with shell script: scripts/base.sh
...
==> qemu.ubuntu: Provisioning with shell script: scripts/cleanup.sh
==> qemu.ubuntu: Gracefully halting virtual machine...
==> qemu.ubuntu: Converting hard drive...
Build 'qemu.ubuntu' finished after 8 minutes 41 seconds.

==> Builds finished. The artifacts of successful builds are:
--> qemu.ubuntu: VM files in directory: output-ubuntu-2404-base

Comprueba la imagen generada:

qemu-img info output-ubuntu-2404-base/ubuntu-2404-base.qcow2
image: output-ubuntu-2404-base/ubuntu-2404-base.qcow2
file format: qcow2
virtual size: 10 GiB (10737418240 bytes)
disk size: 1.1 GiB
...

Paso 8: Probar la imagen

Arranca una copia de la imagen con cloud-init, tal como lo haría un proveedor, para confirmar que acepta tu clave SSH. Instala cloud-image-utils, que incluye cloud-localds:

sudo apt install cloud-image-utils

Crea un disco de prueba que usa la imagen como base, sin modificarla:

qemu-img create -f qcow2 -F qcow2 -b output-ubuntu-2404-base/ubuntu-2404-base.qcow2 prueba.qcow2

Crea un user-data de prueba con tu clave pública. Si no tienes una, genérala con ssh-keygen -t ed25519:

nano prueba-user-data.yaml
#cloud-config
ssh_authorized_keys:
  - ssh-ed25519 AAAA... tu_usuario@tu_equipo

Sustituye la línea por el contenido de tu ~/.ssh/id_ed25519.pub. Genera la ISO de cloud-init y arranca la máquina con el puerto SSH redirigido al 2222 local:

cloud-localds seed.iso prueba-user-data.yaml
qemu-system-x86_64 -enable-kvm -m 2048 -smp 2 -nographic \
  -drive file=prueba.qcow2,if=virtio \
  -drive file=seed.iso,format=raw,if=virtio \
  -nic user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:2222-:22

Cuando aparezca el prompt de login, conéctate desde otra terminal:

ssh -p 2222 [email protected] 'systemctl is-enabled qemu-guest-agent; sudo ufw show added'
enabled
Added user rules (see 'ufw status' for running firewall):
ufw allow OpenSSH

Apaga la máquina de prueba con sudo poweroff dentro de ella (o Ctrl+A y después X en la consola de QEMU) y borra los archivos de prueba:

rm prueba.qcow2 seed.iso

Solución de problemas

Waiting for SSH to become available... hasta agotar el tiempo: cloud-init no aplicó la contraseña. Comprueba que xorriso está instalado, que cd_label es exactamente cidata y que user-data.pkrtpl empieza por #cloud-config. Para ver la consola, cambia temporalmente headless = false en un equipo con escritorio o añade -on-error=ask a packer build para inspeccionar la máquina antes de que se borre.

Could not access KVM kernel module: Permission denied: tu usuario no está en el grupo kvm en la sesión actual. Cierra sesión y vuelve a entrar.

Could not get lock /var/lib/dpkg/lock-frontend: algún proceso de apt seguía activo. Asegúrate de que base.sh empieza con cloud-init status --wait.

Artefacto existente: Packer se niega a sobrescribir output-ubuntu-2404-base. Bórralo o ejecuta packer build -force ..

Conclusión

Tienes una plantilla de Packer que genera, desde la cloud image oficial y verificada, una imagen qcow2 de Ubuntu 24.04 actualizada, con tus paquetes base y limpia para clonarse sin identidades repetidas. Como siguientes pasos, guarda la plantilla en Git y construye la imagen desde CI de forma periódica para tener siempre los parches al día, sustituye los scripts por el provisioner de Ansible cuando la configuración crezca, y sube la imagen resultante a tu hipervisor (por ejemplo, Proxmox con qm importdisk) o a tu proveedor.