cloud-init es el servicio que configura un servidor Linux en su primer arranque: crea usuarios, instala claves SSH, fija el nombre de host, instala paquetes y ejecuta comandos a partir de un archivo que recibe como user-data. Viene preinstalado en las imágenes cloud de Ubuntu, Debian, Rocky Linux y la mayoría de distribuciones, y es lo que usan casi todos los proveedores para preparar cada servidor nuevo. En este tutorial escribirás un archivo cloud-config que deja listo un servidor web con Nginx, un usuario administrador y un cortafuegos, lo validarás y lo probarás en un contenedor LXD en Ubuntu 24.04 antes de usarlo en un servidor real.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor o equipo con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios
sudo. Se usará para validar el archivo y lanzar contenedores de prueba. - Una clave SSH pública (por ejemplo, el contenido de
~/.ssh/id_ed25519.pubde tu equipo). Si no tienes una, créala en tu equipo conssh-keygen -t ed25519. - Unos 5 GB libres en disco para la imagen de prueba de LXD.
Cómo funciona cloud-init
En cada arranque, cloud-init busca una fuente de datos (datasource): el servicio de metadatos del proveedor, un CD con etiqueta cidata, la configuración de LXD, etc. De ella obtiene los metadatos de la instancia y el user-data. Si el identificador de instancia es nuevo, ejecuta sus módulos en cuatro etapas:
| Etapa | Qué hace |
|---|---|
init-local | Localiza la fuente de datos y aplica la configuración de red |
init | Configura el nombre de host, las claves SSH de host, usuarios y grupos |
config | Ejecuta módulos de configuración como write_files o timezone |
final | Instala paquetes, ejecuta runcmd y scripts de usuario |
La mayoría de módulos se ejecutan una sola vez por instancia. Por eso, si cambias el user-data de un servidor ya creado, cloud-init no lo vuelve a aplicar: está pensado para el primer arranque, no para gestionar la configuración a lo largo del tiempo.
El user-data puede ser un script (empieza por #!) o, lo más habitual, un documento YAML cloud-config, que debe empezar exactamente por la línea #cloud-config.
Paso 1: Escribir el archivo cloud-config
Crea un directorio de trabajo y el archivo:
mkdir -p ~/cloud-init && cd ~/cloud-init
nano user-data.yaml
Pega el siguiente contenido y sustituye la clave SSH por la tuya:
#cloud-config
hostname: web01
timezone: Europe/Madrid
users:
- name: deploy
gecos: Usuario de despliegue
groups: [sudo]
shell: /bin/bash
sudo: "ALL=(ALL) NOPASSWD:ALL"
lock_passwd: true
ssh_authorized_keys:
- ssh-ed25519 AAAA... tu_usuario@tu_equipo
ssh_pwauth: false
disable_root: true
package_update: true
package_upgrade: true
packages:
- nginx
- ufw
- fail2ban
write_files:
- path: /etc/ssh/sshd_config.d/10-hardening.conf
permissions: "0644"
content: |
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
- path: /var/www/html/index.html
permissions: "0644"
defer: true
content: |
<h1>web01 aprovisionado con cloud-init</h1>
runcmd:
- ufw allow OpenSSH
- ufw allow "Nginx Full"
- ufw --force enable
- systemctl enable --now nginx fail2ban
- systemctl try-reload-or-restart ssh
final_message: "cloud-init terminó en $UPTIME segundos"
Qué hace cada bloque:
hostnameytimezone: nombre de host y zona horaria.users: creadeploycon sudo sin contraseña y acceso solo por clave. Al definirusers, el usuario por defecto de la imagen (ubuntu) ya no se crea; si lo quieres también, añade- defaultcomo primer elemento de la lista.ssh_pwauthydisable_root: desactivan el acceso SSH por contraseña y el login como root.package_update,package_upgradeypackages: ejecutanapt update, actualizan el sistema e instalan los paquetes indicados.write_files: crea archivos con su contenido y permisos.defer: trueretrasa la escritura hasta la etapafinal, después de instalar paquetes; sin eso, el paquetenginxsobrescribiría elindex.html.runcmd: comandos que se ejecutan una vez, al final, como root. Cada elemento se ejecuta consh, así que escribe comandos simples o usa listas (- [systemctl, restart, nginx]) para evitar problemas de comillas.final_message: aparece en el log cuando todo ha terminado.
Nota
sudo: "ALL=(ALL) NOPASSWD:ALL"es cómodo para automatización. Si prefieres quesudopida contraseña, quita esa línea y asigna una contraseña al usuario después del primer acceso.
Paso 2: Validar el archivo
Un error de indentación en YAML hace que cloud-init ignore partes del archivo sin detener el arranque, así que valídalo siempre antes de usarlo. Ubuntu 24.04 incluye cloud-init con el comando schema:
cloud-init schema --config-file user-data.yaml --annotate
Valid schema user-data.yaml
Si hay un error, --annotate lo marca junto a la línea afectada. Por ejemplo, con package_upgrade: si en lugar de un booleano:
package_upgrade: si # E1
...
# Errors: -------------
# E1: 'si' is not of type 'boolean'
Paso 3: Preparar LXD para las pruebas
LXD puede crear contenedores de Ubuntu con cloud-init en segundos y pasarles tu user-data, así que es la forma más rápida de probar sin crear servidores reales. Instálalo con snap y aplica la configuración por defecto:
sudo snap install lxd
sudo usermod -aG lxd "$USER"
newgrp lxd
lxd init --auto
Comprueba que funciona:
lxc list
+------+-------+------+------+------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------+-------+------+------+------+-----------+
Paso 4: Lanzar un contenedor con el user-data
Crea un contenedor Ubuntu 24.04 y pásale el archivo con la clave cloud-init.user-data:
lxc launch ubuntu:24.04 web01 --config=cloud-init.user-data="$(cat user-data.yaml)"
Espera a que cloud-init termine. El comando se queda bloqueado hasta entonces:
lxc exec web01 -- cloud-init status --wait --long
.....................
status: done
extended_status: done
boot_status_code: enabled-by-generator
last_update: Thu, 25 Sep 2026 10:42:18 +0000
detail: DataSourceLXD
errors: []
recoverable_errors: {}
status: done con errors: [] indica que todos los módulos terminaron bien. Si ves status: error, pasa a la sección de depuración.
Paso 5: Verificar el resultado
Comprueba cada parte de la configuración dentro del contenedor.
Nombre de host, zona horaria y usuario:
lxc exec web01 -- hostname
lxc exec web01 -- timedatectl show -p Timezone --value
lxc exec web01 -- id deploy
web01
Europe/Madrid
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo)
Cortafuegos y servicios:
lxc exec web01 -- ufw status
lxc exec web01 -- systemctl is-active nginx fail2ban
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Nginx Full ALLOW Anywhere
...
active
active
Página web desde el host. Obtén la IP del contenedor y haz una petición:
IP=$(lxc list web01 -c 4 --format csv | cut -d' ' -f1)
curl -s "http://$IP"
<h1>web01 aprovisionado con cloud-init</h1>
Por último, entra por SSH con tu clave como deploy. Desde el propio host, si la clave privada está ahí, o desde tu equipo si el host enruta hacia el contenedor:
ssh deploy@"$IP"
Paso 6: Depurar cloud-init
Cuando algo no se aplica, estas son las fuentes de información, de más a menos útil:
lxc exec web01 -- cat /var/log/cloud-init-output.log
cloud-init-output.log contiene la salida de apt, de runcmd y de los scripts. Es donde aparecen los errores de tus comandos.
lxc exec web01 -- grep -E "WARNING|ERROR|Traceback" /var/log/cloud-init.log
cloud-init.log es el log interno de cloud-init: qué módulos se ejecutaron, cuáles fallaron y por qué.
Para ver el user-data que realmente recibió la instancia (útil si sospechas que la fuente de datos no lo entregó):
lxc exec web01 -- cloud-init query userdata
Y para saber cuánto tardó cada etapa y módulo:
lxc exec web01 -- cloud-init analyze blame
-- Boot Record 01 --
41.20300s (modules-final/config-package_update_upgrade_install)
03.11200s (modules-final/config-scripts_user)
...
Volver a ejecutar cloud-init
Mientras ajustas el archivo, lo más rápido es borrar el contenedor y crear otro:
lxc delete --force web01
En un servidor real puedes forzar que cloud-init se ejecute de nuevo como si fuera el primer arranque. Esto vuelve a aplicar todos los módulos con el user-data actual del proveedor y reinicia el servidor, así que úsalo solo en servidores de prueba:
sudo cloud-init clean --logs --reboot
Paso 7: Usar el archivo en un servidor real
El mismo archivo sirve para cualquier plataforma que admita user-data:
- Proveedores cloud: pégalo en el campo de user-data o datos de usuario al crear el servidor, si el proveedor lo ofrece.
- Terraform y Pulumi: la mayoría de proveedores tienen un argumento
user_data(por ejemplo,user_data = file("user-data.yaml")). - Máquinas virtuales locales o Proxmox: genera un disco NoCloud con
cloud-localds seed.iso user-data.yaml(paquetecloud-image-utils) y adjúntalo como CD-ROM a una cloud image.
Antes de desplegarlo, recuerda dos límites:
- Muchos proveedores limitan el tamaño del user-data (16 KB es habitual). Si crece mucho, deja que cloud-init solo prepare el acceso y lance una herramienta de configuración como Ansible.
- El user-data suele ser legible desde dentro del servidor a través del servicio de metadatos. No incluyas contraseñas ni tokens en él.
Solución de problemas
El archivo se ignora por completo: la primera línea no es exactamente #cloud-config (sin espacios delante ni después de #). Compruébalo con cloud-init query userdata.
Algunos módulos no se aplican y no hay error visible: suele ser una clave mal escrita o mal indentada. Ejecuta cloud-init schema --config-file user-data.yaml --annotate y, dentro de la instancia, sudo cloud-init schema --system --annotate para validar lo que recibió.
El index.html es el de Nginx por defecto: falta defer: true en write_files, y el paquete sobrescribió el archivo.
No puedes entrar por SSH con el usuario ubuntu: al definir users sin - default, ese usuario no se crea. Usa deploy o añade - default a la lista.
Conclusión
Has escrito un archivo cloud-config que deja un servidor listo con usuario administrador, SSH endurecido, paquetes actualizados, Nginx y cortafuegos, lo has validado con cloud-init schema y lo has probado en LXD en menos de un minuto por iteración. Como siguientes pasos, pasa el mismo archivo como user_data desde Terraform, combina cloud-init para el primer arranque con Ansible para la configuración continua, y usa plantillas Jinja (primera línea ## template: jinja) para adaptar el archivo a los metadatos de cada instancia.
