Introduction to Ansible Block and Rescue
Ansible Block and Rescue are powerful features in Ansible, a popular automation tool, which are used for grouping tasks and handling errors in playbooks. They offer an efficient way to manage complex automation scripts, making them more readable, maintainable, and resilient to failures. Let's delve into each of these concepts in detail.
What is Ansible Block?
An Ansible Block is a mechanism to group multiple tasks together in an Ansible playbook. The primary purpose of a block is to create logical groupings of tasks, which can be treated as a single unit. This is particularly useful for organizing complex playbooks by breaking them down into smaller, more manageable parts.
Syntax and Structure
A block is structured in the Ansible playbook by enclosing a set of tasks within the block keyword.
- name: Example of an Ansible Block
block:
- name: Task 1
module: module_name
args:
key: value
- name: Task 2
module: module_name
args:
key: value
rescue:
# Rescue tasks go here
always:
# Always tasks go here
What is Rescue in Ansible?
Rescue in Ansible is part of the Ansible Block and Rescue framework, used for error handling. It specifies a set of tasks that should be executed if an error occurs in any of the tasks within a block. The rescue section is akin to an exception handling mechanism found in many programming languages.
Syntax: The rescue section is included within the block structure as shown earlier. Here's an example focusing on the rescue part:
- name: Ansible Block with Rescue
block:
# Tasks that may fail
rescue:
- name: Recovery Task
module: module_name
args:
key: value
Grouping Tasks with Blocks in Ansible Block and Rescue
Grouping tasks using blocks in Ansible Block and Rescue is a fundamental technique that enhances the structure and readability of playbooks. It involves organizing related tasks into logical units, making the playbook easier to maintain and understand.
How to Group Multiple Tasks Using Blocks
Concept: In Ansible, blocks are used to group multiple tasks together. This grouping is not only beneficial for organizational purposes but also plays a crucial role in error handling and applying common settings to a group of tasks.
Syntax:
- name: Example of Grouping Tasks in Ansible Block
block:
- name: Task 1
module: some_module
args:
key: value
- name: Task 2
another_module:
setting: value
rescue:
# Rescue tasks if any in the block fails
always:
# Always tasks, irrespective of the block's success or failure
Example: Imagine you have a series of tasks to configure a web server. Instead of listing them individually, you can group them into a block:
- name: Configure Web Server
block:
- name: Install Apache
yum:
name: httpd
state: present
- name: Start Apache Service
service:
name: httpd
state: started
- name: Deploy Webpage
copy:
src: /files/index.html
dest: /var/www/html/index.html
rescue:
- name: Error Handling
debug:
msg: "An error occurred while configuring the web server."
Application of Directives at the Block Level and Their Inheritance by Enclosed Tasks
Concept: In Ansible Block and Rescue, tasks within a block inherit any directives applied at the block level. This feature allows for setting common parameters or conditions for all tasks within the block, streamlining the playbook.
Example of Directive Inheritance: Consider applying a condition to a block of tasks using the when
directive:
- name: Conditional Execution of Tasks
block:
- name: Task A
debug:
msg: "Task A executed"
- name: Task B
debug:
msg: "Task B executed"
when: ansible_os_family == "RedHat"
rescue:
- debug:
msg: "An error occurred in RedHat family tasks."
In this example, the when
directive applies to each task within the block. This means tasks A and B will only execute if the condition (ansible_os_family == "RedHat"
) is true.
Error Handling with Blocks and Rescue
Error handling is a crucial aspect of automation, and Ansible Block and Rescue provide a robust framework for managing errors in playbooks. Understanding how blocks and the rescue keyword work together for error handling is essential for creating resilient Ansible configurations.
The Relationship Between Blocks and Rescue for Error Handling
Concept: In Ansible Block and Rescue, a block is used to group tasks, while the rescue section within the block is designed to handle any errors that occur in those tasks. This setup mimics the try-catch mechanism found in many programming languages, where the block acts as the 'try' part, and the rescue section as the 'catch' part.
How it Works:
- When a task within a block fails, execution is immediately transferred to the rescue section.
- The rescue section contains tasks that are designed to handle the failure, such as logging the error, performing cleanup actions, or executing alternative tasks.
- name: Ansible Block and Rescue for Error Handling
block:
- name: Attempt to perform a task
command: /path/to/some/command
- name: Another task
module: some_module
args:
key: value
rescue:
- name: Handle the error
debug:
msg: "An error occurred in the block."
In this example, if either of the tasks in the block fails, the control jumps to the rescue section, and the error handling task (debug
in this case) is executed.
Defining Tasks Under the Rescue Keyword for Error Recovery
Concept: The tasks under the rescue section are defined to manage the scenario where one or more tasks in the block fail. These tasks can vary based on the requirements, such as sending notifications, performing rollbacks, or collecting debug information.
Example: Consider a scenario where you're deploying an application, and you want to ensure proper error handling:
- name: Deploy Application with Ansible Block and Rescue
block:
- name: Clone Repository
git:
repo: 'https://example.com/repo.git'
dest: /var/www/app
- name: Install Dependencies
shell: pip install -r /var/www/app/requirements.txt
rescue:
- name: Send Failure Notification
mail:
to: Admin <admin@example.com>
subject: Deployment Failed
body: The deployment process failed.
In this playbook, if either cloning the repository or installing dependencies fails, the control is passed to the rescue section, where a failure notification is sent.
Using the Always Section in Ansible Block and Rescue
The always
section within an Ansible block plays a crucial role in ensuring certain tasks are executed regardless of the success or failure of previous tasks in the playbook. It's an integral part of the Ansible Block and Rescue mechanism, offering a guaranteed execution path for specified tasks.
Explanation of the Always Section in Blocks and Its Usage
Concept:
- The
always
section is a part of the block structure in Ansible, used to define tasks that should be executed after the block and rescue sections, irrespective of their outcomes. - This section is particularly useful for cleanup operations, logging, or resetting environment states, ensuring these actions are performed no matter what happens in the preceding block or rescue sections.
Syntax:
- name: Ansible Block with Always Section
block:
# Tasks that might fail
rescue:
# Tasks for error handling
always:
- name: Always executed task
module: some_module
How Tasks in the Always Section Run Irrespective of the Task Status of the Previous Block
Functionality:
- Tasks within the
always
section are executed after the block and rescue sections have finished, regardless of whether the tasks in these sections were successful or not. - This ensures that certain critical tasks, such as releasing resources or sending notifications, are always executed, providing a fail-safe mechanism in the playbook.
Example: Consider a scenario where you need to ensure that a notification is sent and resources are cleaned up after a series of tasks, whether they succeed or fail:
- name: Deployment with Always Section in Ansible
block:
- name: Start Deployment
shell: /path/to/deploy_script.sh
rescue:
- name: Handle Deployment Failure
debug:
msg: "Deployment failed. Initiating cleanup."
always:
- name: Send Deployment Status Notification
mail:
to: team@example.com
subject: Deployment Status
body: "Deployment process completed. Check logs for details."
- name: Cleanup Resources
shell: /path/to/cleanup_script.sh
In this playbook:
- If the deployment (
Start Deployment
) fails, the control moves to the rescue section (Handle Deployment Failure
). - Regardless of what happens in the block and rescue, the tasks in the
always
section (Send Deployment Status Notification
andCleanup Resources
) will be executed.
Advanced Concepts and Considerations
Understanding the advanced behavior of Ansible, especially how it handles failures within blocks and subsequent rescue successes, is crucial for mastering Ansible Block and Rescue. Let's dive into these concepts with detailed explanations and examples.
The Behavior of Ansible When Tasks in the Block Fail and the Rescue Task Succeeds
Concept:
- In an Ansible playbook, when a task in a block fails, the control is passed to the associated rescue section. If the tasks in the rescue section are executed successfully, Ansible treats the overall block (including the failed task) as successful.
- This behavior is critical for scenarios where a failure in the main task can be compensated or corrected in the rescue section, allowing the playbook to continue smoothly.
Example:
- name: Handling Failure in Ansible Block and Rescue
block:
- name: Primary Task
command: /path/to/unreliable_command.sh
register: result
ignore_errors: yes
rescue:
- name: Compensating Task for Failure
debug:
msg: "Primary task failed, but this task compensates for the failure."
when: result is failed
Explanation:
- In this example, if
/path/to/unreliable_command.sh
fails, the playbook does not halt. Instead, it moves to the rescue section. - The rescue task provides a compensatory action, and its success leads Ansible to mark the entire block as successful, allowing the playbook to proceed without marking it as a failure.
How Ansible Handles Failed Tasks and Reports in Playbook Statistics
Concept:
- Ansible's reporting mechanism distinguishes between tasks that fail within a block and those that are successfully handled in the rescue section.
- Even though the playbook continues after a successful rescue, Ansible still records the initial failure in the playbook statistics.
Example:
- name: Ansible Playbook with Block and Rescue
hosts: all
tasks:
- name: Example Block
block:
- name: Task that might fail
shell: might_fail_command
ignore_errors: yes
rescue:
- name: Recovery Task
debug:
msg: "Recovered from a failure."
Explanation:
- In this playbook, if
might_fail_command
fails, the recovery task in the rescue section is executed. - Ansible marks the
might_fail_command
as a failure in its statistics but continues executing the rest of the playbook since the rescue section succeeds.
Frequently Asked Questions on Ansible Block and Rescue
What happens if a task in the rescue
section also fails?
If a task in the rescue
section fails, Ansible stops executing any further tasks within that block and marks the block as failed. This behavior underscores the importance of ensuring that rescue
tasks are robust and less likely to fail.
Can rescue
and always
sections be used without a block
?
No, rescue
and always
sections are part of the block structure in Ansible. They must be used within a block
and cannot exist independently.
How does Ansible handle errors in included files or roles within a block?
Ansible treats included files or roles as part of the block. If an error occurs in an included task or role, the control transfers to the rescue
section of the encompassing block.
Can loops be used within a block?
Yes, you can use loops within tasks in a block. However, the loop itself cannot be directly applied to the block. Instead, each task within the block that requires looping should have its loop defined individually.
How does the always
section interact with handlers?
Tasks in the always
section are executed at the end of the block, regardless of the outcome. They do not directly interact with handlers. If you need to trigger a handler, it should be done within the block or rescue tasks using the notify
directive.
Is it possible to have nested blocks?
Yes, Ansible allows nesting of blocks. You can have a block within another block, which allows for more complex error handling scenarios. However, this should be done judiciously to avoid making the playbook overly complex and hard to read.
How can I ensure idempotency in tasks within a block?
To ensure idempotency, each task within the block should be written in an idempotent way. This means the task should not change the system state if run multiple times under the same conditions.
Can when
conditions be used within a block, rescue, or always sections?
Yes, when
conditions can be applied to individual tasks within a block, rescue, or always sections. They control the execution of each task based on the specified conditions.
How does Ansible handle skipped tasks within a block?
If a task within a block is skipped (due to a when
condition, for instance), Ansible simply moves to the next task. Skipped tasks do not trigger the rescue
section.
Practical Examples and Use Cases
Example-1: Perform 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 filevsftpd.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-2: 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:
- Ensure their group exists.
- Create the user.
- 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:
- Looping Through the Dataset: The main playbook starts by looping through the list of users. For each user, it includes
user_tasks.yml
. - 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. - Creating the User: The next task ensures the user exists.
- 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-3: 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 thevars
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
In summary, Ansible Block and Rescue is a vital feature in Ansible that enhances playbook structure and error handling. Blocks allow for logical grouping of tasks, making playbooks more organized and manageable. The rescue section within a block provides a way to handle errors that occur in tasks, similar to exception handling in programming. This is particularly useful for implementing custom error recovery logic, ensuring playbooks can handle unexpected situations gracefully. The always section, part of the block, guarantees the execution of specific tasks regardless of the success or failure of tasks in the block or rescue sections, ideal for cleanup or final status updates.
Advanced concepts in Ansible Block and Rescue include understanding how Ansible handles tasks when a block fails but the rescue succeeds, and the nuances in playbook statistics and reporting. Nested blocks, error handling in included files or roles, and ensuring idempotency are also essential considerations for advanced users.
For more detailed information and official documentation on Ansible Block and Rescue, the following resources are invaluable:
- Ansible Documentation: The official Ansible documentation provides comprehensive details on various aspects of Ansible, including blocks and error handling. Visit Ansible Documentation on Blocks.
- Ansible Best Practices: This section of the Ansible documentation offers insights into best practices for writing efficient and reliable playbooks, including the use of blocks and rescue. Ansible Best Practices.
- Ansible GitHub Repository: For examples and community contributions related to Ansible Block and Rescue, the Ansible GitHub repository is a great resource. Ansible GitHub Repository.
- Ansible Blog: The official Ansible blog often features articles and tutorials on various features, including block and rescue, providing practical insights and use cases. Ansible Blog.
What's Next
Next in our Ansible Tutorial we will learn about include and import module in Ansible with mutliple examples.