En la entrada anterior vimos cómo utilizar los principales módulos y comandos de Ansible para administrar un inventario de 100 servidores Linux.
Pero ejecutar comandos sobre 100 servidores es solamente el comienzo.
En ambientes productivos existe una pregunta mucho más importante:
¿Cómo podemos aplicar actualizaciones a 100 servidores sin detener todo el servicio?
Actualizar simultáneamente todos los nodos de un clúster puede provocar una interrupción completa. Por ello, una estrategia más segura consiste en dividir el mantenimiento en lotes, validar cada grupo antes y después del cambio y detener automáticamente el procedimiento cuando algo sale mal.
En este ejemplo construiremos un Playbook utilizando:
serial
pre-checks
health checks
handlers
patching
reboot
rollback
post-checks
La idea será implementar el siguiente flujo:
100 SERVIDORES
│
▼
┌───────────────┐
│ PRE-CHECKS │
└───────┬───────┘
│
▼
LOTE DE 10
│
▼
┌───────────────┐
│ PATCHING │
└───────┬───────┘
│
▼
┌───────────────┐
│ REBOOT │
└───────┬───────┘
│
▼
┌───────────────┐
│ HEALTH CHECK │
└───────┬───────┘
│
OK ──┴── ERROR
│ │
▼ ▼
SIGUIENTE ROLLBACK
LOTE
1. Nuestro inventario de 100 servidores
Supongamos que tenemos 100 application servers:
[appservers]
app001
app002
app003
...
app098
app099
app100
Podemos comprobar el inventario antes de iniciar:
ansible-inventory -i inventory.ini --graph
Y validar conectividad:
ansible appservers \
-i inventory.ini \
-m ansible.builtin.ping
Nunca deberíamos comenzar una ventana de mantenimiento sin comprobar primero qué hosts serán afectados.
2. El problema de actualizar 100 servidores simultáneamente
Podríamos crear un Playbook con:
hosts: appservers
Pero si los 100 servidores forman parte de una misma plataforma, realizar operaciones disruptivas simultáneamente puede provocar una caída completa.
Nuestro objetivo será mantener siempre servidores disponibles mientras otros reciben mantenimiento.
Por ejemplo:
Hosts totales: 100
Tamaño del lote: 10
Lotes necesarios: 10
Mientras actualizamos:
app001 → app010 PATCHING
app011 → app100 DISPONIBLES
Después continuamos:
app001 → app010 OK
app011 → app020 PATCHING
app021 → app100 DISPONIBLES
Y repetimos hasta completar los 100 servidores.
3. serial: la clave para controlar los lotes
Ansible permite controlar cuántos hosts procesa simultáneamente mediante:
serial: 10
Nuestra estructura inicial sería:
---
- name: Rolling patching Linux servers
hosts: appservers
become: true
serial: 10
tasks:
- name: Check connectivity
ansible.builtin.ping:
Aunque nuestro inventario tenga:
100 hosts
Ansible procesará el Play aproximadamente como:
Batch 01 → 10 hosts
Batch 02 → 10 hosts
Batch 03 → 10 hosts
...
Batch 10 → 10 hosts
Esto reduce considerablemente el riesgo frente a actualizar todos los nodos simultáneamente.
4. PRE-CHECKS antes del mantenimiento
Antes de modificar un servidor debemos verificar que se encuentra en condiciones adecuadas.
Algunos checks habituales son:
✓ Conectividad
✓ Filesystem
✓ Memoria
✓ Servicio activo
✓ Aplicación respondiendo
Podemos empezar comprobando espacio:
- name: Check filesystem
ansible.builtin.command:
cmd: df -P /
register: filesystem_status
changed_when: false
También podemos verificar nuestro servicio:
- name: Check application service
ansible.builtin.command:
cmd: systemctl is-active myapp
register: app_status
changed_when: false
Y detener el mantenimiento de ese host si la aplicación no estaba funcionando antes del cambio:
- name: Validate application status
ansible.builtin.assert:
that:
- app_status.stdout == "active"
fail_msg: "Application was not healthy before patching."
Esto es importante porque evita atribuir al patching un problema que ya existía antes de comenzar.
5. Health check HTTP antes del patching
Si nuestra aplicación proporciona un endpoint de health check, podemos comprobarlo directamente.
Por ejemplo:
http://localhost:8080/health
Con Ansible:
- name: Pre-patching application health check
ansible.builtin.uri:
url: http://localhost:8080/health
method: GET
status_code: 200
register: pre_health
Si el endpoint no devuelve:
HTTP 200
el servidor no debería continuar automáticamente con el mantenimiento.
6. Crear información para rollback
Antes de modificar paquetes conviene registrar su estado.
Por ejemplo:
- name: Get installed package information
ansible.builtin.package_facts:
manager: auto
También podemos respaldar configuraciones críticas.
- name: Backup application configuration
ansible.builtin.copy:
src: /etc/myapp/myapp.conf
dest: /etc/myapp/myapp.conf.prepatch
remote_src: true
backup: true
Dependiendo de la aplicación, el rollback puede requerir mucho más:
Package downgrade
Snapshot de VM
Backup
Restauración de configuración
Rollback de aplicación
Rollback de base de datos
Por eso el rollback real debe diseñarse específicamente para cada plataforma.
7. Aplicando los patches
Para sistemas Red Hat/Oracle Linux podemos utilizar el módulo DNF.
- name: Apply operating system updates
ansible.builtin.dnf:
name: "*"
state: latest
notify:
- Restart application
Para una estrategia más conservadora también podemos actualizar solamente determinados paquetes.
Por ejemplo:
- name: Update OpenSSL
ansible.builtin.dnf:
name: openssl
state: latest
Esto permite reducir el alcance del cambio.
8. Handlers
Los handlers son útiles para ejecutar acciones solamente cuando una tarea genera un cambio.
Por ejemplo:
handlers:
- name: Restart application
ansible.builtin.service:
name: myapp
state: restarted
La tarea de actualización puede utilizar:
notify:
- Restart application
De esta manera el restart solamente será solicitado cuando exista un cambio relevante.
9. Reiniciar el servidor
Algunos patches requieren reiniciar el sistema operativo.
Ansible proporciona:
ansible.builtin.reboot
Podemos utilizar:
- name: Reboot server after patching
ansible.builtin.reboot:
reboot_timeout: 900
Ansible esperará a que el servidor vuelva a estar disponible.
Podemos comprobar nuevamente la conectividad:
- name: Verify server connectivity
ansible.builtin.ping:
10. Esperar a que la aplicación vuelva
Que Linux responda por SSH no significa necesariamente que la aplicación esté lista.
Podemos esperar a que el puerto esté disponible:
- name: Wait for application port
ansible.builtin.wait_for:
host: "{{ inventory_hostname }}"
port: 8080
delay: 5
timeout: 120
O ejecutar directamente un health check:
- name: Application health check
ansible.builtin.uri:
url: http://localhost:8080/health
method: GET
status_code: 200
register: post_health
retries: 12
delay: 10
until: post_health.status == 200
Aquí estamos dando a la aplicación hasta aproximadamente:
12 × 10 segundos = 120 segundos
para responder correctamente.
11. Rollback automático
Aquí comienza una de las partes más interesantes.
Podemos utilizar:
block:
rescue:
always:
La idea es:
BLOCK
│
├── Backup
├── Patching
├── Reboot
└── Health Check
│
├── SUCCESS → continuar
│
└── FAILED
│
▼
RESCUE
│
▼
ROLLBACK
Por ejemplo:
- name: Patching operation
block:
- name: Apply updates
ansible.builtin.dnf:
name: "*"
state: latest
- name: Reboot server
ansible.builtin.reboot:
reboot_timeout: 900
- name: Validate application
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
rescue:
- name: Restore application configuration
ansible.builtin.copy:
src: /etc/myapp/myapp.conf.prepatch
dest: /etc/myapp/myapp.conf
remote_src: true
- name: Restart application after rollback
ansible.builtin.service:
name: myapp
state: restarted
Importante:
Restaurar un archivo de configuración no equivale necesariamente a revertir un patch del sistema operativo.
En producción, el rollback podría depender de snapshots, repositorios versionados, downgrade de paquetes o mecanismos propios de nuestra plataforma.
12. Detener el procedimiento cuando existen demasiados errores
No queremos que Ansible continúe actualizando servidores si comenzamos a detectar fallos.
Podemos utilizar:
max_fail_percentage: 10
Por ejemplo:
- name: Rolling patching
hosts: appservers
serial: 10
max_fail_percentage: 10
Esto introduce otro mecanismo de protección para evitar que una ejecución problemática continúe propagándose por toda la infraestructura.
Para cambios críticos incluso podemos adoptar una política más estricta.
13. POST-CHECKS
Después del mantenimiento debemos comprobar nuevamente el servidor.
Algunos post-checks habituales:
✓ SSH funcionando
✓ Filesystem correcto
✓ Memoria disponible
✓ Servicios activos
✓ Puertos escuchando
✓ Health endpoint = HTTP 200
✓ Aplicación respondiendo
✓ Logs sin errores críticos
Podemos comprobar el servicio:
- name: Verify application service
ansible.builtin.command:
cmd: systemctl is-active myapp
register: final_service
changed_when: false
Validarlo:
- name: Validate service
ansible.builtin.assert:
that:
- final_service.stdout == "active"
Y realizar el health check final:
- name: Final application health check
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
14. Playbook completo
Uniendo los conceptos anteriores podemos crear una base como ésta:
---
- name: Rolling patching for production Linux servers
hosts: appservers
become: true
serial: 10
max_fail_percentage: 10
vars:
application_service: myapp
health_url: http://localhost:8080/health
pre_tasks:
- name: Verify connectivity
ansible.builtin.ping:
- name: Verify application service before maintenance
ansible.builtin.command:
cmd: "systemctl is-active {{ application_service }}"
register: pre_service
changed_when: false
- name: Validate application before maintenance
ansible.builtin.assert:
that:
- pre_service.stdout == "active"
- name: Pre-maintenance health check
ansible.builtin.uri:
url: "{{ health_url }}"
status_code: 200
tasks:
- name: Execute patching operation
block:
- name: Backup application configuration
ansible.builtin.copy:
src: /etc/myapp/myapp.conf
dest: /etc/myapp/myapp.conf.prepatch
remote_src: true
- name: Apply operating system updates
ansible.builtin.dnf:
name: "*"
state: latest
notify:
- Restart application
- name: Reboot server
ansible.builtin.reboot:
reboot_timeout: 900
- name: Wait for application port
ansible.builtin.wait_for:
port: 8080
delay: 5
timeout: 120
- name: Validate application after patching
ansible.builtin.uri:
url: "{{ health_url }}"
status_code: 200
register: health_result
retries: 12
delay: 10
until: health_result.status == 200
rescue:
- name: Report patching failure
ansible.builtin.debug:
msg: "Patching failed on {{ inventory_hostname }}. Starting rollback."
- name: Restore configuration
ansible.builtin.copy:
src: /etc/myapp/myapp.conf.prepatch
dest: /etc/myapp/myapp.conf
remote_src: true
- name: Restart application
ansible.builtin.service:
name: "{{ application_service }}"
state: restarted
always:
- name: Report processed host
ansible.builtin.debug:
msg: "Maintenance processing completed for {{ inventory_hostname }}"
post_tasks:
- name: Verify final service status
ansible.builtin.command:
cmd: "systemctl is-active {{ application_service }}"
register: final_service
changed_when: false
- name: Validate final service
ansible.builtin.assert:
that:
- final_service.stdout == "active"
- name: Final application health check
ansible.builtin.uri:
url: "{{ health_url }}"
status_code: 200
handlers:
- name: Restart application
ansible.builtin.service:
name: "{{ application_service }}"
state: restarted
Este ejemplo es una base de diseño. Antes de utilizarlo en producción debemos adaptarlo al sistema operativo, aplicación, arquitectura, balanceadores, clústeres y procedimiento real de rollback.
15. Validar antes de ejecutar
Antes de una ventana de mantenimiento podemos revisar la sintaxis:
ansible-playbook \
-i inventory.ini \
patching.yml \
--syntax-check
Comprobar los hosts:
ansible-playbook \
-i inventory.ini \
patching.yml \
--list-hosts
Listar las tareas:
ansible-playbook \
-i inventory.ini \
patching.yml \
--list-tasks
Y cuando las tareas utilizadas soporten Check Mode:
ansible-playbook \
-i inventory.ini \
patching.yml \
--check
16. Primera ejecución: no empieces con 100 servidores
Aunque nuestro objetivo sean 100 hosts, una buena estrategia consiste en comenzar con un subconjunto controlado.
Por ejemplo:
ansible-playbook \
-i inventory.ini \
patching.yml \
--limit app001
Después podríamos utilizar un grupo piloto:
[canary]
app001
app002
Y ejecutar:
ansible-playbook \
-i inventory.ini \
patching.yml \
--limit canary
Una vez comprobado el resultado podemos continuar con el resto.
17. Una arquitectura todavía mejor: Load Balancer + Ansible
En sistemas realmente críticos, serial es solamente una parte de la solución.
Una estrategia más robusta sería:
LOAD BALANCER
│
┌─────────────┴─────────────┐
│ │
SERVIDORES SERVIDORES
DISPONIBLES EN PATCHING
│ │
│ 1. Drain node
│ 2. Stop app
│ 3. Patch
│ 4. Reboot
│ 5. Start app
│ 6. Health check
│ 7. Enable node
│ │
└─────────────◄─────────────┘
El procedimiento sería:
1. Seleccionar lote
↓
2. Sacar nodos del Load Balancer
↓
3. Esperar a que terminen conexiones
↓
4. Ejecutar pre-checks
↓
5. Aplicar patches
↓
6. Reiniciar
↓
7. Ejecutar health checks
↓
8. Regresar nodos al Load Balancer
↓
9. Ejecutar post-checks
↓
10. Continuar con siguiente lote
Este modelo resulta mucho más apropiado para aplicaciones que deben mantenerse disponibles durante toda la ventana de mantenimiento.
18. De 100 servidores a una estrategia de Rolling Maintenance
El concepto importante de este ejercicio no es solamente aprender:
serial: 10
Es cambiar nuestra forma de pensar sobre las actividades de mantenimiento.
En lugar de:
PATCH 100 SERVERS
podemos diseñar:
DISCOVER
↓
PRE-CHECK
↓
DRAIN
↓
PATCH
↓
REBOOT
↓
HEALTH CHECK
↓
RESTORE SERVICE
↓
POST-CHECK
↓
NEXT BATCH
De esta forma Ansible no solamente ejecuta comandos.
Ansible orquesta todo el procedimiento operacional.
Conclusión
Administrar 100 servidores Linux no significa que debamos modificar los 100 simultáneamente.
Utilizando:
serial
pre_tasks
health checks
handlers
block/rescue/always
rollback
post_tasks
podemos construir procedimientos de mantenimiento mucho más controlados.
serial nos permite dividir nuestra infraestructura en lotes; los pre-checks nos ayudan a comprobar que partimos de un estado saludable; los health checks verifican que la aplicación realmente regresó al servicio; los handlers controlan acciones derivadas de cambios; rescue proporciona una ruta para manejar errores y los post-checks confirman el estado final.
Pero existe un principio todavía más importante:
Automatizar una mala estrategia no la convierte en una buena estrategia.
Antes de automatizar un procedimiento de patching debemos comprender las dependencias de la aplicación, clúster, balanceadores, bases de datos, mecanismos de failover y procedimiento real de rollback.
Cuando combinamos ese conocimiento operacional con Ansible podemos pasar de simplemente ejecutar comandos sobre servidores a construir verdaderas estrategias de Rolling Maintenance automatizado para infraestructura de producción.
