Los flujos dentro de los playbooks de Ansible son fundamentales para organizar y secuenciar las tareas necesarias para alcanzar un estado deseado en los sistemas gestionados. Estas secuencias no solo permiten estructurar los pasos necesarios, sino que también permiten orquestar pasos que son dependientes de otros, manejar condiciones, ejecutar tareas en paralelo y garantizar que los resultados sean predecibles,mediante bucles, condicionales, bloques o manejo de errores.
En el post vamos a ver lo siguiente:
Bucles
Los bucles o loops, son útiles para ejecutar tareas repetitivas sobre un conjunto de elementos, como archivos, usuarios, paquetes o cualquier lista definida.
Ejemplos:
- Uso de whit_items
En el siguiente ejemplo, la tarea apt se ejecuta por cada elemento en la lista with_items
, instalando cada uno de los paquetes.
---
- name: Ejemplo de uso de with_items
hosts: all
tasks:
- name: Instalar varios paquetes
apt:
name: ""
state: present
with_items:
- git
- vim
- htop
- Uso de loop
A partir de la v2.5 se recomienda usar loop
en lugar de with_items
.
Las ventajas de usar loop:
- Simplicidad y consistencia
- Compatibilidad con nuevas funcionalidades
- Flexibilidad en datos estructurados
- Desambiguación de Múltiples Iteradores
A continuación el mismo ejemplo anterior pero con loop.
---
- name: Ejemplo de uso de loop
hosts: all
tasks:
- name: Instalar varios paquetes
apt:
name: ""
state: present
loop:
- git
- vim
- htop
- Anidar loops
La anidación de loops está permitida y se usa para iterar sobre listas de listas.
---
- name: Ejemplo de anidación de loops
hosts: all
tasks:
- name: Crear usuarios y asignarles grupos
user:
name: ""
groups: ""
state: present
loop:
- [ 'user1', 'group1' ]
- [ 'user2', 'group2' ]
- [ 'user3', 'group3' ]
- Loops con Diccionarios
Los loops también se pueden usar sobre diccionarios.
En el siguiente ejemplo, se usa dict2items
. Se trata de un filtro muy útil en Ansible para transformar un diccionario en una lista de elementos clave-valor. Este formato resultante es mucho más fácil de iterar en un bucle, ya que cada entrada se convierte en un objeto con atributos key y value
---
- name: Ejemplo de loops con diccionarios
hosts: all
vars:
users:
user1: "group1"
user2: "group2"
user3: "group3"
tasks:
- name: Crear usuarios y asignarles grupos desde diccionario
user:
name: ""
groups: ""
state: present
loop: ""
Usar dict2items es especialmente útil cuando trabajamos con diccionarios dinámicos o provenientes de datos externos, ya que facilita la iteración sobre pares clave-valor sin necesidad de transformaciones manuales adicionales.
- Condicionales en Loops
Se pueden combinar loops con condicionales para que se ejecuten tareas cuando una condición se cumpla.
En el ejemplo siguiente, los paquetes se instalan sólo si el sistema operativo pertenece a la familia Debian.
---
- name: Ejemplo de loops con condiciones
hosts: all
tasks:
- name: Instalar paquetes si no están ya instalados
apt:
name: ""
state: present
loop:
- git
- vim
- htop
when: ansible_facts['os_family'] == "Debian"
Condicionales
Los condicionales permiten ejecutar tareas en función del cumplimiento de ciertas condiciones, haciendo que los playbooks sean más dinámicos y adaptables. Usan la directiva when
, que evalúa expresiones basadas en variables, hechos (facts) recopilados por Ansible o cualquier lógica personalizada. Esto es especialmente útil para realizar tareas específicas solo en ciertos entornos, sistemas operativos o configuraciones.
- when
Es la más común. Se usa para ejecutar una tarea solo si una condición es verdadera
En el ejemplo siguiente, la tarea solo se ejecuta si el Sistema Operativo de destino pertenece a la familia Debian.
- name: Instalar Apache en servidores Debian
apt:
name: apache2
state: present
when: ansible_os_family == 'Debian'
- with items
Como vimos en el apartado de bucles, se puede usar condiciones con bucles.
En este ejemplo, se crearán los usuarios solo si su estado es ‘present’.
- name: Crear usuarios
user:
name: ""
state: present
when: item.state == 'present'
with_items:
- { name: 'john', state: 'present' }
- { name: 'doe', state: 'absent' }
NOTA: Se puede sustituir with_items por loop
- name: Crear usuarios
user:
name: ""
state: present
when: item.state == 'present'
loop:
- { name: 'john', state: 'present' }
- { name: 'doe', state: 'absent' }
- Operadores lógicos y de comparación
Podemos usar operadores lógicos para combinar múltiples condiciones.
El siguiente ejemplo, apache se reiniciará solo si el SO es de la familia Debian y la versión es la 10.
- name: Reiniciar el servicio si es necesario
service:
name: apache2
state: restarted
when: ansible_os_family == 'Debian' and ansible_distribution_version == '10'
Operadores Lógicos en Condicionales
and
Combina dos o más condiciones y evalúa como verdadero solo si todas las condiciones se cumplen. Ejemplo:
when: ansible_os_family == 'Debian' and ansible_distribution_version == '10'
or
Evalúa como verdadero si al menos una de las condiciones se cumple. Ejemplo:
when: ansible_os_family == 'Debian' or ansible_os_family == 'RedHat'
not
Invierte el resultado de una condición, evaluándola como falsa si era verdadera y viceversa. Ejemplo:
when: not ansible_os_family == 'Windows'
Paréntesis ()
Utilizados para agrupar condiciones y controlar el orden de evaluación. Esto es útil en expresiones complejas. Ejemplo:
when: (ansible_os_family == 'Debian' and ansible_distribution_version == '10') or ansible_os_family == 'RedHat'
Operadores de Comparación
Además de los operadores lógicos, Ansible también permite utilizar operadores de comparación dentro de las condiciones:
==
Igual a
when: ansible_os_family == 'Debian'
!=
Diferente de
when: ansible_os_family != 'Windows'
> / <
Mayor o menor que (útil para versiones numéricas)
when: ansible_distribution_version | int > 10
>= / <=
Mayor o igual / menor o igual que
when: ansible_distribution_version | int >= 10
- Registrar variables
En algunas ocasiones puede que necesitemos usar la salida de una tarea previa como una condición.
En el ejemplo siguiente, la segunda tarea solo se ejecutará si la salida de la primera es diferente de active.
- name: Obtener el estado del servicio
shell: systemctl is-active apache2
register: apache_status
- name: Reiniciar Apache si está inactivo
service:
name: apache2
state: restarted
when: apache_status.stdout != 'active'
El atributo register
permite almacenar la salida de una tarea como una variable en el contexto del playbook. Esta varible contiene:
-
stdout
: La salida estándar del comando ejecutado -
stderr
: La salida de errores, si los hay. -
rc
: El código de retorno del comando. Útil para saber si una tarea se completó con éxito o con fallo.
En nuestro ejemplo anterior:
apache_status.stdout
contiene la respuesta del comando systemctl is-active apache2, que debería ser ‘active’, ‘inactive’, etc.
La condición when: apache_status.stdout != 'active'
evalúa si el servicio no está activo y toma acción en consecuencia.
Bloques
Los bloques o blocks se usan para agrupar tareas y manejar errores de manera más eficiente.
Un bloque puede contener tres secciones:
block
: En donde se definen las tareas
rescue
: Es una sección opcional. En ésta se definen las tareas a ejecutar si alguna tarea del bloque principal falla
always
: Es otra sección opcional en la cual se definen las tareas que siempre deben ejecutarse, independientemente del éxito o fallo de las tareas del bloque principal o de rescate.
A continuación un ejemplo básico donde se ven las 3 secciones.
- name: Ejemplo de uso de blocks
hosts: localhost
tasks:
- name: Tarea que puede fallar
block:
- name: Crear un archivo
file:
path: /tmp/ejemplo.txt
state: touch
- name: Ejecutar comando
command: /bin/false
rescue:
- name: Notificar sobre el error
debug:
msg: "La tarea falló, ejecutando acciones de rescate."
always:
- name: Limpiar recursos
file:
path: /tmp/ejemplo.txt
state: absent
Manejo de errores
El manejo de errores es una parte esencial en la automatización con Ansible, ya que permite controlar cómo deben reaccionar los playbooks frente a fallos inesperados. Con herramientas como ignore_errors
, failed_when
, bloques rescue
y always
, entre otras, es posible personalizar la ejecución, garantizar la continuidad o realizar acciones correctivas para minimizar interrupciones y asegurar que las tareas críticas se cumplan de forma eficiente.
ignore_errors
El módulo ignore_errors te permite continuar la ejecución de un playbook incluso si una tarea falla. Esto puede ser útil cuando un fallo en una tarea no debería detener todo el playbook.
- name: This task will ignore any errors
command: /bin/false
ignore_errors: yes
failed_when
El módulo failed_when te permite definir condiciones personalizadas para determinar cuándo una tarea debería considerarse fallida.
- name: This task will fail if the output does not contain 'SUCCESS'
command: /bin/true
register: result
failed_when: "'SUCCESS' not in result.stdout"
rescue
y always
Ansible 2.0 introdujo los bloques block, rescue y always para manejar errores. Estos bloques permiten definir una lógica de recuperación cuando ocurren errores.
- name: Attempt a command that might fail
block:
- command: /bin/false
rescue:
- debug: msg="There was an error, but we recovered"
always:
- debug: msg="This task always runs"
handlers
y notify
Utiliza handlers y notify para manejar errores y notificaciones. Los handlers son tareas especiales que solo se ejecutan cuando son notificadas por otra tarea.
En el siguiente ejemplo, en la tarea principal, el módulo apt se utiliza para instalar el paquete nginx, y con la clave notify se configura que, tras la instalación, se active un handler llamado restart nginx. Este handler, definido más adelante en el playbook, emplea el módulo service para reiniciar el servicio nginx.
- name: Install a package
apt:
name: nginx
state: present
notify: restart nginx
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
until
y retries
El módulo until se utiliza para repetir una tarea hasta que se cumpla una condición específica, mientras que retries y delay controlan cuántas veces se intenta la tarea y cuánto tiempo se espera entre cada intento, respectivamente.
- name: Wait for the service to be available
command: curl http://localhost:8080
register: result
until: result.rc == 0
retries: 5
delay: 10
run_once
El módulo run_once garantiza que una tarea se ejecute solo una vez, sin importar cuántos hosts estén en juego. Esto es útil para evitar errores cuando solo necesitas que una acción se realice una vez.
- name: Run this task only once
command: /bin/true
run_once: true
max_fail_percentage
Con el módulo max_fail_percentage, puedes definir un umbral de porcentaje de fallos permitido. Si se excede este porcentaje, el playbook se detendrá.
- hosts: all
max_fail_percentage: 10
tasks:
- name: This task might fail
command: /bin/false
any_errors_fatal
El módulo any_errors_fatal detiene la ejecución en todos los hosts si un error ocurre en cualquier host.
- hosts: all
any_errors_fatal: true
tasks:
- name: This task will cause all hosts to stop if it fails
command: /bin/false
notify
con múltiples handlers
Puedes notificar múltiples handlers desde una misma tarea, lo que te permite manejar errores de manera más flexible y organizada.
En el ejemplo siguiente, la tarea principal utiliza el módulo apt para instalar el paquete nginx y, una vez completada, notifica a dos handlers: restart nginx y notify admin. El primer handler reinicia el servicio nginx utilizando el módulo service, mientras que el segundo envía un correo electrónico al administrador (admin@example.com) para informar que el servicio ha sido reiniciado.
- name: Install a package
apt:
name: nginx
state: present
notify:
- restart nginx
- notify admin
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
- name: notify admin
mail:
to: "admin@example.com"
subject: "nginx was restarted"