Mastering Dictionary Iteration in Ansible: From with_dict to Modern Loop Architectures

The capacity to automate repetitive tasks is the cornerstone of infrastructure as code, and within the Ansible ecosystem, the ability to iterate over complex data structures—specifically dictionaries—is fundamental for scaling configurations. In the early stages of Ansible's evolution, the with_dict keyword served as the primary mechanism for traversing associative arrays (known as dictionaries or maps). This construct allowed administrators to map a set of keys to their corresponding values, enabling the dynamic creation of users, configuration of virtual hosts, and management of system services without writing redundant tasks.

However, as Ansible evolved toward a more unified and consistent syntax, the community and the developers shifted toward the loop keyword. While with_dict remains functional in many legacy environments, the modern standard since Ansible 2.5 has transitioned to the combination of loop and the dict2items filter. This shift is not merely cosmetic; it represents a fundamental change in how data is piped into a task. By transforming a dictionary into a list of key-value pairs via dict2items, Ansible can utilize a single, consistent looping mechanism across all data types, whether they are simple lists, complex dictionaries, or dynamically generated objects.

The transition from with_dict to loop resolves several critical limitations. Specifically, the old with_dict approach made it exceedingly difficult to filter or transform data before the iteration began. In a modern loop construct, the data undergoes a transformation process (such as filtering with selectattr) before the task ever executes, reducing the reliance on the when conditional and improving the overall efficiency and readability of the playbook.

The Mechanics of with_dict and Traditional Dictionary Looping

The with_dict keyword is specifically designed to iterate over Python dictionaries. In a dictionary, data is stored as a collection of keys, where each key is associated with a value. This is analogous to an associative array in other programming languages. For example, in PHP, a similar operation would be executed using a foreach loop where the key and value are both extracted:

