Playbook de Ansible para aplicar patching a 100 servidores Linux por lotes sin provocar una caída total del servicio

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.

Deja un comentario

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