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.

Similar Posts