The orchestration of complex infrastructure requires a mechanism to execute repetitive tasks across varying datasets without the redundancy of writing individual tasks for every unique entity. In the Ansible ecosystem, this necessity is met by the concept of looping. At the heart of this capability lies with_items, a specialized keyword powered by a lookup plugin designed to iterate over lists. While modern Ansible development has shifted toward the loop keyword, with_items remains a functional and widely utilized component of the automation toolkit. Understanding its behavior—from simple list processing to the flattening of nested lists and the handling of complex dictionaries—is essential for any engineer aiming to achieve operational efficiency in configuration management.
The Architecture of the with_items Plugin
To understand with_items, one must first understand its identity as a lookup plugin. In Ansible, lookup plugins are specific components that allow the system to access data from external resources. The with_items keyword specifically utilizes the items lookup plugin.
The technical operation of this plugin involves the conversion of data into a standard templating format, specifically using the Jinja2 system. This allows Ansible to load variables or templates and integrate data from diverse systems into a usable format for the playbook. A critical architectural detail is that all variable realization and item parsing occur on the local Ansible controller node. This means the computational overhead of processing the list and resolving the variables happens on the machine where the playbook is executed, rather than on the remote target hosts.
The primary utility of with_items is its ability to implement a "for loop" logic within a YAML-based playbook. When a list of items is passed to a task, the Ansible engine treats that task as a template and executes it once for every individual element found within the provided list.
Deep Dive into with_items Functionality and Behaviors
The versatility of with_items is demonstrated through its ability to handle different data structures, ranging from primitive strings to complex objects.
Simple List Iteration
The most common application of with_items is the processing of a simple list of strings or integers. In this scenario, the plugin iterates through the list and assigns the current value to the item variable.
For example, in a task designed to debug fruit names, the syntax would be:
yaml
- name: ASimple List
debug:
msg: "My Fruit is {{ item }}"
with_items:
- apple
- banana
- orange
- pineapple
- grapes
In this instance, the task will run five times. The first execution replaces {{ item }} with "apple", the second with "banana", and so on. This removes the need to write five separate debug tasks.
Flattening of Nested Lists
A distinguishing technical characteristic of the with_items plugin is its approach to nested lists. If a high-level item contains another list, with_items will flatten that list.
Flattening means that if you provide a list that contains another list as an element, Ansible will treat the elements of the inner list as individual items for the loop. It does not perform recursion in the traditional programming sense; rather, it expands the inner list into the main sequence of items to be processed.
Consider the following structure:
yaml
- name: A List with An Item Having Another List
debug: var=item
with_items:
- apple
- [eagle, falcon]
- lion
In this case, the loop will not treat [eagle, falcon] as a single unit. Instead, it will produce four iterations: "apple", "eagle", "falcon", and "lion". If a user requires actual recursion or the preservation of the list structure, the list lookup plugin must be used instead, as with_items is explicitly designed to flatten.
Complex Data Structures and Multi-Parameter Tasks
In real-world production environments, tasks often require more than one parameter. For instance, creating a system user requires a username, a User ID (UID), and a group assignment. with_items supports the passing of dictionaries (key-value pairs) to handle these requirements.
When a list of dictionaries is passed, the item variable becomes an object. You can then access specific values using dot notation (e.g., item.name).
The following example demonstrates how to create multiple users with specific attributes:
yaml
- name: A List where multiple variables are passed in an item of list
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups }}"
state: present
with_items:
- { name: sample1, uid: 2001, groups: "HR" }
- { name: sample2, uid: 2002, groups: "Finance" }
This flexibility allows a single task to manage diverse configurations for different users, ensuring that each account is provisioned with the correct permissions and identity markers.
Comparative Analysis: with_items vs. loop
While with_items is functional and not yet deprecated, official Ansible documentation recommends the use of the loop keyword for introducing looping constructs. The transition from with_items to loop represents a shift toward more flexible and powerful iteration.
Key Differences and Capabilities
The following table provides a technical comparison between the two methods:
| Feature | with_items | loop |
|---|---|---|
| Primary Mechanism | Lookup Plugin (items) | Core Loop Engine |
| List Handling | Flattens nested lists | Maintains structure (usually) |
| Flexibility | Limited to simple lists/dicts | High; works with filters/plugins |
| Recommended Use | Legacy playbooks/Simple lists | Modern playbooks/Complex data |
| Integration | Basic Jinja2 templating | Advanced filters (e.g., zip, dict2list) |
Advanced Iteration with loop
The loop keyword provides capabilities that with_items cannot easily replicate, such as the use of the zip filter. The zip filter allows two separate lists to be combined into a single list of tuples, which is invaluable for assigning specific values to specific entities.
Example of using loop to assign specific UIDs:
yaml
- name: Create multiple users with specific UIDs
user:
name: "{{ item.0 }}"
uid: "{{ item.1 }}"
state: present
shell: /bin/bash
loop: "{{ ['mjordan', 'mmathers', 'pparker'] | zip([1001, 1002, 1003]) | list }}"
In this advanced configuration, item.0 represents the username (from the first list) and item.1 represents the UID (from the second list). This provides a level of precision and programmatic control that exceeds the capabilities of a standard with_items list.
Practical Application Scenarios
To illustrate the impact of these tools in a real-world environment, consider a lab setup consisting of an Ansible control server named ansible-controller and two target machines named host-one and host-two.
File Management and Ownership
In a scenario where an administrator needs to create multiple configuration files with specific ownership and permissions, with_items can be used to define a list of files and their corresponding attributes.
yaml
- name: Create multiple files and set ownership
file:
path: "/etc/{{ item.path }}"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
state: touch
with_items:
- { path: 'config1.conf', owner: 'root', group: 'root', mode: '0644' }
- { path: 'config2.conf', owner: 'admin', group: 'admin', mode: '0660' }
The execution of this playbook ensures that each file is created with the exact specifications required for security compliance. Upon completion, a verification check would confirm that the files exist with the correct ownership and permissions.
Debugging and List Verification
For engineers testing new playbooks, the debug module combined with with_items is the primary method for verifying how Ansible perceives a list before applying changes to a production server.
Example for verifying dictionary items:
yaml
- name: Here we are providing a list which have items containing multiple values
debug:
msg: "current first value is {{ item.first }} and second value is {{ item.second }}"
with_items:
- { first: lemon, second: carrot }
- { first: cow, second: goat }
This allows the developer to ensure that the item.first and item.second keys are being parsed correctly by the controller node before passing those variables to a destructive module like file or user.
Advanced Looping Concepts
Beyond the basic with_items usage, Ansible supports more complex looping patterns to handle sophisticated infrastructure requirements.
Nested Loops
Nested loops occur when one loop is placed inside another, allowing for the processing of multi-dimensional data. This is achieved by nesting loop constructs within each other, enabling the automation of tasks that require combinations of multiple variable sets.
Loop Control and Conditionals
Ansible provides mechanisms to control how loops behave. This includes the ability to perform conditional checks within a loop, ensuring that a task is only executed if a certain condition is met for that specific item. This prevents the playbook from attempting to apply configurations to items that do not require updates.
Looping Blocks
While standard tasks are easily looped, looping a block of tasks requires specific strategies to ensure that the group of tasks is executed as a unit for every item in the list.
Conclusion
The with_items keyword, powered by the items lookup plugin, serves as a foundational element for iterative automation in Ansible. By shifting the burden of variable parsing to the controller node and utilizing Jinja2 templating, it enables administrators to replace repetitive manual entries with dynamic lists. The ability to flatten nested lists and process complex dictionaries makes it a versatile tool for managing users, files, and system configurations.
However, the evolution of Ansible has introduced the loop keyword as the superior alternative. While with_items remains functional, loop offers deeper integration with Ansible filters and provides the flexibility needed for complex data manipulation, such as zipping lists together. For a modern DevOps professional, the choice between with_items and loop depends on the complexity of the data: simple lists can be handled by either, but complex, multi-parameter configurations benefit significantly from the advanced features of the loop construct. Mastery of both allows for the creation of highly efficient, scalable, and maintainable playbooks.