Ansible block and rescue for error handling with examples


Written By - admin
Advertisement

What is Ansible Block?

In Ansible, a block is a grouping mechanism for multiple tasks. It allows you to apply particular directives, like error handling or looping, to several tasks collectively, instead of applying them to each task individually. Essentially, a block provides a structured way of handling a batch of tasks as a single unit within an Ansible playbook.

The primary use cases include:

  1. Grouping Multiple Tasks: Blocks help in organizing your playbook by grouping related tasks. This enhances readability and makes the playbook easier to maintain.
  2. Error Handling: With the rescue and always sections, blocks facilitate structured error handling. If any task within a block fails, the tasks within the rescue section are executed. The always section ensures certain tasks are run regardless of success or failure in the main block or rescue section.
  3. Variable Scope: Variables defined in a block are scoped to that block, which can be useful for temporary values or overriding global values for a specific set of tasks.
  4. Applying Directives: Directives like tags, become, or when can be applied to an entire block, making it more efficient than applying them to individual tasks.
  5. Looping: If you need to run a series of tasks multiple times with different data, you can loop over an entire block rather than just single tasks.

 

Syntax and Structure

A block is structured in the Ansible playbook by enclosing a set of tasks within the block keyword.

---
- hosts: all
  tasks:
    - name: This is the start of a block
      block:
        - name: Task 1 inside block
          command: echo "Hello from Task 1"

        - name: Task 2 inside block
          command: echo "Hello from Task 2"

      # Optional parameters
      become: yes
      when: some_condition is true
      tags: ['block_tag']
      ignore_errors: yes
      any_other_task_level_parameter: value

Mandatory:

  • block: This keyword is mandatory, and it encapsulates the tasks you want to group together.

Optional:

rescue: This section contains tasks that should be executed if any of the tasks inside the main block fail

rescue:
  - name: This task will run only if a task inside the block fails
    command: /path/to/some_command

always: This section contains tasks that will always be executed after the block and rescue sections, regardless of whether the tasks in the block were successful or not.

always:
  - name: This task will always run
    command: /path/to/another_command

Task-level Parameters: Any task-level parameter (like become, when, tags, ignore_errors, etc.) that you'd normally apply to an individual task can also be applied to a block. When applied to a block, these parameters are inherited by all tasks within that block. This allows you to avoid repetitive code by applying common parameters to a group of tasks at once.

Advertisement

 

Error handling in Ansible Blocks

Error handling is one of the most beneficial features offered by blocks in Ansible. By using the rescue and always clauses, you can control the flow of your playbook's execution based on the outcome of tasks within a block.

 

rescue Clause

The rescue section within a block is used to specify tasks that should be run if any of the tasks within the main block encounter an error or fail to execute.

How it helps handle errors:

  1. Fallback Mechanism: If there's a potential for failure in certain tasks, the rescue section can provide alternative actions or fallback mechanisms. For example, if you're trying to deploy an application and it fails, the rescue section can be used to roll back to the previous state.
  2. Diagnostic Information: If a task within the block fails, tasks within the rescue clause can be used to gather diagnostic information, log errors, or send alerts/notifications.
  3. Conditional Execution: The rescue section ensures that its tasks are only executed upon a failure in the block, enabling condition-based logic in the playbook flow.

 

always Clause

The always section contains tasks that will be executed after the main block and rescue sections, regardless of whether the tasks in the block were successful or not.

Ensuring certain tasks run no matter what:

  1. Cleanup Tasks: It's common to have cleanup operations, like removing temporary files or shutting down services, which need to run irrespective of success or failure. The always section is ideal for such tasks.
  2. Notifications: You may want to notify stakeholders or systems about the completion of a set of tasks, irrespective of their outcome. Placing notification tasks in the always section ensures they are triggered every time.
  3. Logging and Auditing: If you want to maintain a consistent log of playbook runs or perform auditing, tasks for these purposes can be placed in the always section.

Here's a basic structure showing a block with rescue and always clauses:

tasks:
  - name: Main block of tasks
    block:
      - name: Task that might fail
        command: /path/to/some_command

    rescue:
      - name: Task to run on failure
        command: /path/to/fallback_command

    always:
      - name: Task that will always run
        command: /path/to/cleanup_or_log_command

 

 