<?php foreach ($users_with_dict as $user => $properties) { // Create a user named $user... } ?>

When using with_dict in an Ansible task, the loop provides two primary variables for each iteration: item.key and item.value. The item.key represents the unique identifier (the dictionary key), and item.value represents the data associated with that key, which could be a simple string or another nested dictionary.

Implementation Example: User Creation via with_dict

To illustrate the practical application of with_dict, consider a scenario where multiple user accounts must be created with specific UIDs and shells.

yaml - name: Create user accounts hosts: all become: true vars: users: alice: uid: 1001 shell: /bin/bash bob: uid: 1002 shell: /bin/zsh charlie: uid: 1003 shell: /bin/bash tasks: - name: Create users ansible.builtin.user: name: "{{ item.key }}" uid: "{{ item.value.uid }}" shell: "{{ item.value.shell }}" with_dict: "{{ users }}"

In this configuration, the with_dict keyword tells Ansible to look at the users variable. In the first iteration, item.key is alice and item.value is a dictionary containing uid: 1001 and shell: /bin/bash. The ansible.builtin.user module then uses these values to configure the system.

Transitioning to Modern Loops: The loop and dict2items Paradigm

Starting with Ansible 2.5, the recommended method for iterating over dictionaries is the loop keyword combined with the dict2items filter. The dict2items filter transforms a dictionary into a list of dictionaries, where each element in the list has a key and a value attribute. This ensures that the behavior remains identical to with_dict while adhering to the modern, consistent looping syntax.

Comparative Analysis of Syntax Evolution

The following table outlines the structural differences between the legacy and modern approaches.

Feature Legacy Approach (with_dict) Modern Approach (loop + dict2items)
Primary Keyword with_dict loop
Data Transformation Implicit (Internal to Ansible) Explicit (via dict2items filter)
Variable Access item.key and item.value item.key and item.value
Filterability Limited (requires when conditions) High (supports Jinja2 filters like selectattr)
Consistency Specialized loop type Unified loop system

The Migration Process in Practice

The migration from with_dict to loop is straightforward because the variable references (item.key and item.value) remain unchanged. The only modification is the replacement of the keyword and the addition of the filter.

Updated User Creation Example

yaml - name: Create user accounts hosts: all become: true vars: users: alice: uid: 1001 shell: /bin/bash bob: uid: 1002 shell: /bin/zsh charlie: uid: 1003 shell: /bin/bash tasks: - name: Create users ansible.builtin.user: name: "{{ item.key }}" uid: "{{ item.value.uid }}" shell: "{{ item.value.shell }}" loop: "{{ users | dict2items }}"

Advanced Data Manipulation and Filtering

One of the most significant advantages of moving to loop is the ability to manipulate the data stream before it reaches the task. With with_dict, filtering usually required a when statement, which means the task would still "run" for every item, even if it was skipped. With loop and Jinja2 filters, you can remove unwanted items from the list entirely before the loop begins.

Filtering with selectattr

Consider a scenario where a dictionary of databases is managed, but only those with replication enabled should be processed.

yaml - name: Process only databases with replication enabled ansible.builtin.debug: msg: "{{ item.key }} has replication" loop: >- {{ databases | dict2items | selectattr('value.replication', 'equalto', true) | list }} vars: databases: primary_db: owner: dbadmin replication: true analytics_db: owner: analyst replication: false reporting_db: owner: reporter replication: true

In this example, the dict2items filter first converts the dictionary into a list. Then, selectattr is used to filter the list, keeping only those items where the value.replication attribute is true. This results in a cleaner execution log and more efficient processing.

Real-World Application: Managing Virtual Hosts

In a production environment, dictionaries are often stored in group_vars to maintain a separation between logic (playbooks) and data (variables). Managing Nginx or Apache virtual hosts is a prime use case for this architecture.

Variable Definition in group_vars/webservers.yml

yaml virtual_hosts: api.example.com: port: 8080 ssl: true root: /var/www/api app.example.com: port: 3000 ssl: true root: /var/www/app static.example.com: port: 80 ssl: false root: /var/www/static

Implementation using loop and loop_control

To keep the output clean, the loop_control keyword can be used with the label attribute. Without a label, Ansible prints the entire dictionary for every iteration, which clutters the console. Using label: "{{ item.key }}" ensures that only the domain name is displayed.

yaml - name: Generate vhost configs ansible.builtin.template: src: vhost.conf.j2 dest: "/etc/nginx/sites-available/{{ item.key }}.conf" loop: "{{ virtual_hosts | dict2items }}" loop_control: label: "{{ item.key }}"

Handling Registered Variables during Dictionary Loops

When a loop is registered using the register keyword, Ansible stores the results of every iteration in a list called results. This structure is consistent whether you use with_dict or loop with dict2items.

Registration and Result Processing Example

The following example demonstrates checking database connectivity and then looping through the results to identify failed connections.

```yaml
- name: Check database connectivity
ansible.builtin.command: "pgisready -h {{ item.value.host }} -p {{ item.value.port }}"
loop: "{{ databases | dict2items }}"
register: db
checks
changed_when: false

  • name: Show failed connections
    ansible.builtin.debug:
    msg: "Cannot reach {{ item.item.key }}"
    loop: "{{ db_checks.results }}"
    when: item.rc != 0
    ```

In the second task, item.item.key is used. The first item refers to the result object in the db_checks.results list, and the second .item refers to the original item that was being looped over in the first task.

Limitations and Alternatives for Nested Data

A critical limitation of with_dict (and by extension, the simple loop with dict2items) is that it cannot iterate over sub-elements of a dictionary. If a dictionary contains lists as values, and those lists must be iterated over individually, a different approach is required.

The Challenge of Sub-element Iteration

If you have a dictionary of users, and each user has a list of common directories that need to be created, with_dict cannot be used to create each directory. This is because with_dict iterates at the top level of the dictionary.

Solving Nesting with with_nested

When you need to iterate over two different lists—such as a list of users and a list of directories—with_nested is the appropriate tool. with_nested takes multiple lists and iterates over them in a Cartesian product.

```yaml
vars:
userswithitems:
- name: "alice"
personaldirectories: ["bob", "carol", "dan"]
- name: "bob"
personal
directories: ["alice", "carol", "dan"]
common_directories:
- ".ssh"
- "loops"

tasks:
- name: "Loop 2: create common users' directories using 'withnested'."
file:
dest: "/home/{{ item.0.name }}/{{ item.1 }}"
owner: "{{ item.0.name }}"
group: "{{ item.0.name }}"
state: directory
with
nested:
- "{{ userswithitems }}"
- "{{ common_directories }}"
```

In this construct:
- item.0 refers to the current element of the first list (users_with_items).
- item.1 refers to the current element of the second list (common_directories).

This allows for the creation of paths like /home/alice/.ssh and /home/alice/loops in a single task.

Solving Nesting with with_subelements

For cases where the list to be iterated over is actually a property within the dictionary item itself, with_subelements is used. This allows for a high level of abstraction and readability, which is essential when dealing with complex infrastructure deployments.

Migration Checklist for Infrastructure Engineers

To ensure a seamless transition from legacy with_dict implementations to modern loop structures, the following step-by-step process should be followed:

  • Find the with_dict line in the playbook.
  • Identify if the dictionary is defined as an inline variable or passed as a variable reference.
  • If the dictionary is inline, migrate it to a vars block to improve maintainability.
  • Replace the with_dict: "{{ dict_var }}" syntax with loop: "{{ dict_var | dict2items }}".
  • Verify that the item.key and item.value references are still correctly mapped to the intended attributes.
  • Implement loop_control: label if the dictionary values are large, to prevent verbose output in the console.
  • Check if any filtering is required; if so, replace when conditions with Jinja2 filters like selectattr within the loop statement.

Conclusion: The Strategic Shift toward Declarative Iteration

The evolution from with_dict to the loop and dict2items pattern is more than a change in syntax; it is a shift toward a more powerful, declarative method of handling data in Ansible. By decoupling the data structure from the iteration mechanism, Ansible allows engineers to perform complex transformations—such as filtering and sorting—before the task is even executed.

While with_dict provided a quick way to handle associative arrays, its rigidity limited the ability to manipulate data on the fly. The modern approach provides the flexibility of Python-like list comprehensions within the YAML structure. This allows for a cleaner separation of concerns: variables define the state, filters define the subset of that state to be acted upon, and the loop keyword executes the action.

Moreover, the integration of loop_control and the ability to easily register results into a results list ensures that large-scale deployments remain observable and manageable. Whether managing a handful of virtual hosts or thousands of user accounts across a global fleet, the transition to loop and dict2items provides the scalability and precision required for modern DevOps operations.

Sources

  1. OneUptime - How to Migrate from with_dict to loop in Ansible
  2. ChromaticHQ - Untangling Ansible Loops
  3. DoHost - Looping with Ansible (loop, withitems, withdict)

Related Posts