cloud-init: configuración automática de instancias cloud

Cuando lanzas una instancia en cualquier proveedor cloud —AWS, GCP, Azure, DigitalOcean, Hetzner— lo que convierte una imagen genérica de Debian en tu servidor configurado es cloud-init. No es una herramienta del proveedor: es un proyecto independiente que todos los proveedores adoptaron como estándar de facto. La imagen arranca, cloud-init se ejecuta una sola vez, y cuando terminas de conectarte por SSH el servidor ya tiene tus paquetes, tus usuarios y tu configuración.

El mecanismo central es user data: un bloque de texto que el proveedor expone a la instancia a través de una URL de metadatos (http://169.254.169.254/latest/user-data en AWS, misma IP en GCP y la mayoría de proveedores). cloud-init recupera ese texto durante el arranque y lo interpreta. Si el texto empieza por #cloud-config, cloud-init lo trata como YAML estructurado con directivas declarativas. Si empieza por #!, lo ejecuta como script de shell. El formato #cloud-config es el útil para el 90% de los casos: describes el estado deseado, no los pasos para llegar a él.

¿Cuándo usarlo? Siempre que automatices el despliegue de instancias cloud. Si estás lanzando servidores a mano uno a uno sin user data, estás haciendo configuración manual que no se puede reproducir. cloud-init no reemplaza a Ansible ni a Terraform para configuración compleja, pero sí cubre el bootstrap inicial: lo que necesitas que esté listo antes de que llegue tu herramienta de configuración, o en instancias simples que no justifican más infraestructura.

Lo que rompe si lo haces mal: cloud-init solo se ejecuta una vez en la vida de la instancia, en el primer arranque. Si hay un error en tu #cloud-config —YAML mal formado, un paquete que no existe, un comando que falla— la instancia arranca con una configuración incompleta y no hay forma de volver a ejecutar cloud-init sin intervención manual o recrear la instancia. Un error silencioso en user data es especialmente traicionero porque la instancia parece arrancar bien.

Vale la pena separarlo de dos herramientas con las que se confunde: preseed (Debian) y autoinstall (Ubuntu) operan durante el instalador, antes de que el sistema exista. cloud-init opera en el primer arranque de un sistema ya instalado. Las imágenes cloud no pasan por el instalador; llegan ya instaladas, y cloud-init hace el trabajo de personalización.

#cloud-config

# El hostname que verá el sistema y el FQDN en /etc/hosts
hostname: web-prod-01
fqdn: web-prod-01.ejemplo.com
manage_etc_hosts: true

# Paquetes a instalar en el primer arranque.
# cloud-init ejecuta apt-get update antes de instalar.
package_update: true
package_upgrade: false   # No actualizamos todo en el primer boot; eso lo gestiona Ansible
packages:
  - nginx
  - ufw
  - fail2ban
  - curl

# Crear un usuario de despliegue con acceso SSH por clave pública.
# El usuario 'debian' ya viene en la imagen; este es adicional.
users:
  - name: deploy
    groups: [sudo]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"   # Necesario para automatización sin contraseña
    ssh_authorized_keys:
      - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGk7V... deploy@ci-runner"

# Escribir archivos antes de que se ejecuten los comandos.
# Los permisos van en octal como string, no como entero.
write_files:
  - path: /etc/nginx/sites-available/default
    permissions: "0644"
    owner: root:root
    content: |
      server {
          listen 80 default_server;
          root /var/www/html;
          index index.html;
          server_name _;
          location / { try_files $uri $uri/ =404; }
      }

  - path: /etc/ufw/applications.d/nginx-custom
    permissions: "0644"
    content: |
      [Nginx]
      title=Nginx HTTP
      description=Nginx web server
      ports=80/tcp

# Comandos que se ejecutan al final, en orden, con /bin/sh.
# Si un comando falla, cloud-init marca el módulo runcmd como error
# pero continúa con el siguiente módulo — no aborta todo.
runcmd:
  - ufw allow OpenSSH
  - ufw allow Nginx
  - ufw --force enable           # --force evita la pregunta interactiva
  - systemctl enable nginx
  - systemctl start nginx
  - nginx -t                     # Validar configuración antes de dar por bueno el boot

La primera línea #cloud-config no es un comentario decorativo: es el identificador de formato que cloud-init busca para saber cómo parsear el documento. Sin ella, cloud-init trata el texto como un script de shell opaco y falla en silencio.

package_upgrade: false merece atención. Actualizar todos los paquetes del sistema en el primer arranque puede tardar varios minutos y, en imágenes con kernel reciente, puede desencadenar un reboot automático que interrumpe el resto del proceso. Para instancias de producción con Ansible o similar, es mejor que la herramienta de configuración controle cuándo y cómo se actualizan los paquetes.

La sección users crea el usuario deploy con sudo sin contraseña. Fíjate en la sintaxis del campo sudo: es un string entre comillas, no un booleano. Cloud-init escribe literalmente ese string en el archivo de sudoers bajo /etc/sudoers.d/90-cloud-init-users. Si pones un boolean true ahí, el módulo lo ignora silenciosamente en algunas versiones.

write_files ejecuta antes de runcmd, lo cual importa: cuando UFW y Nginx arrancan en runcmd, el archivo de configuración de Nginx y el perfil de UFW ya existen. El orden entre módulos cloud-init está definido internamente; dentro de runcmd sí controlas el orden tú.

Para diagnosticar qué pasó durante el arranque:

# Estado general: did cloud-init finish? did it error?
cloud-init status --long

# El log completo, con timestamps y nivel de cada módulo
journalctl -u cloud-init -u cloud-init-local -u cloud-config -u cloud-final

# O directamente el archivo (más verboso, incluye output de comandos)
sudo tail -f /var/log/cloud-init.log
sudo tail -f /var/log/cloud-init-output.log   # stdout/stderr de runcmd

cloud-init status --long devuelve status: done o status: error con un campo errors que lista qué módulo falló. Es el primer comando que ejecutas cuando una instancia nueva no tiene lo que esperabas.

Si necesitas forzar que cloud-init vuelva a ejecutarse —en desarrollo, cuando estás iterando sobre el user data antes de producción— puedes limpiar la caché de estado:

# Esto hace que cloud-init se ejecute de nuevo en el próximo arranque.
# NUNCA en producción sobre una instancia real.
sudo cloud-init clean --logs
sudo reboot

Cloud-init guarda en /var/lib/cloud/instance/ todos los artefactos de la ejecución: el user data tal como llegó, los módulos ejecutados y su resultado. Si hay discrepancia entre lo que esperabas y lo que ocurrió, /var/lib/cloud/instance/user-data.txt te confirma qué recibió realmente la instancia.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio