En este tutorial se detalla paso a paso cómo centralizar y automatizar el respaldo de los archivos de configuración críticos de un clúster de Proxmox VE y de un servidor Proxmox Backup Server (PBS). Mediante el uso de Ansible Semaphore, se recolectarán de forma periódica las estructuras de metadatos, configuraciones de red y planificaciones de replicación de los nodos, almacenándolas en un repositorio persistente. Finalmente, el sistema generará un reporte dinámico en formato HTML con un manual interactivo de recuperación ante desastres y lo enviará automáticamente por correo electrónico para garantizar la resiliencia del CPD.
- 1. Requisitos previos y preparación del entorno.
- 2. Configuración de variables en Ansible Semaphore.
- 3. Docker Compose de Ansible para mapeo que unidad de playbooks y otras configuraciones.
- 4. Manual HTML de recuperación ante desastres (
recuperacion_proxmox_pbs.html). - 5. Inventario de nodos Proxmox en Ansible/Semaphore.
- 6. El Playbook de Ansible para automatizar el backup Proxmox y envío de email.
- 7. Creación y ejecución de la tarea periódica.
1. Requisitos previos y preparación del entorno
Para llevar a cabo la automatización de copias de seguridad de configuración de Proxmox VE y PBS, es necesario contar con la siguiente infraestructura:
- Un clúster de Proxmox VE operativo (en este escenario, compuesto por tres nodos:
n1,n2yn3). En el siguiente enlace indicamos cómo desplegar un clúster Proxmox VE: Crear un clúster de virtualización con Proxmox VE. - Un servidor Proxmox Backup Server (PBS) integrado o coexistiendo en uno de los nodos (en este escenario, configurado en el nodo
n3). En el siguiente enlace explicamos cómo desplegar un servidor PBS sobre un nodo Proxmox VE: Servidor de Backup Proxmox (PBS) sobre ZFS. - Una instancia de Ansible Semaphore desplegada mediante Docker. En el siguiente enlace explicamos cómo desplegar Ansible en Linux doquerizado: Despliegue de Ansible y Semaphore para automatización de tareas.
- Una cuenta de correo electrónico con soporte SMTP activo (por ejemplo, una cuenta de Gmail con una Contraseña de Aplicación generada para el envío seguro a través del puerto 587 con STARTTLS).
A nivel de red, el contenedor de Semaphore debe tener acceso libre en el cortafuegos (por ejemplo, OPNsense) para comunicarse con los nodos Proxmox, así como resolución DNS externa directa hacia internet (servidores como 8.8.8.8) para interactuar con las API de mensajería o servidores de correo.
NOTA IMPORTANTE: en la copia de seguridad que se va a automatizar en este tutorial NO se realizan copias de las máquinas virtuales de Proxmox, únicamente de las configuraciones de los nodos y el PBS. Las copias de las máquinas virtuales deben realizarse con Proxmox Backup Server (PBS), como indicamos en este tutorial:
2. Configuración de variables en Ansible Semaphore
Para evitar exponer credenciales en texto plano dentro del código, se deben dar de alta las siguientes variables, en ANsible Semaphore, desde «Grupos de variables» – «Nuevo grupo»:

Añadiremos estas variables:
|
1 2 3 4 5 6 7 8 |
{ "smtp_host": "smtp.gmail.com", "smtp_port": "587", "smtp_user": "tu_usuario@ajpdsoft.com", "smtp_pass": "xxxx xxxx xxxx xxxx", "email_from": "tu_usuario@ajpdsoft.com", "email_to": "destinatario@ajpdsoft.com" } |

3. Docker Compose de Ansible para mapeo que unidad de playbooks y otras configuraciones
En el siguiente tutorial explicamos cómo desplegar Ansible/Semaphore en docker con Linux:
Aún así, mostramos aquí el docker-compose.yml para desplegar Ansible con mapeo a carpetas del anfitrión, para tenerlas disponibles tanto fuera como dentro del contenedor:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
services: postgres: image: postgres:14-alpine volumes: - semaphore-db:/var/lib/postgresql/data environment: POSTGRES_USER: semaphore POSTGRES_PASSWORD: contraseña POSTGRES_DB: semaphore restart: unless-stopped semaphore: image: semaphoreui/semaphore:latest ports: - "3000:3000" # ---> DNS en contenedor <--- dns: - 8.8.8.8 - 4.4.4.4 environment: SEMAPHORE_DB_USER: semaphore SEMAPHORE_DB_PASS: contraseña SEMAPHORE_DB_HOST: postgres SEMAPHORE_DB_PORT: 5432 SEMAPHORE_DB_DIALECT: postgres SEMAPHORE_DB: semaphore # Configuración del usuario Administrador Inicial: SEMAPHORE_ADMIN_PASSWORD: contraseña SEMAPHORE_ADMIN_NAME: Administrator SEMAPHORE_ADMIN_EMAIL: admin@proyectoa.com SEMAPHORE_ADMIN: admin volumes: - ./playbooks:/playbooks - ./config:/etc/semaphore - /backups_proxmox:/var/backups_proxmox depends_on: - postgres restart: unless-stopped volumes: semaphore-db: |
Sobre todo, estamos mapeando:
- ./playbooks:/playbooks
- ./config:/etc/semaphore
- /backups_proxmox:/var/backups_proxmox
De forma que los backups que se realizarán en cada nodo del clúster Proxmox, se almacenarán en el equipo Ansible anfitrión, en la carpeta /backups_proxmox. Y los playbooks se guardarán en /playbooks (dentro de donde se ejecuta el docker-compose.yml). Por ello, en el siguiente punto, guardaremos el fichero recuperacion_proxmox_pbs.html en /playbooks del equipo anfitrión, para que, mediante el mapeo, Ansible del contenedor docker lo cogerá para enviarlo adjunto al email.
Si hemos modificado el docker-compose.yml, podremos volver a desplegar el contenedor, ejecutando, en la carpeta donde esté el fichero docker-compose.yml:
|
1 2 |
sudo docker compose down sudo docker compose up -d |
Nota: estos comandos NO eliminan datos en los contenedores, si teníamos configurada correctamente la persistencia, se detendrán los contenedores referenciados en el doccker-compose.yml y se volverán a regenerar, tomando los datos de la persistencia ya establecida anteriormente.
4. Manual HTML de recuperación ante desastres (recuperacion_proxmox_pbs.html)
Crearemos un fichero en el servidor Ansible en formato HTML, sobre los pasos para realizar una posible recuperación ante desastres, barajando varios escenarios (avería de un nodo, avería de todos los nodos y el clúster, avería del PBS). Este fichero debe ubicarse de en la ruta donde se encuentren los playbooks del servidor anfitrión de Ansible (por ejemplo /home/ansible/semaphore/playbooks/recuperacion_proxmox_pbs.html). Al estar mapeado como volumen en el contenedor de Semaphore, estará disponible para ser adjuntado en los correos salientes.
Crearemos el fichero recuperacion_proxmox_pbs.html en la carpeta de los playbooks del servidor de Ansible:
|
1 |
sudo nano /home/ansible/semaphore/playbooks/recuperacion_proxmox_pbs.html |
Cambiaremos la carpeta /home/ansible/semaphore/playbooks por la que tengamos en nuestro servidor Ansible.
Con el siguiente contenido:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> body { font-family: 'Segoe UI', Arial, sans-serif; line-height: 1.6; color: #2d3748; margin: 40px; background-color: #ffffff; } .header { background-color: #1a365d; color: white; padding: 30px; border-radius: 6px; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } .header h1 { margin: 0; font-size: 24px; letter-spacing: 0.5px; } .header p { margin: 10px 0 0 0; color: #90cdf4; font-weight: bold; } h2 { color: #1a365d; border-bottom: 3px solid #3182ce; padding-bottom: 6px; margin-top: 40px; font-size: 18px; text-transform: uppercase; } h3 { color: #2b6cb0; margin-top: 25px; font-size: 15px; border-left: 4px solid #4299e1; padding-left: 8px; } ul { padding-left: 20px; } li { margin-bottom: 8px; } .info-box { background-color: #ebf8ff; border: 1px solid #bee3f8; padding: 15px; border-radius: 6px; margin-top: 20px; font-size: 14px; } .alert-box { background-color: #fff5f5; border-left: 4px solid #e53e3e; color: #9b2c2c; padding: 15px; border-radius: 4px; margin: 20px 0; } .highlight-val { color: #1a365d; background-color: #edf2f7; font-weight: bold; font-family: monospace; font-size: 13.5px; padding: 2px 6px; border-radius: 3px; } table { width: 100%; border-collapse: collapse; margin: 15px 0; font-size: 13px; } th { background-color: #2d3748; color: white; padding: 10px; border: 1px solid #cbd5e0; text-align: left; } td { padding: 10px; border: 1px solid #cbd5e0; vertical-align: top; } tr:nth-child(even) { background-color: #f7fafc; } .network-badge { font-weight: bold; font-family: monospace; color: #2b6cb0; } pre { background-color: #1a202c; border-radius: 6px; padding: 14px; margin: 12px 0; overflow-x: auto; } code { font-family: 'Fira Code', 'Ubuntu Mono', monospace; font-size: 13.5px; color: #edf2f7; } pre code .cmd { color: #63b3ed; font-weight: bold; } pre code .comment { color: #a0aec0; font-style: italic; } </style> <title>Documento de Recuperación ante Desastres - PROYECTOA.COM</title> </head> <body> <div class="header"> <h1>DOCUMENTO DE RECUPERACIÓN ANTE DESASTRES - CPD PROYECTOA.COM</h1> <p>Clúster Proxmox VE y Proxmox Backup Server (PBS)</p> </div> <div class="info-box"> <b>Ubicación de Backups en ansible:</b> <span class="highlight-val">/backups_proxmox/</span><br> <b>Fecha de registro de infraestructura:</b> <span class="highlight-val">30 de Junio de 2026</span><br> <b>Documento para:</b> Ingenieros Informáticos y Administradores de Sistemas </div> <h2>1. Inventario actual de direccionamiento, topología de red y discos ZFS</h2> <h3>1.1. Distribución física de discos y pools ZFS</h3> <ul> <li><b>n1 (<span class="highlight-val">192.168.10.200</span>):</b> Pool <span class="highlight-val">raid1-1tb</span> (ST1000DM010 x2 Mirror) | <span class="highlight-val">ssd480-pool</span> | <span class="highlight-val">ssd500-pool</span></li> <li><b>n2 (<span class="highlight-val">192.168.10.201</span>):</b> Pool <span class="highlight-val">raid1-1tb</span> (ST1000DM010 x2 Mirror)</li> <li><b>n3 (<span class="highlight-val">192.168.10.202</span>):</b> Pool <span class="highlight-val">raid1-1tb</span> (Con fallos) | Pool <span class="highlight-val">raid1-1tb-backup</span> (Toshiba x2 Mirror → Datastore PBS)</li> </ul> <h3>1.2. Configuración de red</h3> <table> <thead> <tr> <th>Nodo / IP</th> <th>Nombre Bridge</th> <th>Puerto Físico</th> <th>Dirección CIDR / Puerta Enlace</th> <th>Segmento / Notas del Switch</th> <th>VLAN Aware</th> </tr> </thead> <tbody> <tr> <td rowspan="5"><b>n1</b><br>192.168.10.200</td> <td><span class="network-badge">vmbr0</span></td> <td>nic2</td> <td>192.168.10.200/24<br>GW: 192.168.10.1</td> <td>Gestión Principal (VLAN 10, 50 - Interfaz 5 Switch)</td> <td><b>SÍ</b> (Vids 2-4094)</td> </tr> <tr> <td><span class="network-badge">vmbr1</span></td> <td><i>none</i></td> <td><i>manual</i></td> <td>Sin adaptador asignado</td> <td>NO</td> </tr> <tr> <td><span class="network-badge">vmbr2</span></td> <td>nic0</td> <td><i>manual</i></td> <td>WAN dedicada (VLAN 99 - Interfaz 7 Switch)</td> <td>NO</td> </tr> <tr> <td><span class="network-badge">vmbr3</span></td> <td>nic1</td> <td><i>manual</i></td> <td>DMZ dedicada</td> <td>NO</td> </tr> <tr> <td><span class="network-badge">vmbr99</span></td> <td><i>none</i></td> <td><i>manual</i></td> <td>Red_Laboratorio (Aislada)</td> <td>NO</td> </tr> <tr> <td rowspan="4"><b>n2</b><br>192.168.10.201</td> <td><span class="network-badge">vmbr0</span></td> <td>enp4s0</td> <td>192.168.10.201/24<br>GW: 192.168.10.1</td> <td>Gestión Principal Clúster</td> <td><b>SÍ</b> (Vids 2-4094)</td> </tr> <tr> <td><span class="network-badge">vmbr1</span></td> <td><i>none</i></td> <td><i>manual</i></td> <td>Sin adaptador asignado</td> <td>NO</td> </tr> <tr> <td><span class="network-badge">vmbr3</span></td> <td>enp7s0</td> <td><i>manual</i></td> <td>DMZ dedicada</td> <td>NO</td> </tr> <tr> <td><span class="network-badge">vmbr99</span></td> <td><i>none</i></td> <td><i>manual</i></td> <td>Red_Laboratorio (Aislada)</td> <td>NO</td> </tr> <tr> <td rowspan="2"><b>n3</b><br>192.168.10.202</td> <td><span class="network-badge">vmbr0</span></td> <td>nic1</td> <td>192.168.10.202/24<br>GW: 192.168.10.1</td> <td>Gestión Principal Clúster & Servidor PBS</td> <td>NO</td> </tr> <tr> <td>-</td> <td>nic0</td> <td><i>manual</i></td> <td>Interfaz sin puente activo</td> <td>-</td> </tr> </tbody> </table> <div class="alert-box"> <strong>PLANIFICACIONES Y RÉPLICAS:</strong> Los archivos empaquetados por Ansible hacen copia de seguridad del directorio raíz de configuraciones del clúster hipervisor. Al restaurarlos, se recuperan automáticamente los perfiles de hardware de las máquinas, los esquemas de usuarios, el almacenamiento global (<i>storage.cfg</i>), los jobs de réplica ZFS entre nodos (<i>pvejob.cfg</i>) y los calendarios automáticos de backup programados en el Datacenter (<i>vzdump.cron</i>). </div> <h2>2. Escenario A: Pérdida total del clúster (caída de todos los nodos)</h2> <p>Sigue este orden estricto de reconstrucción si toda la infraestructura física se ve comprometida y se reinstala el clúster desde cero:</p> <ol> <li>Instalar Proxmox VE limpio desde el medio oficial en cada máquina, respetando estrictamente los Hostnames y las IPs estáticas de la tabla del punto 1.2.</li> <li>En el nodo asignado como maestro primario (<b>n1</b>), inicializar el clúster Corosync con su nombre real: <pre><code><span class="cmd">pvecm create</span> proxmox</code></pre> </li> <li>En los nodos secundarios (<b>n2</b> y <b>n3</b>), unirse al clúster apuntando a la IP del primero: <pre><code><span class="cmd">pvecm add</span> 192.168.10.200</code></pre> </li> <li>Una vez unificados en el clúster, forzar el escaneo y montaje automático de los pools ZFS de almacenamiento de datos en los tres servidores: <pre><code><span class="cmd">zpool import</span> -f -a</code></pre> </li> <li><b>Envío de metadatos desde Ansible:</b> Desde la terminal del servidor local de Ansible, transfiere el backup de configuración correspondiente a cada nodo (sustituye <i>FECHA_BACKUP</i> por el nombre real de la carpeta persistente generada, ej: <i>2026-06-30</i>): <pre><code><span class="cmd">scp</span> /backups_proxmox/n1_FECHA_BACKUP/pve-config-backup.tar.gz root@192.168.10.200:/tmp/ <span class="cmd">scp</span> /backups_proxmox/n2_FECHA_BACKUP/pve-config-backup.tar.gz root@192.168.10.201:/tmp/ <span class="cmd">scp</span> /backups_proxmox/n3_FECHA_BACKUP/pve-config-backup.tar.gz root@192.168.10.202:/tmp/</code></pre> </li> <li><b>Restauración de perfiles en cada Nodo:</b> Accede por SSH a <b>cada uno de los servidores (n1, n2, n3)</b> de manera independiente y ejecuta este bloque de comandos para transferir las configuraciones en la base de datos distribuida del clúster: <pre><code><span class="comment"># 1. Parar temporalmente los servicios del hipervisor</span> <span class="cmd">systemctl stop</span> pvedaemon pveproxy pvestatd corosync pve-cluster <span class="comment"># 2. Forzar el montaje del sistema de archivos local en modo aislado</span> <span class="cmd">pmxcfs</span> -l <span class="comment"># 3. Crear el directorio de extracción y descomprimir el archivo</span> <span class="cmd">mkdir</span> -p /tmp/rec && <span class="cmd">tar</span> -xzvf /tmp/pve-config-backup.tar.gz -C /tmp/rec/ <span class="comment"># 4. Volcar las configuraciones de hardware (ID, RAM, CPU) y almacenamiento al clúster</span> <span class="cmd">cp</span> -r /tmp/rec/etc/pve/nodes/localhost/qemu-server/* /etc/pve/nodes/localhost/qemu-server/ 2>/dev/null || true <span class="cmd">cp</span> -r /tmp/rec/etc/pve/nodes/localhost/lxc/* /etc/pve/nodes/localhost/lxc/ 2>/dev/null || true <span class="cmd">cp</span> /tmp/rec/etc/pve/storage.cfg /etc/pve/storage.cfg <span class="cmd">cp</span> /tmp/rec/etc/pve/vzdump.cron /etc/pve/vzdump.cron 2>/dev/null || true <span class="cmd">cp</span> /tmp/rec/etc/pve/pvejob.cfg /etc/pve/pvejob.cfg 2>/dev/null || true</code></pre> </li> <li>Reiniciar simultáneamente todos los hosts. Al arrancar, el ecosistema de Proxmox leerá las configuraciones agregadas y vinculará de manera transparente la GUI con el almacenamiento ZFS. <pre><code><span class="cmd">reboot</span></code></pre> </li> </ol> <h2>3. Escenario B: Pérdida de un único nodo Proxmox VE (avería en n1, n2 o n3)</h2> <p>Si solo un equipo sufre una avería en el disco del SO de Proxmox, el clúster bloqueará el nodo caído. Sigue este orden:</p> <ol> <li>Desde la consola de cualquier nodo superviviente que esté ONLINE, expulsar el nodo caído del clúster: <pre><code><span class="cmd">pvecm delnode</span> NOMBRE_NODO_CAIDO</code></pre> </li> <li>Proceder con la instalación limpia de Proxmox VE en el servidor recuperado utilizando su nombre e IP fija correspondientes.</li> <li>Unir el nuevo nodo al clúster activo ejecutando en su terminal: <pre><code><span class="cmd">pvecm add</span> 192.168.10.X <span class="comment"># Usar la IP de cualquier nodo sano (ej. 192.168.10.201)</span></code></pre> </li> <li>Importar el pool local ZFS sin alterar los identificadores ni metadatos: <pre><code><span class="cmd">zpool import</span> -f raid1-1tb</code></pre> </li> <li>Desde el servidor local de Ansible, enviar el tar.gz de configuración al directorio temporal del nodo afectado: <pre><code><span class="cmd">scp</span> /backups_proxmox/n1_2026-06-30/pve-config-backup.tar.gz root@192.168.10.200:/tmp/</code></pre> </li> <li>Dentro del nodo recuperado, detener los servicios del hipervisor para realizar la importación de datos de forma segura: <pre><code><span class="cmd">systemctl stop</span> pvedaemon pveproxy pvestatd corosync pve-cluster</code></pre> </li> <li>Crear directorio de trabajo y descomprimir el backup: <pre><code><span class="cmd">mkdir</span> /tmp/rec && <span class="cmd">tar</span> -xzvf /tmp/pve-config-backup.tar.gz -C /tmp/rec/</code></pre> </li> <li>Restaurar las configuraciones del sistema operativo base (Redes e indexación de discos): <pre><code><span class="cmd">cp</span> /tmp/rec/etc/network/interfaces /etc/network/interfaces <span class="cmd">cp</span> /tmp/rec/etc/fstab /etc/fstab</code></pre> </li> <li><b>PASO CRÍTICO:</b> Levantar el sistema de archivos de Proxmox de manera local y aislada para transferir los perfiles de hardware de las máquinas virtuales y contenedores: <pre><code><span class="cmd">pmxcfs</span> -l <span class="cmd">cp</span> -r /tmp/rec/etc/pve/nodes/localhost/qemu-server/* /etc/pve/nodes/localhost/qemu-server/ 2>/dev/null || true <span class="cmd">cp</span> -r /tmp/rec/etc/pve/nodes/localhost/lxc/* /etc/pve/nodes/localhost/lxc/ 2>/dev/null || true <span class="cmd">cp</span> /tmp/rec/etc/pve/storage.cfg /etc/pve/storage.cfg</code></pre> </li> <li>Reiniciar el servidor por completo para unificar el clúster y arrancar servicios: <pre><code><span class="cmd">reboot</span></code></pre> </li> </ol> <h2>4. Escenario C: Pérdida del Servicio Proxmox Backup Server (PBS) en nodo n3</h2> <p>El nodo n3 gestiona de forma simultánea el hipervisor clúster y el motor centralizado de copias de seguridad de Proxmox (PBS).</p> <ol> <li>Una vez instalado el sistema base Proxmox VE en n3, instalar el paquete de PBS mediante los repositorios oficiales de Proxmox.</li> <li>Reconocer e importar el pool físico de discos que contiene los datos persistentes de los backups antiguos: <pre><code><span class="cmd">zpool import</span> -f raid1-1tb-backup</code></pre> </li> <li>Mover el archivo empaquetado de PBS desde Ansible al `/tmp/` del nodo n3: <pre><code><span class="cmd">scp</span> /backups_proxmox/n3_2026-06-30/pbs-config-backup.tar.gz root@192.168.10.202:/tmp/</code></pre> </li> <li>Apagar de forma temporal el motor de copias para sobreescribir la configuración limpia de fábrica: <pre><code><span class="cmd">systemctl stop</span> proxmox-backup proxmox-backup-proxy</code></pre> </li> <li>Extraer el árbol de configuraciones directamente sobre el directorio raíz: <pre><code><span class="cmd">tar</span> -xzvf /tmp/pbs-config-backup.tar.gz -C /</code></pre> </li> <li><b>PERMISOS:</b> Modificar el propietario de las carpetas inyectadas para habilitar el acceso al usuario del servicio PBS: <pre><code><span class="cmd">chown</span> -R backup:backup /etc/proxmox-backup <span class="cmd">chown</span> -R backup:backup /var/lib/proxmox-backup</code></pre> </li> <li>Encender los demonios del servicio: <pre><code><span class="cmd">systemctl start</span> proxmox-backup proxmox-backup-proxy</code></pre> </li> <li>Abrir el entorno web de PBS (<i>https://192.168.10.202:8007</i>). El archivo restaurado <i>datastore.cfg</i> enlazará el pool ZFS de backup importado en el Paso 2 y los históricos de copias se mostrarán ONLINE listos para restaurar máquinas virtuales de forma inmediata.</li> </ol> </body> </html> |
Evidentemente, adaptaremos el fichero HTML a la información y características de nuestro entorno. Es un HTML estático, por lo que hay que modificarlo y adaptarlo de forma manual.
5. Inventario de nodos Proxmox en Ansible/Semaphore
Dado que para este caso vamos a realizar conexión SSH desde Ansible a los nodos Proxmox, y NO vamos a usar el API de Proxmox, necesitaremos definir inventario de nodos en Semaphore. Desde «Inventario» – «Nuevo inventario» – «Ansible Inventory»:

Estableceremos el nombre servidores_proxmox para este inventario, esto es importante, porque en el playbook indicaremos este nombre en hosts. Añadiremos el siguiente INI (modificando las IP y nodos Proxmox por las de nuestra organización):
|
1 2 3 4 5 6 7 8 |
[servidores_proxmox] n1 ansible_host=192.168.10.200 n2 ansible_host=192.168.10.201 n3 ansible_host=192.168.10.202 [servidores_proxmox:vars] ansible_user=anb_sru ansible_python_interpreter=/usr/bin/python3.13 |

Estableceremos el usuario con el que realizamos la conexión SSH, como indicamos en este tutorial:
6. El Playbook de Ansible para automatizar el backup Proxmox y envío de email
Crearemos el playbook que realizará todas las tareas en cada nodo Proxmox, desde Ansible: playbook backup_proxmox_pbs.yml. Crearemos el fichero del playbook en el servidor de Ansible:
|
1 |
sudo nano /home/ansible/semaphore/playbooks/backup_proxmox_pbs.yml |
Cambiaremos la carpeta /home/ansible/semaphore/playbooks por la que tengamos en nuestro servidor Ansible.
El contenido del playbook será:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
--- - name: Backup Proxmox, envío de email con resultado y adjunto con documento de recuperación ante desastres hosts: servidores_proxmox become: yes gather_facts: yes vars: base_local_path: "/var/backups_proxmox" local_backup_path: "{{ base_local_path }}/{{ ansible_facts['hostname'] }}_{{ ansible_facts['date_time']['date'] }}" local_log_file: "/playbooks/backup_proxmox_ejecucion.log" tasks: # ------------------------------------------------------------------------- # 0. CONTROL DE DEPENDENCIAS LOCALES EN EL ENTORNO VIRTUAL # ------------------------------------------------------------------------- - name: 0. Asegurar dependencias de Python en el entorno ejecutor de Semaphore ansible.builtin.pip: name: - proxmoxer - requests state: present delegate_to: localhost run_once: true become: no vars: ansible_python_interpreter: "{{ ansible_playbook_python }}" comment: "Garantiza que el venv del contenedor cuente con las librerías necesarias para llamadas a API de terceros" # ------------------------------------------------------------------------- # 1. TAREAS REMOTAS EN LOS NODOS PROXMOX # ------------------------------------------------------------------------- - name: 1. Crear el archivo de backup de Proxmox VE (PVE) community.general.archive: path: - /etc/pve - /etc/network/interfaces - /etc/fstab dest: /tmp/pve-config-backup.tar.gz format: gz force_archive: true comment: "Empaqueta el sistema de archivos virtual del clúster (pmxcfs) y ficheros base de red" - name: 2. Crear el archivo de backup de PBS (nodo n3) community.general.archive: path: - /etc/proxmox-backup - /var/lib/proxmox-backup/chown dest: /tmp/pbs-config-backup.tar.gz format: gz force_archive: true when: ansible_facts['hostname'] == 'n3' comment: "Filtra por hostname y ejecuta el backup exclusivo de la estructura de Proxmox Backup Server" - name: 3. Asegurar directorio local en el volumen del contenedor Ansible ansible.builtin.file: path: "{{ local_backup_path }}" state: directory mode: '0777' delegate_to: localhost become: no comment: "Garantiza de forma local la existencia de la ruta cronológica para la recepción de archivos" - name: 4. Descargar el backup de PVE al servidor Ansible ansible.builtin.fetch: src: /tmp/pve-config-backup.tar.gz dest: "{{ local_backup_path }}/pve-config-backup.tar.gz" flat: yes comment: "Transfiere los archivos comprimidos de configuración de PVE desde los nodos remotos al servidor Ansible" - name: 5. Descargar el backup de PBS al servidor Ansible (nodo n3) ansible.builtin.fetch: src: /tmp/pbs-config-backup.tar.gz dest: "{{ local_backup_path }}/pbs-config-backup.tar.gz" flat: yes when: ansible_facts['hostname'] == 'n3' comment: "Transfiere el archivo comprimido del servidor PBS al servidor Ansible" - name: 6. Limpiar archivos temporales de los nodos remotos ansible.builtin.file: path: "{{ item }}" state: absent loop: - /tmp/pve-config-backup.tar.gz - /tmp/pbs-config-backup.tar.gz comment: "Elimina archivos temporales del almacenamiento local /tmp de los nodos remotos" # ------------------------------------------------------------------------- # AUDITORÍA Y ESCRITURA DE LOG LOCAL # ------------------------------------------------------------------------- - name: Registrar estado de la ejecución en log local ansible.builtin.lineinfile: path: "{{ local_log_file }}" line: "Backup completado correctamente para el nodo {{ ansible_facts['hostname'] }} el {{ ansible_facts['date_time']['iso8601'] }}" create: yes mode: '0644' delegate_to: localhost become: no comment: "Mantiene un histórico persistente dentro del volumen mapeado de Ansible para auditorías rápidas del sistema" # ------------------------------------------------------------------------- # 2. ENVÍO DE EMAIL CON RESULTADO DE BACKUP Y DOCUMENTO HTML DE RECUPERACIÓN ANTE DESASTRES # ------------------------------------------------------------------------- - name: 7. Enviar email de resultado de backup con fichero HTML adjunto de recuperación ante desastres community.general.mail: host: "{{ smtp_host }}" port: "{{ smtp_port | int }}" username: "{{ smtp_user }}" password: "{{ smtp_pass }}" to: "{{ email_to }}" from: "{{ email_from }}" secure: starttls charset: utf-8 subject: "[PROXMOX] [ANSIBLE] Resultado de copia de seguridad de configuraciones de clúster PROXMOX - {{ ansible_facts['date_time']['date'] }}" subtype: html body: | <html> <body style="font-family: Arial, sans-serif; color: #2d3748; line-height: 1.6;"> <div style="padding: 20px; border: 1px solid #e2e8f0; border-top: none; background-color: #f7fafc;"> <p>Se ha realizado la copia de seguridad de las configuraciones de los nodos PROXMOX correctamente.</p> <h3 style="color: #2b6cb0;">Ubicación de las copias en el servidor Ansible (192.168.10.111):</h3> <p style="background-color: #edf2f7; padding: 10px; font-family: monospace;">/backups_proxmox/</p> <h3 style="color: #2b6cb0;">Detalle de ficheros copiados por nodo</h3> <table style="width: 100%; border-collapse: collapse; margin-top: 10px;"> <thead> <tr style="background-color: #2d3748; color: white; text-align: left;"> <th style="padding: 10px; border: 1px solid #cbd5e0;">Nodo Proxmox</th> <th style="padding: 10px; border: 1px solid #cbd5e0;">Tipo de servicio</th> <th style="padding: 10px; border: 1px solid #cbd5e0;">Ruta del archivo en servidor ansible</th> <th style="padding: 10px; border: 1px solid #cbd5e0;">Estado</th> </tr> </thead> <tbody> <tr> <td style="padding: 10px; border: 1px solid #cbd5e0;"><b>n1</b> (192.168.10.200)</td> <td style="padding: 10px; border: 1px solid #cbd5e0;">Proxmox VE</td> <td style="padding: 10px; border: 1px solid #cbd5e0; font-family: monospace; font-size: 12px;">/backups_proxmox/n1_{{ ansible_facts['date_time']['date'] }}/pve-config-backup.tar.gz</td> <td style="padding: 10px; border: 1px solid #cbd5e0; color: #38a169; font-weight: bold;">OK</td> </tr> <tr> <td style="padding: 10px; border: 1px solid #cbd5e0;"><b>n2</b> (192.168.10.201)</td> <td style="padding: 10px; border: 1px solid #cbd5e0;">Proxmox VE</td> <td style="padding: 10px; border: 1px solid #cbd5e0; font-family: monospace; font-size: 12px;">/backups_proxmox/n2_{{ ansible_facts['date_time']['date'] }}/pve-config-backup.tar.gz</td> <td style="padding: 10px; border: 1px solid #cbd5e0; color: #38a169; font-weight: bold;">OK</td> </tr> <tr> <td style="padding: 10px; border: 1px solid #cbd5e0;" rowspan="2"><b>n3</b> (192.168.10.202)</td> <td style="padding: 10px; border: 1px solid #cbd5e0;">Proxmox VE</td> <td style="padding: 10px; border: 1px solid #cbd5e0; font-family: monospace; font-size: 12px;">/backups_proxmox/n3_{{ ansible_facts['date_time']['date'] }}/pve-config-backup.tar.gz</td> <td style="padding: 10px; border: 1px solid #cbd5e0; color: #38a169; font-weight: bold;">OK</td> </tr> <tr> <td style="padding: 10px; border: 1px solid #cbd5e0; background-color: #fffaf0;">Proxmox Backup Server</td> <td style="padding: 10px; border: 1px solid #cbd5e0; font-family: monospace; font-size: 12px; background-color: #fffaf0;">/backups_proxmox/n3_{{ ansible_facts['date_time']['date'] }}/pbs-config-backup.tar.gz</td> <td style="padding: 10px; border: 1px solid #cbd5e0; color: #38a169; font-weight: bold; background-color: #fffaf0;">OK</td> </tr> </tbody> </table> <div style="margin-top: 20px; padding: 15px; background-color: #fff5f5; border-left: 4px solid #e53e3e; font-size: 13px;"> <strong>AVISO IMPORTANTE:</strong> Se adjunta el documento <code>recuperacion_proxmox_pbs.html</code>. Incluye el procedimiento de recuperación ante desastres (avería de nodo, avería de todos los nodos, avería del PBS). </div> </div> </body> </html> attach: - "/playbooks/recuperacion_proxmox_pbs.html" delegate_to: localhost run_once: true become: no comment: "Envía email con resultado de copia de seguridad de cada nodo, y adjunta documento HTML de recuperación ante desastres" |
Consideraciones importantes para la adaptación del Playbook
Antes de ejecutar este playbook en producción, hay que revisar y adaptar ciertos parámetros para asegurar su compatibilidad con la infraestructura de cada organización:
- Identificación del Inventario (
hosts): En la cabecera del playbook se definehosts: servidores_proxmox. Este valor debe coincidir exactamente con el nombre del inventario lógico creado dentro de Ansible Semaphore que agrupa a los nodos del clúster. - Ruta de almacenamiento local (
base_local_path): La variable/var/backups_proxmoxapunta al directorio interno del contenedor de Semaphore. Hay que verificar que esta ruta esté correctamente mapeada en eldocker-compose.ymlcontra un volumen persistente o un disco externo del servidor anfitrión. - Fichero de registro (
local_log_file): La variable/playbooks/backup_proxmox_ejecucion.logescribe un histórico de auditoría. Se debe asegurar que el usuario que ejecuta Semaphore tenga permisos de escritura en la carpeta de playbooks para evitar fallos de denegación de acceso. - Filtros por Hostname (
when): En las tareas dedicadas a Proxmox Backup Server, se incluye la condicionalwhen: ansible_facts['hostname'] == 'n3'. Si el servicio PBS está desplegado en un nodo con otro nombre, se debe sustituir'n3'por el hostname correspondiente.
Análisis detallado de las tareas del Playbook
A continuación, se desglosa el comportamiento técnico de cada una de las tareas automatizadas en este script de Ansible:
- 0. Asegurar dependencias de Python: Esta tarea se ejecuta en el
localhost(el propio contenedor de Semaphore) una única vez (run_once: true). Utiliza el gestor de paquetespipapuntando al intérprete exacto de Ansible (ansible_playbook_python) para instalarproxmoxeryrequests. De este modo, se garantiza que cualquier otro playbook del entorno que use la API de Proxmox disponga de las librerías necesarias, resolviendo además problemas de inicialización de submódulos de Python. - 1. Crear el archivo de backup de Proxmox VE (PVE): Se ejecuta de forma remota en todos los nodos del clúster. Utiliza el módulo
community.general.archivepara empaquetar de manera agnóstica el directorio/etc/pve(el sistema de archivos virtual en clústerpmxcfs), el fichero de configuración de interfaces de red (/etc/network/interfaces) y la tabla de montaje de discos (/etc/fstab). El resultado se comprime en un único archivo.tar.gztemporal en la ruta/tmp/del nodo. - 2. Crear el archivo de backup de PBS (nodo n3): Actúa exclusivamente en el servidor que cumple la condición de hostname (
n3). Comprime la estructura de configuración del servidor de copias de seguridad (/etc/proxmox-backup) y los metadatos de propiedad de los almacenes (/var/lib/proxmox-backup/chown), empaquetándolo en otro archivo temporal.tar.gz. - 3. Asegurar directorio local en el volumen del contenedor Ansible: Delegada al
localhost, esta tarea verifica la estructura del almacenamiento persistente del servidor de automatización Ansible. Crea una carpeta cronológica utilizando el hostname del nodo y la fecha actual del sistema (ej.n1_2026-07-02), asignándole permisos (0777) para evitar conflictos de bloqueo de archivos. - 4. Descargar el backup de PVE al servidor Ansible: Utiliza el módulo
fetchde Ansible para transferir el archivo/tmp/pve-config-backup.tar.gzdesde cada uno de los nodos del clúster hacia la carpeta recién creada en el almacenamiento centralizado del servidor Ansible. - 5. Descargar el backup de PBS al servidor Ansible (nodo n3): Realiza la misma operación de descarga que la tarea anterior, pero recuperando el archivo comprimido con las configuraciones del Proxmox Backup Server ubicado en el nodo configurado.
- 6. Limpiar archivos temporales de los nodos remotos: Una vez que los archivos comprimidos han sido descargados con éxito en el servidor Ansible, esta tarea elimina los archivos
.tar.gzresiduales de la carpeta/tmp/de cada nodo remoto, asegurando que no se consuma espacio innecesario en los discos de los hipervisores. - Registrar estado de la ejecución en log local: Escribe una línea de texto formateada con la fecha, hora y estado de la ejecución dentro del archivo log del volumen permanente (
). Esto permite llevar un control histórico e identificar rápidamente si un nodo ha dejado de reportar sus copias de seguridad.backup_proxmox_ejecucion.log - 7. Enviar email de reporte con fichero HTML adjunto: Esta tarea final se ejecuta una sola vez en el
localhost. Recopila las variables SMTP insertadas en Semaphore, establece un canal seguro mediantestarttls, define la codificación de caracteres enutf-8para soportar textos enriquecidos y genera un correo con formato HTML estructurado que incluye la tabla de estado de las copias de seguridad. Además, localiza en el volumen el manual técnicorecuperacion_proxmox_pbs.htmly lo envía adjunto.
7. Creación y ejecución de la tarea periódica
Para automatizar la ejecución del proceso en entornos de producción, seguiremos los siguientes pasos desde la interfaz Ansible/Semaphore:
- En el panel de control Semaphore, pulsaremos en Plantillas de Tareas y en «Nueva plantilla» – «Ansible Playbook»:

- Introduciremos los siguientes datos en la plantilla:
- Nombre: backup_proxmox_pbs.
- Ruta al archivo de playbook: playbook
backup_proxmox_pbs.yml. - Inventario: sevidores_proxmox.
- Repositorio: Local.
- Grupos de Variables: servidor_email.

Realizaremos una primera prueba de ejecución de la tarea:

Se ejecutará la tarea y mostrará el resultado:

Habrá enviado un email con el resultado de la copia de seguridad:

Dicho correo electrónico contendrá, además, un fichero adjunto en formato HTML con los pasos e instrucciones para una posible recuperación del sistema:

Cuando hayamos revisado que la tarea funciona perfectamente, añadiremos la programación oportuna para que se ejecute cada cierto tiempo, por ejemplo cada dos días:
