Quien toma en serio la automatización en la operación diaria no puede ignorar un principio: la idempotencia. Se entiende que un Playbook al ejecutarse repetidamente deja el sistema en el mismo estado objetivo sin provocar cambios nuevos cada vez. Precisamente aquí se decide si Ansible funciona de forma fiable como gestión de configuración (estado en lugar de script único) o si se siente como una serie de llamadas Shell incontroladas. En este artículo se trata de poder diseñar Ansible-Playbooks idempotentes —práctico para administradores, ingenieros de sistemas y operadores: con handlers, modo de comprobación (Dry Run) y estrategias de rollback sólidas.
El foco está deliberadamente en la operación y la gestión de riesgos: ¿Cómo detecta si una tarea realmente cambia solo cuando hace falta? ¿Cómo evita reinicios de servicio innecesarios? ¿Cómo prueba cambios de forma segura en etapas y pipelines? ¿Y qué hace cuando un cambio se le escapa en el momento equivocado?
Por qué la idempotencia importa tanto en la operación
La automatización idempotente no es solo „código limpio“. Es una característica operativa. Si los Playbooks se pueden ejecutar repetidamente sin generar efectos secundarios, obtiene:
- Reproducibilidad: Un host vuelve al estado deseado tras una re-ejecución —importante después de parches, deriva de configuración o medidas de emergencia.
- Cambios predecibles: ‚changed‘ indica un cambio real, no solo una nueva ejecución.
- Control de ventanas de mantenimiento: Reinicios o recargas inesperadas no ocurren „porque sí“.
- Escalado más seguro: Lo que es estable en 3 sistemas tenderá a serlo también en 300, porque se reducen los efectos secundarios.
En la práctica, las causas más habituales de ejecuciones no idempotentes son sorprendentemente mundanas: se eligen módulos incorrectos (p. ej. shell en lugar de un módulo basado en estados), las tareas no comprueban correctamente el estado actual, o los servicios se reinician en cada ejecución.
Errores típicos: dónde se pierde la idempotencia
1) ’shell‘ y ‚command‘ como herramienta por defecto
command y shell son legítimos pero arriesgados. Son ‚imperativos‘ (ejecutar un comando) en lugar de ‚declarativos‘ (establecer el estado objetivo). Sin mecanismos de verificación adicionales, Ansible no sabe si el comando ha hecho algo. Resultado: las tareas informan ‚changed‘ en cada ejecución o causan efectos secundarios no deseados.
Si aun así necesita command/shell, tres cosas son obligatorias: creates/removes (guard basado en archivos), o condiciones claras de changed_when, y un comando idempotente en sí (p. ej. ‚apply only if missing‘).
2) Templates/Files sin una lógica de activación clara
Copiar archivos de configuración suele ser idempotente: el módulo template o copy calcula sumas de verificación y solo escribe en caso de diferencia. La pérdida de idempotencia suele no ocurrir en la tarea de archivo, sino después: cuando un reinicio de servicio no está vinculado a cambios.
3) ‚Always RESTart‘ en lugar de ‚RESTart only on change‘
Un reinicio de servicio es un evento operativo. Si ocurre en cada ejecución, en muchos entornos es inaceptable (disponibilidad, sesiones, colas, latencia). Para eso existen los handlers: solo se disparan cuando una tarea reporta realmente ‚changed‘.
4) Límites faltantes: orden, dependencias, estados parciales
Especialmente en entornos en la nube o híbridos, los playbooks se ejecutan contra sistemas heterogéneos. Un play puede quedar en un estado intermedio: paquete instalado, configuración parcialmente escrita, servicio no iniciado. Sin manejo de errores y sin un plan de rollback, la segunda ejecución no se recupera automáticamente.
Diseñar Playbooks de Ansible idempotentes: patrones básicos probados
Antes de profundizar en Handlers, el modo Check y los rollbacks, conviene un breve „plan de construcción“ para Roles idempotentes (Roles):
- Preferir módulos orientados al estado: package, service, template, lineinfile, user, cron, mount, sysctl usw.
- Variables claras y valores por defecto: Los roles deberían ser consistentes internamente; las sorpresas suelen surgir por valores por defecto implícitos.
- Grupos de tareas con block: Los cambios que pertenecen juntos deben ir en un bloque — incluyendo la ruta de error.
- Señales de cambio con moderación: changed_when/fail_when sólo donde sea realmente necesario y de forma lo más determinística posible.
Uso correcto de los Handlers: RESTarts, Reloads y «solo cuando sea necesario»
Handlers son tareas de Ansible que se ejecutan al final de un play (o tras meta: flush_handlers) y únicamente si han sido activadas mediante notify. Así enlaza usted las acciones operativas (RESTart/Reload) directamente con cambios reales.
Un patrón sólido de Handler para cambios de configuración
El siguiente ejemplo muestra la estructura típica en un Role: un cambio en un template dispara un Reload (o un RESTart). Es importante la decisión: Reload suele ser menos invasivo que RESTart, pero sólo funciona si el servicio soporta correctamente el Reload.
# tasks/main.yml
- name: Konfiguration ausrollen
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
owner: root
group: root
mode: '0644'
notify:
- myapp reload
- name: Service sicher aktiviert und gestartet
ansible.builtin.service:
name: myapp
state: started
enabled: true
# handlers/main.yml
- name: myapp reload
ansible.builtin.service:
name: myapp
state: reloaded
Por qué esto funciona: template es un módulo orientado al estado y reporta „changed“ sólo cuando el contenido realmente difiere. El Handler no se dispara „en cada ejecución“, sino únicamente ante un cambio real de configuración.
Flush Handlers: de forma dirigida, no por reflejo
Por defecto los Handlers se ejecutan al final del play. Eso suele ser correcto, pero puede ser tarde cuando hay dependencias: por ejemplo, si tras un cambio de configuración necesita ejecutar inmediatamente un health check contra el servicio, necesita que el Reload se haya realizado antes.
- name: Konfiguration ausrollen
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: myapp reload
- name: Handler jetzt ausführen, damit der Health-Check valide ist
ansible.builtin.meta: flush_handlers
- name: Health-Check (Beispiel: HTTP)
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
Riesgo: Un uso frecuente de flush_handlers reduce la consolidación (varios cambios generan múltiples recargas) y puede aumentar el tiempo de ejecución así como el alcance de las interrupciones. Regla práctica: ejecutar flush solo cuando las tareas posteriores requieran un estado de runtime actualizado.
Cascadas de handlers y patrón „listen“
En roles más grandes suele darse ambos casos: un handler de „Config change“ desencadena pasos adicionales (p. ej. systemd daemon-reload y luego reinicio del servicio). Utilice nombres de handlers claros y evite la „magia“ dentro de las tareas.
# handlers/main.yml
- name: systemd daemon-reload
ansible.builtin.systemd:
daemon_reload: true
- name: myapp RESTart
ansible.builtin.service:
name: myapp
state: RESTarted
# tasks/main.yml
- name: systemd unit ausrollen
ansible.builtin.template:
src: myapp.service.j2
dest: /etc/systemd/system/myapp.service
notify:
- systemd daemon-reload
- myapp RESTart
Modo Check (Dry Run) correctamente — y conocer sus límites
El modo Check (Ansible: –check) simula una ejecución y muestra lo que probablemente se cambiaría. Para la planificación de cambios, aprobaciones y CI es extremadamente útil. Al mismo tiempo, el modo Check no es una „prueba“ perfecta: algunos módulos no pueden predecir completamente el estado objetivo o necesitan cambios en vivo para determinar estados posteriores.
Flujo práctico: planificar, revisar, luego desplegar
Un proceso operativo probado es el siguiente:
- Modo Check con diff: ¿Qué archivos cambiarían? (Importante para revisiones.)
- Limit y serial: Primero un pequeño conjunto de hosts, luego ampliar.
- Ejecución normal: Con guards y handlers claros.
- Verificación: Health-Checks, estado del servicio, logs, y en su caso comprobaciones sintéticas.
Ejemplo de llamada para el modo Check con diff (muestra diferencias, p. ej., en plantillas):
ansible-playbook site.yml --check --diffDiseñar tareas compatibles con el modo Check
Muchos módulos de Ansible soportan el modo Check de forma nativa. Los problemas suelen surgir por comandos shell o por tareas que solo generan „conocimiento“ realizando cambios. Para esos casos hay dos estrategias habituales:
- Omitir el modo Check cuando una tarea no pueda simularse de forma significativa.
- Lógica de verificación alternativa en modo Check: p. ej., consultar el estado en lugar de cambiarlo.
Ejemplo: Una tarea que realiza una inicialización única no debe ejecutarse en modo de comprobación, pero debe indicar con claridad qué ocurriría.
- name: Datenbank initialisieren (nur wenn Marker fehlt)
ansible.builtin.command: /usr/local/bin/myapp-init-db
args:
creates: /var/lib/myapp/.db_initialized
register: initdb
changed_when: initdb.rc == 0
when: not ansible_check_mode
- name: Hinweis im Check-Modus
ansible.builtin.debug:
msg: "Check-Modus: DB-Init würde ggf. ausgeführt (Marker wird geprüft)."
when: ansible_check_mode
Importante: Esta estrategia es honesta. No pretende simular con seguridad un cambio, sino que expone explícitamente el riesgo residual.
Trampa: modo de comprobación y „notify“
En modo de comprobación, los cambios suelen simularse. Algunos handlers no se ejecutan igual que en una ejecución real o no dejan el mismo estado porque el servicio no se ha recargado realmente. Por eso planifique las validaciones de modo que no generen una «seguridad falsa» en el modo de comprobación. Para CI suele ser útil además ejecutar ejecuciones reales en un entorno de stage aislado.
changed_when und failed_when: Precisión en lugar de «siempre changed»
Las dos condiciones changed_when y failed_when son herramientas potentes para modelar con precisión el resultado de una tarea. Son especialmente relevantes para comandos cuyos códigos de salida o su salida no reflejan directamente «changed» frente a «ok».
Ejemplo: Comprobar en lugar de cambiar a ciegas
Un caso clásico es establecer una opción de sysctl. Aquí no debería escribir en /proc con shell, sino usar el módulo basado en estado. Si por motivos de plataforma debe usar un comando, la detección del cambio debe ser estable.
- name: Kernel-Parameter setzen (Beispiel)
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
state: present
reload: true
Este módulo es idempotente y compatible con el modo de comprobación. Use changed_when más bien como excepción, no como regla general.
Ejemplo: „grep“ como guardia — pero correctamente
Si trabaja con command, puede consultar primero el «estado actual». Debe tratar conscientemente los códigos de salida (por ejemplo, grep: 0 encontrado, 1 no encontrado, >1 error).
- name: Prüfen, ob Option in Datei vorhanden ist
ansible.builtin.command: grep -q '^OptionX=enabled$' /etc/myapp/app.conf
register: grep_result
changed_when: false
failed_when: grep_result.rc not in [0, 1]
- name: Option ergänzen, falls fehlend
ansible.builtin.lineinfile:
path: /etc/myapp/app.conf
line: 'OptionX=enabled'
create: false
when: grep_result.rc == 1
notify: myapp reload
Por qué es robusto: la tarea de comprobación nunca modifica nada y solo falla ante errores reales. El cambio lo realiza un módulo idempotente que, a su vez, solo activa el handler cuando es necesario.
Rollbacks seguros en Ansible: qué es realista (y qué no)
«Rollback» suena a un interruptor que lo deshace todo. En la práctica, la viabilidad depende en gran medida de qué tipo de cambio está desplegando:
- Archivos/Configuración: bien preparado para rollback (copias de seguridad, versiones anteriores, plantillas).
- Versiones de paquetes: posible, pero dependiente de los repositorios, el Pinning y las dependencias.
- Esquemas de base de datos/Migraciones de datos: a menudo solo seguro con una ruta de Down-Migration planificada con antelación o con RESTauración (Backup/PITR).
- Cambios distribuidos (clúster, colas de mensajes): el rollback suele requerir coordinación y orden.
El objetivo no es «rollback a cualquier precio», sino una estrategia de retroceso que funcione en producción: rápida, rastreable y comprobable.
Patrón 1: Copias de seguridad en archivos/plantillas – dirigidas y controladas
Para archivos de configuración, una copia de seguridad sencilla suele ser el mejor recurso. Módulos de Ansible como copy y template pueden crear copias. Importante: las copias de seguridad deben ser localizables y no deben llenar el disco sin control.
- name: Konfiguration ausrollen mit Backup
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
owner: root
group: root
mode: '0644'
backup: true
notify: myapp reload
Consejo práctico: añada una rutina de limpieza (p. ej. un concepto similar a logrotate o una tarea de limpieza propia) si se despliega con frecuencia. Alternativa: versione la configuración en Git y mantenga el rollback mediante releases definidos (Playbook-Variablen/Tags).
Patrón 2: block/rescue/always para transacciones pequeñas
Ansible ofrece manejo de errores estructurado con block, rescue y always. Con ello puede crear en un playbook una ruta de retroceso controlada. No sustituye a los snapshots de almacenamiento, pero es muy eficaz para «escribir la configuración + recargar el servicio + comprobar».
- block:
- name: Neue Konfiguration ausrollen (Backup aktiv)
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
backup: true
notify: myapp reload
- name: Handler jetzt ausführen
ansible.builtin.meta: flush_handlers
- name: Smoke-Test
ansible.builtin.uri:
url: http://127.0.0.1:8080/health
status_code: 200
rescue:
- name: Rollback-Hinweis
ansible.builtin.debug:
msg: "Smoke-Test fehlgeschlagen. Bitte Backup-Datei unter /etc/myapp/app.conf.* prüfen und ggf. zurückrollen."
- name: Play gezielt fehlschlagen lassen
ansible.builtin.fail:
msg: "Change abgebrochen: Dienst nach Konfig-Änderung nicht gesund."
always:
- name: Status protokollieren
ansible.builtin.debug:
msg: "Play abgeschlossen (ok/rollback je nach Verlauf)."
Por qué ayuda en producción: fuerza un momento de „Stop the line“ antes de que un estado defectuoso se propague a pasos posteriores (p. ej. balanceador de carga, otros nodos).
Patrón 3: Rollback mediante versionado y Pinning de paquetes
Cuando despliega versiones de software, el «rollback» suele ser un Downgrade. Eso solo funciona si:
- la versión antigua sigue disponible en el repositorio (o está en un repositorio/almacén de artefactos propio),
- las dependencias permanecen compatibles,
- que la configuración y el formato de datos no se hayan alterado de forma incompatible.
Operativamente suele ser conveniente fijar versiones de forma explícita (Pinning) y definir el rollback como «volver a la versión X». Eso es menos elegante que un «deshacer», pero planificable.
Pasos de comprobación y resolución de problemas: así detecta rápidamente la falta de idempotencia
1) Ejecución repetida como prueba
La prueba más sencilla es la más importante: ejecute el playbook dos veces seguidas. En la segunda ejecución solo deberían aparecer «ok» (y ningún Handler inesperado). Si en la segunda ejecución aparece todavía «changed», proceda tarea por tarea.
2) Usar diff y la verbosidad correctamente
Si hay archivos afectados, use diff en modo de comprobación (–check) o en ejecución real. En roles complejos ayuda aumentar la verbosidad para entender la resolución de variables y las condiciones.
ansible-playbook site.yml --check --diff -v3) Causas frecuentes de «changed en cada ejecución»
- Plantillas no deterministas: p. ej. marcas de tiempo o valores aleatorios en plantillas (provocan una nueva checksum).
- Permisos/propietario de archivos son modificados por otro proceso posteriormente (drift).
- Comandos sin guardas: command/shell sin creates/removes o sin un changed_when limpio.
- Tareas de servicio: state: RESTarted en lugar de started/reloaded + disparo de Handler.
- Errores de orden: la Tarea A cambia algo y la Tarea B lo revierte (ping-pong).
Lista de verificación: idempotencia, modo de comprobación y rollback antes de producción
- Prueba de idempotencia: ejecutar el playbook dos veces; la segunda ejecución no debe producir cambios inesperados.
- Handlers: reinicios/recargas únicamente mediante notify, salvo que esté justificado conscientemente.
- Modo de comprobación: los roles críticos se ejecutan con –check al menos hasta que sean planificables; las tareas no aptas para check están marcadas y justificadas.
- Guards: command/shell solo con creates/removes o con lógica clara de changed_when/failed_when.
- Smoke-Tests: tras cambios que afecten al runtime (puertos, autenticación, TLS, unidades systemd).
- Ruta de rollback: para configuración (backup/versionado), para versiones (Pinning), para datos (plan de backup/RESTore).
- Control de despliegue: serial, –limit, ventana de mantenimiento y criterios claros de abortar.
Estrategia de despliegue en la nube: serial, límites y control del radio de impacto
Especialmente en operación en la nube (pools dinámicos, autoscaling, Multi-AZ) es importante que los playbooks no solo sean idempotentes, sino que también limiten el radio de impacto. Dos palancas son especialmente prácticas:
- serial: desplegar cambios por host o en pequeños lotes.
- –limit: afectar de forma dirigida solo a grupos/hosts específicos (p. ej. nodo canario).
Esto no es solo una «opción de Ansible», sino una disciplina operativa: primero canary, luego ampliación, siempre con validación intermedia. La idempotencia garantiza que una ejecución posterior tras las correcciones no escale adicionalmente.
Conclusión: la idempotencia es su cinturón de seguridad — los Handlers y los rollbacks son los airbags
Si opera Ansible de forma consecuente como una herramienta basada en el estado, la idempotencia se convierte en la base para cambios fiables. Los Handlers permiten controlar las acciones sobre servicios y reducen los efectos secundarios. El Check-Modus es una palanca potente para planificación y revisión, siempre que sus límites sean transparentes. Y los rollbacks funcionan mejor cuando no se interpretan como un botón de „deshacer mágico“, sino como una ruta de retroceso planificada: copias de seguridad de configuración, releases versionados, pinning y pruebas de humo claras.
En el día a día esto se paga doble: menos sorpresas en las ventanas de mantenimiento y un análisis de fallos notablemente más rápido si algo sale mal. La prueba más importante sigue siendo simple: la segunda ejecución debe transcurrir sin cambios.
Para este tema son también importantes los Ansible Handlers y el Ansible Check-Modus. El artículo sitúa estos aspectos de forma comprensible y muestra en qué debe centrarse la práctica cotidiana.