6 minutes
Ansible - Control Flow Loops and Conditionals - Part 4
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.
- 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 theyum/aptmodule.
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.
- 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.
- 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
- 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.