Mastering Dictionary Iteration in Ansible with the dict2items Filter

In the landscape of infrastructure automation, the ability to manage complex data structures is what separates basic playbooks from scalable, professional-grade orchestration. One of the most frequent hurdles encountered by Ansible engineers is the inherent incompatibility between the loop keyword and the dictionary (hash) data type. By default, the loop mechanism in Ansible expects a list—a sequential collection of items. When a user attempts to pass a dictionary directly into a loop, Ansible will throw an error because it cannot iterate over a mapping without a defined sequence. This is where the dict2items filter becomes an essential architectural tool.

The dict2items filter acts as a transformation layer, converting a dictionary into a list of dictionaries. Each resulting dictionary in the list contains two specific attributes: key and value. This transformation allows the Ansible engine to traverse the data linearly, providing a predictable way to access both the identifier (the key) and the associated data (the value) during the execution of a task. This capability is fundamental for managing system configurations, environment variables, and API-driven deployments where data is naturally structured as key-value pairs but must be applied as a series of discrete actions.

The Mechanics of dict2items Transformation

The primary function of dict2items is to reshape data for the loop keyword. In a standard Ansible dictionary, data is stored as a mapping. For example, a variable might define a set of port mappings where the service name is the key and the port number is the value. While this is an efficient way to store data, it is not an iterable format for a task.

When the dict2items filter is applied, it decomposes the dictionary. Every single entry is converted into a new dictionary object. This object is standardized with two keys: key and value.

Consider the following technical transformation:

Original Dictionary Format Transformed List Format (via dict2items)
name: webserver {"key": "name", "value": "webserver"}
port: 8080 {"key": "port", "value": 8080}
protocol: https {"key": "protocol", "value": "https"}

This structural change has a profound impact on playbook development. It enables the developer to use the item variable within a loop to reference item.key for the attribute name and item.value for the attribute data. Without this conversion, the developer would be forced to use complex Jinja2 expressions or manual list definitions, which are prone to error and significantly harder to maintain.

Implementing Basic Dictionary Iteration

To implement dictionary iteration, the filter is typically applied directly within the loop statement. This ensures that the data is converted on-the-fly as the task begins its execution.

A practical implementation for configuring kernel parameters using the ansible.posix.sysctl module demonstrates this efficiency:

yaml - name: Configure kernel parameters ansible.posix.sysctl: name: "{{ item.key }}" value: "{{ item.value }}" state: present reload: yes loop: "{{ sysctl_settings | dict2items }}" vars: sysctl_settings: net.core.somaxconn: "1024" net.ipv4.tcp_max_syn_backlog: "2048" vm.swappiness: "10" fs.file-max: "65536"

In this scenario, the sysctl_settings dictionary provides a clean, readable overview of the desired system state. The dict2items filter converts these four parameters into a list of four items. The ansible.posix.sysctl module then receives the specific key (e.g., net.core.somaxconn) and the value (e.g., 1024) in each iteration. This approach ensures that adding a new kernel parameter only requires adding a single line to the vars section, rather than modifying the task logic itself.

Advanced Application: Complex Value Objects

The dict2items filter is not limited to simple key-string pairs. It is equally powerful when the value associated with a key is itself another dictionary. This is common in network configuration or interface management.

For example, when managing network interfaces, a dictionary might store the interface name as the key and another dictionary containing the IP address and subnet as the value.

yaml - name: Example of dict2items filter hosts: localhost gather_facts: false vars: interfaces: eth0: ip: "192.168.1.1/24" eth1: ip: "192.168.2.1/24" tasks: - ansible.builtin.debug: msg: | Interface: {{ item.key }} IP Address: {{ item.value.ip }} loop: "{{ interfaces | dict2items }}"

In this technical implementation, the transformation creates an item where item.key is eth0 and item.value is the dictionary {'ip': '192.168.1.1/24'}. To access the IP address, the developer uses dot notation (item.value.ip). This nested structure allows for highly granular control over complex system components while maintaining the simplicity of a single loop.

