Este es uno de los capítulos del tutorial Automatización con Ansible. Una introducción.. Encontrarás los enlaces a todos los de capítulos, al final de este artículo.
Hasta el momento en este tutorial de Ansible has visto sobre los playbooks, play y las tareas. Sin embargo, existe un concepto intermedio entre un play y una task, que son los bloques o blocks. Esta nueva unidad te permite agrupar diferentes tareas en un mismo play. Un bloque, como verás a continuación, te permite realizar prácticamente las mismas operaciones que realizas con las tareas a excepción de los bucles. Así, en este nuevo capítulo del tutorial, trataremos los bloques en Ansible.
Pero ¿que sentido tienen los bloques en Ansible? Como he dicho, se trata de agrupar tareas, a las que puedes aplicar propiedades, información o incluso directivas comunes. Esto implica que esas propiedades, información y directivas, que defines por bloque se aplicarán a cada uno de las tareas que formen parte del bloque. Así, por ejemplo, puedes definir un bloque constituidos por tareas, que únicamente se apliquen en el caso de que la distribución sea Ubuntu, ansible_facts.distribution == 'Ubuntu
.
Bloques en Ansible
Como te decía en la introducción, los bloques en Ansible, son una entidad, que te permite agrupar tareas dentro de un juego para aplicar determinadas configuraciones y condiciones.
Puedes aplicar prácticamente las mismas opciones y acciones que en el caso de las tareas, a excepción de los bucles. Y es que los bloques no soportan loops.
Un ejemplo de bloque lo tienes a continuación,
---
- hosts: all
order: sorted
gather_facts: yes
tasks:
- name: Lista un directorio según la distribución
block:
- name: lista el directorio raíz
shell: ls -la /
register: result
- name: print result
debug:
msg: "{{ result }}"
when: (ansible_facts.distribution == 'Ubuntu') and
(ansible_facts.os_family == 'Debian')
ignore_errors: yes
remote_user: root
Como ves en el ejemplo anterior, solo se ejecutará el bloque en aquellas máquinas cuya distribución sea Ubuntu. Además, el bloque tiene dos configuraciones adicionales,
- el usuario remoto es
root
- he incluido la cláusula
ignore_errors: yes
, de forma que aunque alguna de las tareas falle, continuará la ejecución delplaybook
.
Gestión de errores con bloques
Quizá, uno de los aspectos mas interesantes que ofrecen los bloques es precisamente la gestión de errores. Así, tienes una primera acción que se ejecutará en caso de que se produzca un error. Se trata de rescue
. Para el ejemplo anterior, podrías hacer algo como lo que te muestro a continuación,
---
- hosts: all
order: sorted
gather_facts: yes
tasks:
- name: Hace un ping en algunos casos
block:
- name: lista el directorio /etz
shell: ls -la /etz
register: result
- name: print result
debug:
msg: "{{ result }}"
rescue:
- name: en caso de error
debug:
msg: "He capturado un error"
- name: lista el directorio /etc
shell: ls -la /etc | tail -1
register: result
- name: print result
debug:
msg: "{{ result.stdout }}"
when: (ansible_facts.distribution == 'Ubuntu') and
(ansible_facts.os_family == 'Debian')
ignore_errors: no
remote_user: root
Ahora toca ejecutar este playbook, para comentar la jugada. Para ello, ejecuta la siguiente instrucción,
ansible-playbook -i inv playbooks/sample05.yml
Y esto, da como resultado lo siguiente,
PLAY [all] ************************************************
TASK [Gathering Facts] ************************************
ok: [do01]
ok: [co01]
TASK [lista el directorio /etz] ***************************
fatal: [co01]: FAILED! => {"changed": true, "cmd": "ls -la /etz", "delta": "0:00:00.006586", "end": "2020-07-19 21:35:16.870630", "msg": "non-zero return code", "rc": 2, "start": "2020-07-19 21:35:16.864044", "stderr": "ls: cannot access '/etz': No such file or directory", "stderr_lines": ["ls: cannot access '/etz': No such file or directory"], "stdout": "", "stdout_lines": []}
fatal: [do01]: FAILED! => {"changed": true, "cmd": "ls -la /etz", "delta": "0:00:00.008679", "end": "2020-07-19 19:35:17.066975", "msg": "non-zero return code", "rc": 2, "start": "2020-07-19 19:35:17.058296", "stderr": "ls: cannot access '/etz': No such file or directory", "stderr_lines": ["ls: cannot access '/etz': No such file or directory"], "stdout": "", "stdout_lines": []}
TASK [en caso de error] ***********************************
ok: [co01] => {
"msg": "He capturado un error"
}
ok: [do01] => {
"msg": "He capturado un error"
}
TASK [lista el directorio /etc] ***************************
changed: [co01]
changed: [do01]
TASK [print result] ***************************************
ok: [co01] => {
"msg": "-rw-r--r-- 1 root root 477 Mar 16 2018 zsh_command_not_found"
}
ok: [do01] => {
"msg": "-rw-r--r-- 1 root root 477 Mar 16 2018 zsh_command_not_found"
}
PLAY RECAP ************************************************
co01: ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
do01: ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0
Como tu mismo puedes observar, al no encontrar el directorio /etz
, ejecuta la acción rescue
. En este caso, esta acción está compuesta de tres tareas. La primera simplemente consiste en indicar que se ha capturado un error, la siguiente es listar el contenido del directorio correcto /etc
y por último mostrar el resultado utilizando resoult.stdout
.
Ejecutando tareas en cualquier caso
Pero ¿que sucede si quieres que una determinada tarea o conjunto de tareas se ejecuten siempre? Con independencia de que las tareas del mencionado bloque, hayan dado, o no hayan dado error, tu quieres que determinadas tareas se ejecuten. En este caso tienes la opción always
. Por ejemplo, puedes probar con el siguiente playbook,
---
- hosts: all
order: sorted
gather_facts: no
tasks:
- name: probando always
block:
- name: esta tarea se ejecuta
debug:
msg: "Esta tarea se ejecuta"
- name: esta tarea da error
shell: /bin/false
- name: esta tarea no se ejecuta
debug:
msg: "Esta tarea no se ejecuta"
always:
- name: esta tarea se ejecuta siempre
debug:
msg: "Esta tarea se ejecuta siempre"
ignore_errors: no
remote_user: root
Ahora solo tienes que ejecutarlo en los hosts que tu consideres,
ansible-playbook -i inv playbooks/sample06.yml --limit co01,do01
Y el resultado que vas a obtener, es algo parecido al que me ha dado a mi,
PLAY [all]
TASK [esta tarea se ejecuta]
ok: [co01] => {
"msg": "Esta tarea se ejecuta"
}
ok: [do01] => {
"msg": "Esta tarea se ejecuta"
}
TASK [esta tarea da error]
fatal: [co01]: FAILED! => {"changed": true, "cmd": "/bin/false", "delta": "0:00:00.005079", "end": "2020-07-20 06:07:29.816472", "msg": "non-zero return code", "rc": 1, "start": "2020-07-20 06:07:29.811393", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
fatal: [do01]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": true, "cmd": "/bin/false", "delta": "0:00:00.008330", "end": "2020-07-20 04:07:30.303517", "msg": "non-zero return code", "rc": 1, "start": "2020-07-20 04:07:30.295187", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
TASK [esta tarea se ejecuta siempre]
ok: [co01] => {
"msg": "Esta tarea se ejecuta siempre"
}
ok: [do01] => {
"msg": "Esta tarea se ejecuta siempre"
}
PLAY RECAP
co01: ok=2 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
do01: ok=2 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Combinando ambas acciones
Por supuesto, y como bien te podías imaginar, es posible combinar ambas acciones, tanto rescue
como always
. De esta manera, es posible reaccionar ante aquellas tareas que no se ejecuten, y por otro lado, nos aseguramos de que las tareas que queramos, sean ejecutadas en cualquier caso.
---
- hosts: all
order: sorted
gather_facts: no
tasks:
- name: probando always
block:
- name: esta tarea se ejecuta
debug:
msg: "Esta tarea se ejecuta"
- name: esta tarea da error
shell: /bin/false
- name: esta tarea no se ejecuta
debug:
msg: "Esta tarea no se ejecuta"
rescue:
- name: he intentado recuperar esta tarea
debug:
msg: "He intentado ejecutar esta tarea"
always:
- name: esta tarea se ejecuta siempre
debug:
msg: "Esta tarea se ejecuta siempre"
ignore_errors: no
remote_user: root
Conclusiones
Esto de los bloques es una solución ideal para trabajar con diferentes distribuciones, o incluso con distintos sistemas operativos, donde a cada uno le tienes que aplicar las tareas de una forma diferente. Es una interesante forma de agrupar tareas.
Por otro lado, la gestión de errores es algo que hay que tener presente, para poder modificar nuestras acciones en función de la respuesta que tengas del host, con el que estés trabajando.
Imagen de portada de Christian Fregnan en Unsplash