In previous parts, we’ve covered the basics of playbooks, variables, and templates. Now, we’ll dive into two crucial control flow mechanisms that allow you to create more dynamic and flexible playbooks: loops and conditionals.

Loops: Executing Tasks Multiple Times

Loops are used to repeat a task for each item in a list. This is incredibly useful for tasks like creating multiple users, installing several packages, or managing a list of services.

  1. loop Keyword (Recommended for newer Ansible versions)

The loop keyword is the modern and preferred way to create loops in Ansible. It iterates over a list of items.

Example 1: Installing multiple packages

Let’s assume you want to install git, htop, and vim on your web servers.

---
# playbooks/install_tools.yml
- name: Install common development tools
  hosts: webservers
  become: true
  tasks:
    - name: Install specified packages
      ansible.builtin.yum: # or apt for Debian/Ubuntu
        name: "{{ item }}"
        state: present
      loop:
        - git
        - htop
        - vim
        - tree # Example of another package

Explanation:

  • loop:: Defines the list of items to iterate over.
  • {{ item }}: Inside the loop, the current item in the list is exposed via the magic variable item. This allows you to reference the current package name in the name parameter of the yum/apt module.

When this playbook runs, Ansible will execute the yum (or apt) task four times, once for each package in the loop list.

Example 2: Creating multiple users with specific UIDs

You can iterate over a list of dictionaries to pass multiple parameters for each item.

---
# playbooks/manage_users.yml
- name: Create and manage application users
  hosts: webservers
  become: true
  tasks:
    - name: Create application users with specified UIDs
      ansible.builtin.user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        state: present
        shell: /bin/bash
      loop:
        - { name: 'appuser1', uid: 1001 }
        - { name: 'appuser2', uid: 1002 }
        - { name: 'adminuser', uid: 1003 }

Explanation:

  • loop:: Contains a list of dictionaries, where each dictionary represents a user with name and uid keys.
  • {{ item.name }} and {{ item.uid }}: Access specific keys from the current dictionary using dot notation.
  1. Looping over variables

You can define the list in your vars or group_vars and loop over it.

group_vars/webservers.yml:

# group_vars/webservers.yml
nginx_vhosts:
  - name: website1
    port: 80
    root: /var/www/html/website1
  - name: blog
    port: 8080
    root: /var/www/html/blog
    ssl_enabled: true

templates/nginx_vhost.conf.j2:

# templates/nginx_vhost.conf.j2
server {
    listen {{ item.port }};
    server_name {{ item.name }}.example.com;
    root {{ item.root }};
    index index.html;

    {% if item.ssl_enabled is defined and item.ssl_enabled %}
    listen 443 ssl;
    ssl_certificate /etc/nginx/ssl/{{ item.name }}.crt;
    ssl_certificate_key /etc/nginx/ssl/{{ item.name }}.key;
    {% endif %}

    location / {
        try_files $uri $uri/ =404;
    }
}

playbooks/deploy_vhosts.yml:

---
# playbooks/deploy_vhosts.yml
- name: Deploy Nginx virtual hosts
  hosts: webservers
  become: true
  tasks:
    - name: Create Nginx virtual host configuration
      ansible.builtin.template:
        src: ../templates/nginx_vhost.conf.j2
        dest: "/etc/nginx/conf.d/{{ item.name }}.conf"
        owner: root
        group: root
        mode: '0644'
      loop: "{{ nginx_vhosts }}" # Loop over the variable defined in group_vars
      notify: Reload Nginx

  handlers:
    - name: Reload Nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

This setup will create two separate Nginx configuration files (website1.conf and blog.conf) based on the data in nginx_vhosts variable.

Conditionals: Running Tasks Based on Conditions

Conditionals allow you to execute tasks only if a specific condition is met. This is powerful for handling different operating systems, environments, or specific server roles.

The when Keyword

The when keyword is used to add a conditional statement to a task. The condition is evaluated using Jinja2 expressions.

Example 1: OS-specific package installation (revisiting a previous example)

---
# playbooks/os_specific_install.yml
- name: Install web server based on OS family
  hosts: all
  become: true
  tasks:
    - name: Install Apache on Debian-based systems
      ansible.builtin.apt:
        name: apache2
        state: present
      when: ansible_os_family == "Debian"

    - name: Install Nginx on RedHat-based systems
      ansible.builtin.yum:
        name: nginx
        state: present
      when: ansible_os_family == "RedHat"

Explanation:

  • ansible_os_family: This is an Ansible fact that automatically tells you the broad operating system family (e.g., "Debian", "RedHat"). Ansible gathers these facts at the beginning of each play by default.
  • ==: The equality operator. You can use other comparison operators like != (not equal), > (greater than), < (less than), >= (greater than or equal), <= (less than or equal).

