Table of Contents
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:
- Grouping Multiple Tasks: Blocks help in organizing your playbook by grouping related tasks. This enhances readability and makes the playbook easier to maintain.
- Error Handling: With the
rescue
andalways
sections, blocks facilitate structured error handling. If any task within a block fails, the tasks within therescue
section are executed. Thealways
section ensures certain tasks are run regardless of success or failure in the main block or rescue section. - 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.
- Applying Directives: Directives like
tags
,become
, orwhen
can be applied to an entire block, making it more efficient than applying them to individual tasks. - 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.
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:
- 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, therescue
section can be used to roll back to the previous state. - 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. - 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:
- 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. - 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. - 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 ~/"
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 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-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:
- 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-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 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
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.