As I mentioned before, iterations in Ansible are not first-class citizens. The best you can have - loop for a single task, with one nested loop maximum. Anything more sophisticated than that should be implemented as a separate module with proper procedural language. Yet once again, the include_tasks command gives us a helping hand.

Let's take a pretty typical, old-school example: you have a set of application servers, and each one may have one or more binaries installed. From time to time, you apply one or more patches to each installation. The description screams "iterations": one over the installation locations and the other over the list of patches to apply. The sample code below resembles the one I use to patch Oracle Fusion Middleware farms.

# Main task list - ofmw-patch.yml
- name: Apply Patches to OFMW Home
    name: apply_patches.yml
    target_home: "{{ mw_home }}"
    what_to_apply: "{{ mw_patch_list }}"
  loop: "{{ homes_on_host }}"
    loop_var: mw_home
# Inner loop tasks - apply_patches.yml
- name: Apply Patch from patch list
    name: opatch.yml
    current_patch: "{{ l_patch }}"
  loop: "{{ what_to_apply }}"
    loop_var: l_patch
Nested loops in Ansible

As you may see, I use more than one include_tasks to loop over:

  • Main task flow - installation preparation: Stop the processes, prepare folders, set up some facts.
  • Outer loop -  prepare patch binaries on targets: download archives, unpack the code, identify the patch type, etc.
  • The inner loop - apply a single patch to the current installation.  

A few takeaways :

  • You may have more than one task list to iterate. It means instead of one playbook file, you would carry at least two or even three, as in the example above. It makes playbooks mouthful and too techy. Dictionaries to iterate over will add up more to it. It may be worth hiding all the complexity inside the role.
  • Use loop_control clause to redefine default loop variable name and avoid name overlaps for nested loops.
  • Use unique parameter names for each include_tasks when possible, same as we discussed in the previous post. It eliminates the hassle with variable redefinitions, previous values, and residual values.