Bloques en Ansible

Automatización con Ansible. Una introducción.

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 de Ansible

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 del playbook.

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

Deja una respuesta

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