Using Boolean Variables

You can use boolean variables (true/false) to control task execution.

Example 2: Enable/Disable a feature

---
# playbooks/feature_toggle.yml
- name: Manage a specific feature
  hosts: webservers
  become: true
  vars:
    enable_feature_x: true # Set this to false to disable the feature

  tasks:
    - name: Ensure Feature X service is running
      ansible.builtin.service:
        name: featurex
        state: started
        enabled: true
      when: enable_feature_x

    - name: Stop Feature X service (if disabled)
      ansible.builtin.service:
        name: featurex
        state: stopped
        enabled: false
      when: not enable_feature_x

Combining Conditions (AND, OR)

You can combine multiple conditions using and and or operators.

Example 3: Install a package only on specific OS and hostname

---
# playbooks/complex_condition.yml
- name: Install specific tool on certain hosts
  hosts: all
  become: true
  tasks:
    - name: Install specific monitoring agent only on web1 if it's Debian
      ansible.builtin.apt:
        name: monitoring-agent
        state: present
      when: ansible_os_family == "Debian" and inventory_hostname == "web1.example.com"

    - name: Install debug tools on any server that is NOT RedHat
      ansible.builtin.yum:
        name: debug-tools
        state: present
      when: ansible_os_family != "RedHat"

Explanation:

  • inventory_hostname: Another Ansible magic variable that contains the current ```hostname```` as defined in the inventory.

Practical Example: Deploying a Simple Website with Loops and Conditionals

Let’s combine these concepts to create a playbook that:

  • Installs a web server (Apache or Nginx) based on OS.
  • Creates multiple virtual host directories.
  • Deploys a basic index.html to each virtual host.
  1. Update group_vars/webservers.yml:
# group_vars/webservers.yml
web_sites:
  - name: mycorp_main
    document_root: /var/www/html/mycorp_main
    port: 80
  - name: internal_wiki
    document_root: /var/www/html/internal_wiki
    port: 8080
    content: "<h1>Welcome to the Internal Wiki!</h1><p>Under construction.</p>" # Inline content
  1. Create playbooks/deploy_multi_website.yml:
---
# playbooks/deploy_multi_website.yml
- name: Deploy multiple websites with OS-specific web server
  hosts: webservers
  become: true

  tasks:
    - name: Install Apache on Debian-based systems
      ansible.builtin.apt:
        name: apache2
        state: present
      when: ansible_os_family == "Debian"

    - name: Install Nginx on RedHat-based systems
      ansible.builtin.yum:
        name: nginx
        state: present
      when: ansible_os_family == "RedHat"

    - name: Ensure web server service is started and enabled (Apache)
      ansible.builtin.service:
        name: apache2
        state: started
        enabled: true
      when: ansible_os_family == "Debian"

    - name: Ensure web server service is started and enabled (Nginx)
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true
      when: ansible_os_family == "RedHat"

    - name: Create document root directories for each website
      ansible.builtin.file:
        path: "{{ item.document_root }}"
        state: directory
        owner: www-data # Or nginx for RedHat-based
        group: www-data # Or nginx for RedHat-based
        mode: '0755'
      loop: "{{ web_sites }}"

    - name: Deploy index.html for each website
      ansible.builtin.copy:
        content: |
          <!DOCTYPE html>
          <html>
          <head>
              <title>{{ item.name | capitalize }}</title>
          </head>
          <body>
              {% if item.content is defined %}
              {{ item.content }}
              {% else %}
              <h1>Hello from {{ item.name | capitalize }}!</h1>
              <p>This is a simple page deployed by Ansible.</p>
              {% endif %}
          </body>
          </html>          
        dest: "{{ item.document_root }}/index.html"
        owner: www-data # Or nginx for RedHat-based
        group: www-data # Or nginx for RedHat-based
        mode: '0644'
      loop: "{{ web_sites }}"

    # Add tasks here to configure Apache/Nginx virtual hosts
    # For Apache, you'd use a template for .conf files, loop over web_sites, and notify apache restart
    # For Nginx, you'd use a template for .conf files, loop over web_sites, and notify nginx reload

Running the playbook:


ansible-playbook -i inventory.ini playbooks/deploy_multi_website.yml

This playbook demonstrates how loops and conditionals allow you to automate complex scenarios, adapting to different environments and configurations.

Next Steps

In Part 5, we’ll explore Roles, which are a powerful way to organize your Ansible content into a reusable and shareable structure, making your projects more maintainable and scalable.