Example-1: Why we should use blocks

I have written a small playbook ansible-blocks-1.yml which will help you understand how ansible blocks can be useful and why it should be used. In this playbook we have 4 tasks wherein 3 tasks requires root level privilege while one of the task can be executed without root level privilege.

Now I have used become: yes 3 times which is just increasing the number of lines in the playbook.

---
 - name: Ansible Blocks
   hosts: server1
   gather_facts: false

   tasks:
     - name: List usr directory content
       command: "ls -l /usr/"
       become: yes

     - name: List root partition content
       command: "ls -l /root/"
       become: yes

     - name: List ansible user's home directory content
       command: "ls -l ~/"

     - name: List bin diretcory content
       command: "ls -l /bin/"
       become: yes

We could have added become: yes in the starting of the play but then that variable will be applied to the entire play wherein we don't want this permission for task 3. To overcome this we have block level solution wherein we can place all the common tasks under single block.

 

This is how the playbook would look with blocks. Here we have combined all the tasks which requires root level privilege into single block and instead of using become 3 times, we have applied this to the entire block.

---
 - name: Ansible Blocks
   hosts: server1
   gather_facts: false

   tasks:
     - block:
        - name: List usr directory content
          command: "ls -l /usr/"

        - name: List root partition content
          command: "ls -l /root/"

        - name: List bin directory content
          command: "ls -l /bin/"
       become: yes

     - name: List ansible user's home directory content
       command: "ls -l ~/"
NOTE:

Observe the alignment which is important. The become key has same number of whitespace compared to block so the become value is applicable for the entire block.

Now we can execute the playbook:

[ansible@controller ~]$ ansible-playbook ansible-blocks-1.yml

PLAY [Ansible Blocks] ************************************************************************************************

TASK [List usr directory content] ************************************************************************************
changed: [server1]

TASK [List root partition content] ***********************************************************************************
changed: [server1]

TASK [List bin directory content] ************************************************************************************
changed: [server1]