Practical Use Case: Managing Docker Labels

Docker labels are fundamentally key-value pairs, making them a perfect candidate for dict2items. Depending on the deployment method (CLI versus Compose), the implementation varies.

CLI-style Label Generation

When generating a docker run command, labels must be passed as separate --label flags. This can be achieved by chaining dict2items with the map and join filters.

```yaml
- name: Define container labels
ansible.builtin.setfact:
container
labels:
app: frontend
environment: production
version: "2.1.0"
maintainer: team-platform
monitoring: enabled

  • name: Show labels for Docker run command
    ansible.builtin.debug:
    msg: "docker run {{ containerlabels | dict2items | map('regexreplace', '^(.*)$', '--label \1') | join(' ') }}"
    ```

In this pipeline, dict2items converts the labels into a list, map applies a regular expression to prefix each item with --label, and join collapses the list into a single space-separated string.

Docker Compose Template Integration

For docker-compose.yml files, a template is more appropriate. Here, the loop is handled within Jinja2 syntax rather than the Ansible task level.

jinja2 {# templates/docker-compose.yml.j2 - Generate labels from dictionary #} version: "3.8" services: app: image: myapp:latest labels: {% for item in container_labels | dict2items | sort(attribute='key') %} {{ item.key }}: "{{ item.value }}" {% endfor %}

The addition of the sort(attribute='key') filter ensures that the labels are written to the file in alphabetical order, which is a best practice for version control to prevent unnecessary diffs in configuration files.

Filtering and Cleaning Dictionary Data

A sophisticated pattern in Ansible involves converting a dictionary to items, filtering out unwanted entries, and then converting the result back into a dictionary. This is often necessary when dealing with "dirty" data, such as API responses or optional configuration variables where some values might be empty strings or null.

The rejectattr filter is used in conjunction with dict2items to prune the data set.

yaml - name: Clean up configuration dict ansible.builtin.set_fact: clean_config: >- {{ raw_config | dict2items | rejectattr('value', 'equalto', '') | rejectattr('value', 'none') | list | items2dict }} vars: raw_config: db_host: db.internal db_port: "5432" db_name: myapp db_password: "" cache_host: "" cache_port: "6379"

The technical flow of this operation is as follows:
1. dict2items: Transforms the dictionary into a list of key-value objects.
2. rejectattr('value', 'equalto', ''): Removes any item where the value is an empty string.
3. rejectattr('value', 'none'): Removes any item where the value is null/none.
4. list: Ensures the filtered result is cast back into a list.
5. items2dict: Reverts the list of key-value pairs back into a standard Ansible dictionary.

This ensures that the resulting clean_config variable only contains entries that actually have usable values, preventing the application of empty configurations to the target server.

Strategic Chaining and the items2dict Reverse Filter

While dict2items is used for iteration, there are scenarios where you must return to a dictionary format. The items2dict filter performs this exact reverse operation. This is critical when receiving data from external sources, such as an API, that returns a list of objects which you then need to use as a lookup table.

Converting API Responses to Lookup Dictionaries

Consider an API that returns DNS records as a list: [{"name": "web01", "ip": "10.0.1.10"}, {"name": "db01", "ip": "10.0.2.10"}]. To use this as a dictionary where the hostname is the key, one can utilize items2dict.

yaml - name: Build config dictionary from list ansible.builtin.set_fact: config: "{{ config_items | items2dict }}" vars: config_items: - key: database_host value: db.example.com - key: database_port value: 5432 - key: database_name value: myapp

The resulting config variable becomes {"database_host": "db.example.com", "database_port": 5432, "database_name": "myapp"}, allowing for direct key-based access throughout the rest of the playbook.

Environment Variable Deployment

A highly common use case for dict2items is the creation of environment configuration files (e.g., .env files). Because these files require a KEY=VALUE format per line, a dictionary is the most logical way to store the data, but lineinfile requires a loop to process them.

yaml - name: Configure application environment ansible.builtin.lineinfile: path: /etc/myapp/environment regexp: "^{{ item.key }}=" line: "{{ item.key }}={{ item.value }}" create: yes mode: '0600' loop: "{{ app_env | dict2items }}" loop_control: label: "{{ item.key }}" vars: app_env: DATABASE_URL: "postgresql://localhost/myapp" REDIS_URL: "redis://localhost:6379" SECRET_KEY: "change-me-in-production" LOG_LEVEL: "info" WORKERS: "4"

The use of loop_control with the label attribute is a technical optimization here. By setting label: "{{ item.key }}", Ansible only prints the key of the current item in the console output during execution, rather than printing the entire key-value pair. This keeps the logs clean and prevents the accidental exposure of sensitive data, such as the SECRET_KEY, in the CI/CD logs.

Complex Key and Value Transformations

In advanced DevOps scenarios, you may need to modify keys or values globally across a dictionary. This requires a combination of dict2items, the combine filter, and sometimes a Jinja2 namespace for state management.

Prefixing Keys

To add a prefix to all keys in a dictionary (e.g., transforming env to APP_env), the following logic is used:

yaml - name: Add prefix to dict keys ansible.builtin.set_fact: prefixed_env: >- {{ env_vars | dict2items | map('combine', {'key': 'APP_' + item.key}) | list | items2dict }}

Uppercasing Keys via Namespace

For more complex transformations, such as converting all keys to uppercase, a Jinja2 loop with a namespace is employed to maintain the state of the dictionary across iterations.

yaml - name: Uppercase dict keys ansible.builtin.debug: msg: "{{ result }}" vars: original: database_host: localhost database_port: 5432 items_list: "{{ original | dict2items }}" result: >- {% set ns = namespace(d={}) %} {% for item in items_list %} {% set ns.d = ns.d | combine({item.key | upper: item.value}) %} {% endfor %} {{ ns.d }}

This process involves:
1. Converting the original dictionary to a list via dict2items.
2. Initializing a namespace object to hold the new dictionary.
3. Iterating through the list and using the combine filter to merge the uppercased key and its original value into the namespace dictionary.
4. Returning the final dictionary object.

Conditional Iteration and Filtering

The power of dict2items is most evident when combined with selectattr. This allows the playbook to execute tasks only for a subset of the dictionary based on specific criteria.

Example: Processing only SSL-enabled virtual hosts.

yaml - name: Configure only SSL-enabled virtual hosts ansible.builtin.template: src: ssl-vhost.conf.j2 dest: "/etc/nginx/sites-available/{{ item.key }}.conf" loop: >- {{ virtual_hosts | dict2items | selectattr('value.ssl') | list }} loop_control: label: "{{ item.key }}" vars: virtual_hosts: api.example.com: port: 8080 ssl: true internal.example.com: port: 9090 ssl: false app.example.com: port: 3000 ssl: true

In this technical implementation:
- The virtual_hosts dictionary is converted to a list.
- selectattr('value.ssl') filters the list to keep only those items where the ssl attribute in the value dictionary is true.
- The resulting list contains only api.example.com and app.example.com.
- The template is then applied only to these specific hosts, ensuring that non-SSL configurations are not accidentally overwritten or created.

Conclusion

The dict2items filter is an indispensable tool for any Ansible practitioner seeking to implement flexible, data-driven automation. By transforming static dictionaries into iterable lists, it bridges the gap between the efficient data storage of mappings and the requirement of the loop keyword for sequential processing. Whether it is used for simple system parameterization via sysctl, the generation of complex Docker labels, the pruning of API data through rejectattr, or the selective application of configurations using selectattr, dict2items provides the necessary flexibility to handle real-world infrastructure complexity. When paired with its counterpart, items2dict, it allows for a complete bidirectional flow of data transformation, enabling developers to manipulate configuration sets with precision and scale.

Sources

  1. Packet Coders
  2. OneUptime - How to Iterate Over a Dictionary in Ansible with dict2items
  3. Amrelboridy on LinkedIn
  4. OneUptime - How to Use the dict2items and items2dict Filters in Ansible

Related Posts