As a full-stack developer and Ansible specialist with over 5 years of experience, I utilize Ansible daily to automate Linux infrastructure and deploy cloud-native applications. Ansible‘s simple, yet flexible architecture has made it an essential tool in my toolbox. In particular, Ansible‘s support for logical blocks and conditional statements stands out by enabling clear and efficient playbook authoring.
In this comprehensive guide, I‘ll share expert techniques for leveraging blocks and when directives to optimize your Ansible workflows.
Grouping Tasks into Blocks
Blocks provide a way to bundle multiple related tasks together into a single logical unit. According to Ansihle‘s official documentation, this delivers several key advantages:
Logical Grouping of Tasks: Breaking a lengthy playbook into smaller blocks helps immensely with organization. Related tasks can be visually grouped together rather than having one hugely long playbook file.
Simplified Directives: Certain directives like rescue, always, when, become etc. can be specified once at the block level instead of repeating them for every single task. This removes clutter and redundancy.
Consistent Error Handling: Blocks make it very easy to implement standardized error handling across tasks using Ansible‘s rescue, always and ignore_errors features. This way failure policies get uniformly applied.
Here is a simple example demonstrating how we might group some common web server setup tasks together into a block:
- name: Configure web server
block:
- name: Install Apache
yum:
name: httpd
state: latest
- name: Open HTTP port
firewalld:
service: http
permanent: true
state: enabled
immediate: yes
rescue:
- name: Notify admin of failure
slack:
token: "<token>"
msg: "Web server configuration failed!"
This wraps the installation and firewall configuration into a clean block, along with some basic error handling via the rescue clause. If any task in the block fails, the handler will trigger a Slack notification.
Conditionally Executing Blocks
The when conditional statement allows controlling whether a task or block runs based on evaluating a logical test. For example, we may only want to execute a block when deploying to CentOS servers:
- name: Configure web server on CentOS
block:
- name: Install httpd
yum:
name: httpd
state: latest
- firewalld:
service: http
permanent: true
state: enabled
immediate: yes
when: ansible_facts[‘os_family‘] == "RedHat"
Here the entire web server block will be skipped on any non-RedHat hosts. When combined with blocks, Ansible‘s when conditionals become quite versatile for customizing playbook behavior.
Comparison to Ansible Includes
Blocks are related to, but serve a different role than Ansible includes. Both help eliminate unnecessary task duplication:
-
Includes extract reusable tasks into separate files that can be referenced in multiple playbooks. Great for sharing common tasks.
-
Blocks group related tasks to simplify directives and error handling. Help organize lengthy playbooks and handle failures consistently.
For example, an include statement might load a common package installation task:
- name: Install base packages
include_tasks: packages.yml
While a block structures tasks already defined within the playbook itself:
- name: Configure web services
block:
- yum:
name: "{{ common_packages }}"
state: latest
- template:
src: templates/config.j2
dest: /etc/foo.conf
So in summary:
- Includes = Reuse tasks across multiple playbooks
- Blocks = Logically group tasks within a single playbook
Both are extremely useful for eliminating duplication and improving playbook organization.
Streamlining Playbooks with Blocks
For longer, more complex playbooks, it becomes tedious to repeat the same when conditionals and directives across many different tasks.
Blocks allow us to avoid this repetition by applying directives only once to a group of tasks.
For example, consider if we want to execute a series of steps only when deploying to CentOS 7:
- block:
- name: Step 1
yum:
name: nfs-utils
state: latest
- name: Step 2
template:
src: templates/config.j2
dest: /etc/foo.conf
- name: Step 3
service:
name: bar
state: restarted
enabled: yes
when:
- ansible_facts[‘distribution‘] == "CentOS"
- ansible_facts[‘distribution_major_version‘] == "7"
Here we avoid having to specify the OS conditionals repeatedly per task by encapsulating everything in a conditional block. Much easier to maintain as we add/remove tasks later on!
Abstracting Common Tasks into Reusable Blocks
It‘s also possible to abstract common playbook patterns into reusable paramterized blocks that function similarly to functions or includes in programming.
For example, we could define a generic block for installing and configuring Apache:
- name: Apache install block
block:
- name: Install httpd
yum:
name: httpd
state: latest
- name: Open firewall ports
firewalld:
service: http
permanent: true
state: enabled
immediate: yes
rescue:
- debug:
msg: "Failed to configure Apache"
- name: Configure Apache on web servers
include_tasks: apache.yml
vars:
pkg_name: httpd
- name: Configure Apache on CI servers
include_tasks: apache.yml
vars:
pkg_name: apache2
Now the apache block can be reused anywhere needed, avoiding code duplication. We parameterize using vars to customize package names if necessary. This demonstrates an expert Ansible technique for playbook optimization.
Dynamic Blocks
Blocks can also be generated dynamically using Jinja2 templating. This allows creating multiple blocks programmatically based on underlying data:
- name: Configure applications
block:
{% for app in apps %}
- name: Configure {{ app }}
template:
src: "{{ app }}.conf.j2"
dest: /etc/{{ app }}.conf
{% endfor %}
Here we can iterate the apps variable to deploy arbitrarily many application configurations in a standardized way. Dynamic blocks like this really showcase Ansible‘s capabilities for data-driven automation.
Delegating Blocks to Particular Hosts
The delegate_to directive allows executing a block on a remote host rather than the Ansible controller. This enables certain tasks to run where it makes most sense:
- hosts: webservers
tasks:
- name: Install monitoring agent
block:
- get_url:
url: http://monitor.acme.com/agent.pkg
dest: /tmp/agent.pkg
- command: /opt/acme/install-agent /tmp/agent.pkg
delegate_to: "{{ inventory_hostname }}"
Here we download and install the agent on the target web servers themselves, avoiding extra network hops. Delegating blocks in this way is an Ansible best practice that utilizes server resources efficiently.
Putting It All Together: Multi-Tier Web Architecture
To tie together everything we‘ve covered, let‘s design an Ansible playbook to deploy a configurable, scalable web architecture across front-end proxies, application servers and database hosts.
We can architect modular, reusable blocks to provision infrastructure in a robust and flexible fashion:
- name: Common Base Setup
hosts: all
tasks:
- block:
- debug:
msg: "Installing base packages on {{ inventory_hostname }}"
- name: Install common packages
yum:
name:
- wget
- git
- python3
state: latest
- name: Configure Proxy Servers
hosts: proxyservers
tasks:
- import_tasks: common.yml
- block:
- name: Install Nginx
yum:
name: nginx
state: latest
# Config steps for Nginx
rescue:
- notify: alert_proxy_failure
- name: Configure App Servers
hosts: appservers
tasks:
- import_tasks: common.yml
- block:
# App server config steps
rescue:
- include_tasks: handles/app_failure.yml
when: ansible_facts[‘distribution‘] == "CentOS"
- name: Configure DB Servers
hosts: databases
tasks:
- include_tasks: common.yml
- name: Provision PostgreSQL
block:
# DB setup steps
delegate_to: "{{ inventory_hostname }}"
This playbook demonstrates several best practices:
- Extracted common base setup into an include
- Error handling standardized via rescue blocks
- Conditional execution when deploying apps
- Database block delegated to remote hosts
Composing modular blocks in this way results in maintainable, scalable Ansible automation code.
Final Thoughts
Ansible blocks and when conditionals unlock a whole range of efficiency improvements for playbook developers. Strategically bundling tasks into blocks and implementing conditional logic facilitates everything from better organization to simplified error handling and delegated server usage.
I hope you‘ve found these tips and code samples useful for enhancing your own Ansible automation skills! As always, feel free to provide any other block/when use cases in the comments.