TASK [List ansible user's home directory content] ********************************************************************
changed: [server1]

PLAY RECAP ***********************************************************************************************************
server1                    : ok=4    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

 

Example-2: Perform error recovery with rescue block

We have additional benefit while using ansible blocks to perform recovery operation i.e. if any of the tasks within a block fails then in a ideal scenario the playbook would exit (unless you are using ignore_errors: yes). But with blocks we can assign a rescue block which can again contain a bunch of tasks. So if any of the tasks within block fails then automatically the tasks from recovery block will be executed which can perform any clean up activity which you would want to do in case of failure.

Lastly we also have an always block which will be called independent of the task execution status which can be used to give something like SUMMARY or anything as per your requirement.

In this sample playbook ansible-blocks-2.yml we have created two tasks inside the block wherein one task will pass while the other is expected to fail. In which case the rescue block will be executed followed by the always block.

---
 - name: Ansible Blocks
   hosts: server1
   gather_facts: false

   tasks:
     - block:
        - name: List home directory content
          command: "ls -l ~/"

        - name: Failing intentionally
          command: "ls -l /tmp/does-not-exist"

       rescue:
        - name: Rescue block (perform recovery)
          debug:
            msg: "Something went wrong, cleaning up.."

       always:
        - name: This will execute always
          debug:
            msg: "I will execute even in failure scenario"

Let us execute this playbook:

[ansible@controller ~]$ ansible-playbook ansible-blocks-2.yml

PLAY [Ansible Blocks] ************************************************************************************************

TASK [List home directory content] ***********************************************************************************
changed: [server1]

TASK [Failing intentionally] *****************************************************************************************
fatal: [server1]: FAILED! => {"changed": true, "cmd": ["ls", "-l", "/tmp/does-not-exist"], "delta": "0:00:00.003050", "end": "2020-09-26 10:29:13.864820", "msg": "non-zero return code", "rc": 2, "start": "2020-09-26 10:29:13.861770", "stderr": "ls: cannot access '/tmp/does-not-exist': No such file or directory", "stderr_lines": ["ls: cannot access '/tmp/does-not-exist': No such file or directory"], "stdout": "", "stdout_lines": []}

TASK [Rescue block (perform recovery)] *******************************************************************************
ok: [server1] => {
    "msg": "Something went wrong, cleaning up.."
}

TASK [This will execute always] **************************************************************************************
ok: [server1] => {
    "msg": "I will execute even in failure scenario"
}

PLAY RECAP ***********************************************************************************************************
server1                    : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

As you can see, our task1 was successful but task2 has failed after which the recovery block was executed followed by always block.

 

Now assuming none of the tasks failed in the block section in which case the rescue block will not be executed, but always block will be executed independent of the task status.

I have updated my playbook to list /tmp directory content so that the second task does not fail, let's observe the output after execution:

[ansible@controller ~]$ ansible-playbook ansible-blocks-2.yml

PLAY [Ansible Blocks] ************************************************************************************************

TASK [List home directory content] ***********************************************************************************
changed: [server1]

TASK [List tmp directory content] ************************************************************************************
changed: [server1]

TASK [This will execute always] **************************************************************************************
ok: [server1] => {
    "msg": "I will execute even in failure scenario"
}

PLAY RECAP ***********************************************************************************************************
server1                    : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

So this time the rescue block has not been called but always block was executed as expected.

 

Example-3: Practical example of error and recovery with blocks

Now that we know the concept of block and rescue let's use this in practical use case. I will create one playbook which will perform the following task under our block

  • Install vsftpd package
  • Take a backup of /etc/vsftpd/vsftpd.conf on the same managed node
  • Copy vsftpd.j2 jinja template from controller node to the managed node and replace /etc/vsftpd/vsftpd.conf
  • Intentionally fail the block by trying to access incorrect location

Next I will have a rescue block so that if something goes wrong we will perform some clean up/fallback. In this I have two tasks:

  • Print a message on the console for the recovery
  • Restore vsftpd.conf using the backup file vsftpd.conf.bkp on the managed node

Lastly we will have always block will restart the vsftpd service independent of the task execution status

This is our playbook ansible-blocks-3.yml:

---
 - name: Install vsftpd
   hosts: server1
   become: yes
   vars:
     anonymous_enable: yes
     local_enable: yes
     write_enable: yes
     anon_upload_enable: yes
   tasks:
     - block:
        - name: install vsftp
          yum:
            name: vsftpd
        - name: take backup of existing config
          copy:
            src: /etc/vsftpd/vsftpd.conf
            dest: /etc/vsftpd/vsftpd.conf.bkp
            remote_src: yes
        - name: use Jinja2 template to configure vsftpd
          template:
            src: vsftpd.j2
            dest: /etc/vsftpd/vsftpd.conf
        - name: This will fail
          command: "ls -l /tmp/does-not-exist"

       rescue:
        - name: Recovery block
          debug:
            msg: "something failed, restoring vsftpd.conf from backup"
        - name: Restoring vsftpd.conf
          copy:
            src: /etc/vsftpd/vsftpd.conf.bkp
            dest: /etc/vsftpd/vsftpd.conf
            remote_src: yes

       always:
        - name: Restarting vsftpd
          service:
            name: vsftpd
            state: restarted

This is our sample jinj2 template. We had used the same template while working with Jinja2 templates chapter earlier.

[ansible@controller ~]$ cat vsftpd.j2
anonymous_enable={{ anonymous_enable }}
local_enable={{ local_enable }}
write_enable={{ write_enable }}
anon_upload_enable={{ anon_upload_enable }}
dirmessage_enable=YES
xferlog_enable=YES
connect_from_port_20=YES
pam_service_name=vsftpd
userlist_enable=YES
# MY IP Address={{ ansible_facts['default_ipv4']['address'] }}

Let us execute this playbook and verify the steps:

[ansible@controller ~]$ ansible-playbook ansible-blocks-3.yml

PLAY [Install vsftpd] ************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [server1]

TASK [install vsftp] *************************************************************************************************
ok: [server1]

TASK [take backup of existing config] ********************************************************************************
changed: [server1]

TASK [use Jinja2 template to configure vsftpd] ***********************************************************************
changed: [server1]

TASK [This will fail] ************************************************************************************************
fatal: [server1]: FAILED! => {"changed": true, "cmd": ["ls", "-l", "/tmp/does-not-exist"], "delta": "0:00:00.003701",                                                                                                                        "end": "2020-09-26 10:56:49.076834", "msg": "non-zero return code", "rc": 2, "start": "2020-09-26 10:56:49.073133", "s                                                                                                                       tderr": "ls: cannot access '/tmp/does-not-exist': No such file or directory", "stderr_lines": ["ls: cannot access '/tm                                                                                                                       p/does-not-exist': No such file or directory"], "stdout": "", "stdout_lines": []}

TASK [Recovery block] ************************************************************************************************
ok: [server1] => {
    "msg": "something failed, restoring vsftpd.conf from backup"
}

TASK [Restoring vsftpd.conf] *****************************************************************************************
changed: [server1]

TASK [Restarting vsftpd] *********************************************************************************************
changed: [server1]

PLAY RECAP ***********************************************************************************************************
server1                    : ok=7    changed=4    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

As expected TASK 1 and TASK 2 have successfully executed while TASK 3 has failed. Since one of the task in our block has failed, the rescue block is executed which will restore our vsftpd.conf file. Lastly the always block is executed to restart the vsftpd service.

 

We can also connect to server1 and check if vsftpd.conf.bkp is created:

[ansible@server-1 ~]$ sudo ls -l /etc/vsftpd/vsftpd.conf*
-rw------- 1 root root 5098 May 14  2019 /etc/vsftpd/vsftpd.conf
-rw-r--r-- 1 root root 5098 May 14  2019 /etc/vsftpd/vsftpd.conf.bkp

 

Example-4: How Loops Can Be Integrated Within Blocks

In Ansible, the versatility of loops combined with task includes provides a mechanism to handle scenarios where we want to execute a series of tasks iteratively based on a dataset. Though block doesn't natively support the loop attribute, this combination of loops with include_tasks allows for the repetition of multiple tasks for each item in a given list.

You have a list of users and their respective groups. For each user in this list, you want to:

  1. Ensure their group exists.
  2. Create the user.
  3. Add the user to their respective group.

Main Playbook:

Your main playbook create-user.yaml sets the stage by looping through your list of users and including a separate task file for each user.

---
- hosts: all
  tasks:
    - name: Create users and add them to groups
      include_tasks: user_tasks.yml
      loop:
        - { name: 'alice', group: 'admins' }
        - { name: 'bob', group: 'users' }
        - { name: 'charlie', group: 'developers' }

Included Tasks File (user_tasks.yml):

This is where the sequence of tasks is defined for each user in the loop:

---
- name: Ensure group "{{ item.group }}" exists
  group:
    name: "{{ item.group }}"
    state: present

- name: Create user
  user:
    name: "{{ item.name }}"
    state: present

- name: Add user to group
  user:
    name: "{{ item.name }}"
    groups: "{{ item.group }}"
    append: yes

How It Works:

  1. Looping Through the Dataset: The main playbook starts by looping through the list of users. For each user, it includes user_tasks.yml.
  2. Ensuring the Group Exists: For every iteration (i.e., for each user), the first task in user_tasks.yml checks if the group exists. If not, it creates the group.
  3. Creating the User: The next task ensures the user exists.
  4. Adding User to Group: The final task adds the user to the group. The append: yes directive ensures the user is added to the new group without removing them from any other existing groups.
root@host01:~$ ansible-playbook create-user.yaml 

PLAY [all] ********************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************
ok: [host02]

TASK [Create users and add them to groups] ************************************************************************************************
included: /root/user_tasks.yaml for host02 => (item={'name': 'alice', 'group': 'admins'})
included: /root/user_tasks.yaml for host02 => (item={'name': 'bob', 'group': 'users'})
included: /root/user_tasks.yaml for host02 => (item={'name': 'charlie', 'group': 'developers'})

TASK [Ensure group "admins" exists] *******************************************************************************************************
changed: [host02]

TASK [Create user] ************************************************************************************************************************
ok: [host02]

TASK [Add user to group] ******************************************************************************************************************
changed: [host02]

TASK [Ensure group "users" exists] ********************************************************************************************************
ok: [host02]

TASK [Create user] ************************************************************************************************************************
changed: [host02]

TASK [Add user to group] ******************************************************************************************************************
changed: [host02]

TASK [Ensure group "developers" exists] ***************************************************************************************************
changed: [host02]

TASK [Create user] ************************************************************************************************************************
changed: [host02]

TASK [Add user to group] ******************************************************************************************************************
changed: [host02]

PLAY RECAP ********************************************************************************************************************************
host02                     : ok=13   changed=7    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

 

Example-5: Using variables inside Blocks

In Ansible, variables serve as placeholders for values, enabling playbooks to be more dynamic and reusable. The use of variables within blocks offers a structured approach to group related tasks and their configurations, enhancing readability and maintainability. In this example, we'll explore how to effectively utilize variables inside a block.

Our sample playbook aims to set up the Nginx web server on a target machine, place a custom welcome page, and ensure the service is running.

---
- hosts: all
  become: yes
  tasks:
    - name: Set up specific application environment
      block:
        - name: Install specific package
          apt:
            name: "{{ package_name }}"
            state: present

        - name: Place custom application page
          template:
            src: "{{ template_src }}"
            dest: "{{ template_dest }}"

        - name: Ensure application service is running
          service:
            name: "{{ service_name }}"
            state: started
            enabled: yes
      vars:
        package_name: "nginx"
        template_src: "index.html.j2"
        template_dest: "/var/www/html/index.html"
        service_name: "nginx"
        app_name: "My Web Application"

Here is my index.html.j2

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome to {{ app_name }}</title>
</head>
<body>
    <h1>Welcome to {{ app_name }}</h1>
    <p>This page is served by Nginx!</p>
</body>
</html>

Variables in Block:

  • The vars keyword within the block allows us to define a set of variables scoped to the tasks inside that block.
  • Variables such as {{ package_name }}, {{ template_src }}, {{ template_dest }}, and {{ service_name }} are introduced in the vars section and utilized in the tasks.
  • These variables are specific to this block, which means even if there were other blocks or tasks in this playbook using the same variable names, they wouldn’t conflict or overwrite these.

Let's execute this playbook:

root@host01:~$ ansible-playbook web-server.yml 

PLAY [all] ********************************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************
ok: [host02]

TASK [Install specific package] ***********************************************************************************************************
ok: [host02]

TASK [Place custom application page] ******************************************************************************************************
changed: [host02]

TASK [Ensure application service is running] **********************************************************************************************
ok: [host02]

PLAY RECAP ********************************************************************************************************************************
host02                     : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

You can verify if the tasks worked successfully by following these methods:

SSH into the target host and run the following command to check the status of Nginx:

root@host02:~$ sudo systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2023-08-20 05:20:39 UTC; 6min ago
       Docs: man:nginx(8)
    Process: 3104 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
    Process: 3105 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
   Main PID: 3195 (nginx)
      Tasks: 3 (limit: 1585)
     Memory: 7.2M
     CGroup: /system.slice/nginx.service
             ├─3195 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
             ├─3198 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""
             └─3199 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""

Aug 20 05:20:39 host02 systemd[1]: Starting A high performance web server and a reverse proxy server...
Aug 20 05:20:39 host02 systemd[1]: Started A high performance web server and a reverse proxy server.

Check the contents of the file you placed with the template module. It should match the content you defined in your index.html.j2 template, with the variable {{ app_name }} replaced by "My Web Application".

root@host02:~$ cat /var/www/html/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome to My Web Application</title>
</head>
<body>
    <h1>Welcome to My Web Application</h1>
    <p>This page is served by Nginx!</p>
</body>
</html>

 

Summary

Ansible blocks represent a significant advancement in playbook design, offering enhanced readability, structure, and error management. By enabling users to group related tasks, handle errors gracefully with the rescue and always clauses, scope variables to specific contexts, and integrate loops effectively, blocks augment playbook versatility and robustness. As the complexity of infrastructure automation grows, such features are invaluable. Through the appropriate use of blocks, Ansible users can ensure that their playbooks remain not just functional, but also maintainable and organized, setting the foundation for scalable and efficient infrastructure automation.

 

What’s Next

Next in our Ansible Tutorial we will learn about include and import module in Ansible with mutliple examples.

 

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment