Ansible product vs combine: List Cartesian Product & Dict Merging
By Luca Berton · Published 2024-01-01 · Category: troubleshooting
How to use Ansible product filter for list combinations and combine filter for dictionary merging. Cartesian products, dict merging, and data manipulation.
Introduction
In the realm of Ansible automation, managing complex data structures is often required. Two powerful tools, the product and combine filters, play distinct roles in manipulating lists and dictionaries, respectively. Understanding their use cases can significantly enhance your playbook efficiency.
See also: Ansible Split String: Filter Guide for CSV, Delimiters & Lists
product: Cartesian Product of Lists
The product filter in Ansible generates the Cartesian product of two or more lists, producing all possible combinations of their elements. This is particularly useful when you need to iterate over multiple sets of values to configure systems or environments.
Example: Configuring Hosts Across Environments
- hosts: localhost
gather_facts: no
tasks:
- name: Generate combinations of environments and roles
ansible.builtin.debug:
msg: "{{ ['dev', 'test', 'prod'] | product(['web', 'db']) | list }}"
Output:
[
["dev", "web"], ["dev", "db"],
["test", "web"], ["test", "db"],
["prod", "web"], ["prod", "db"]
]
In this example, the product filter creates pairs of environments and roles, enabling scalable and dynamic configuration.
Use Case:
• Generating matrix-style combinations for testing or deployments. • Configuring multiple environments in one go.combine: Merging Dictionaries
The combine filter allows you to merge multiple dictionaries into one, providing a way to consolidate configurations or override existing values.
Example: Merging Configuration Maps
- hosts: localhost
gather_facts: no
tasks:
- name: Merge default and custom configurations
ansible.builtin.debug:
msg: "{{ {'default_key': 'default_value'} | combine({'custom_key': 'custom_value'}) }}"
Output:
{
"default_key": "default_value",
"custom_key": "custom_value"
}
You can use the combine filter to dynamically construct configurations or override default values with user-provided data.
Use Case:
• Building dynamic variables from defaults and user inputs. • Consolidating configurations for modular roles or playbooks.See also: Ansible Transform JSON Data: Filters for Parsing & Manipulating JSON
Combining product and combine for Advanced Scenarios
Sometimes, these filters can be used together to manage complex scenarios, such as creating a dictionary of all possible role-environment pairs.
Example:
- hosts: localhost
gather_facts: no
tasks:
- name: Create a dictionary of environment-role pairs
ansible.builtin.debug:
msg: >-
{{
dict(
['dev', 'test', 'prod'] | product(['web', 'db']) |
map('join', '_') |
map('extract', 0, ['web', 'db'])
)
}}
This advanced usage combines Cartesian products and dictionary manipulations to build intricate data structures for automation workflows.
Conclusion
Understanding the nuances of the product and combine filters empowers you to handle lists and dictionaries effectively in Ansible. Whether you're iterating over combinations or merging configurations, these tools are essential for robust and flexible automation playbooks.
For more detailed examples and real-world use cases, explore additional Ansible resources or check out my book Ansible by Example.
See also: Automating PostgreSQL Configuration with Ansible Setting Maximum Connections
product Filter (Lists)
Creates all combinations (cartesian product) of two or more lists:
- vars:
colors: [red, blue]
sizes: [S, M, L]
debug:
msg: "{{ colors | product(sizes) | list }}"
# [['red','S'], ['red','M'], ['red','L'],
# ['blue','S'], ['blue','M'], ['blue','L']]
Real-world: Create directories for all app/env combos
- vars:
apps: [frontend, backend, api]
envs: [dev, staging, prod]
ansible.builtin.file:
path: "/opt/{{ item.0 }}/{{ item.1 }}"
state: directory
loop: "{{ apps | product(envs) | list }}"
Three-way product
- vars:
regions: [us-east, eu-west]
envs: [dev, prod]
services: [web, api]
debug:
msg: "{{ regions | product(envs, services) | list }}"
# All 8 combinations
combine Filter (Dictionaries)
Merges two or more dictionaries (later values win):
- vars:
defaults:
port: 8080
debug: false
log_level: info
overrides:
debug: true
port: 9090
debug:
msg: "{{ defaults | combine(overrides) }}"
# {port: 9090, debug: true, log_level: info}
Deep merge (recursive)
- vars:
base:
database:
host: localhost
port: 5432
cache:
ttl: 300
override:
database:
host: db.prod.com
debug:
msg: "{{ base | combine(override, recursive=True) }}"
# database: {host: db.prod.com, port: 5432}, cache: {ttl: 300}
Merge multiple dicts
- vars:
a: { x: 1 }
b: { y: 2 }
c: { z: 3 }
debug:
msg: "{{ a | combine(b, c) }}"
# {x: 1, y: 2, z: 3}
product vs combine
| Filter | Input | Output | Use Case |
|--------|-------|--------|----------|
| product | Lists | List of tuples | All combinations for looping |
| combine | Dicts | Merged dict | Override defaults, merge configs |
Related Filters
| Filter | Description |
|--------|-------------|
| zip | Pair elements at same index |
| union | Unique elements from both lists |
| intersect | Elements in both lists |
| difference | Elements in first but not second |
# zip pairs by index
- vars:
keys: [name, age]
vals: [Alice, 30]
debug:
msg: "{{ dict(keys | zip(vals)) }}"
# {name: Alice, age: 30}
FAQ
How is product different from with_nested?
They're equivalent. product is the modern filter approach:
# Old style
with_nested:
- [a, b]
- [1, 2]
# Modern style
loop: "{{ ['a','b'] | product([1,2]) | list }}"
Does combine overwrite or merge nested dicts?
By default, it overwrites. Use recursive=True for deep merge.
product — Cross-Product of Lists
- vars:
environments: [dev, staging, prod]
services: [web, api, worker]
debug:
msg: "{{ item.0 }}-{{ item.1 }}"
loop: "{{ environments | product(services) | list }}"
# dev-web, dev-api, dev-worker, staging-web, staging-api, ...
combine — Merge Dictionaries
- vars:
defaults:
port: 80
workers: 4
debug: false
overrides:
port: 8080
debug: true
set_fact:
config: "{{ defaults | combine(overrides) }}"
# { port: 8080, workers: 4, debug: true }
product Use Cases
Create User+Group Pairs
- vars:
users: [alice, bob]
groups: [docker, sudo]
user:
name: "{{ item.0 }}"
groups: "{{ item.1 }}"
append: true
loop: "{{ users | product(groups) | list }}"
become: true
Multi-Region Deployment
- vars:
regions: [us-east-1, eu-west-1]
services: [frontend, backend, cache]
debug:
msg: "Deploy {{ item.1 }} to {{ item.0 }}"
loop: "{{ regions | product(services) | list }}"
Three-Way Product
- vars:
envs: [dev, prod]
apps: [web, api]
actions: [deploy, monitor]
debug:
msg: "{{ item.0 }}/{{ item.1 }}/{{ item.2 }}"
loop: "{{ envs | product(apps, actions) | list }}"
combine Use Cases
Environment-Specific Config
- vars:
base_config:
log_level: info
port: 8080
workers: 4
prod_overrides:
log_level: warn
workers: 16
set_fact:
final: "{{ base_config | combine(prod_overrides) }}"
Deep Merge
- vars:
dict1:
database:
host: localhost
port: 5432
dict2:
database:
host: db.prod.com
set_fact:
# Shallow (default) — replaces entire 'database' key
shallow: "{{ dict1 | combine(dict2) }}"
# { database: { host: db.prod.com } } -- port is LOST!
# Deep merge — preserves nested keys
deep: "{{ dict1 | combine(dict2, recursive=true) }}"
# { database: { host: db.prod.com, port: 5432 } }
Merge Multiple Dicts
- set_fact:
final: "{{ defaults | combine(group_vars) | combine(host_vars) | combine(extra_vars) }}"
Merge List of Dicts
- vars:
config_layers:
- { port: 80 }
- { workers: 4 }
- { debug: true }
set_fact:
merged: "{{ config_layers | combine }}"
# { port: 80, workers: 4, debug: true }
Key Differences
| Feature | product | combine |
|---------|-----------|-----------|
| Input | Lists | Dictionaries |
| Output | List of tuples | Merged dict |
| Purpose | Cross-product (all combinations) | Merge/overlay |
| Typical use | Nested loops | Config layering |
| recursive | N/A | Deep merge option |
FAQ
product vs with_nested?
Same result — loop: "{{ a | product(b) }}" is the modern equivalent of with_nested: [a, b].
combine overwrites or merges?
By default, overwrites — later values replace earlier ones. Use recursive=true for deep merge of nested dicts.
Can I combine with list_merge?
# Control how lists are merged in recursive combine
merged: "{{ dict1 | combine(dict2, recursive=true, list_merge='append') }}"
product (Cartesian Product of Lists)
- vars:
servers: [web1, web2]
ports: [80, 443]
debug:
msg: "{{ item.0 }}:{{ item.1 }}"
loop: "{{ servers | product(ports) | list }}"
# web1:80, web1:443, web2:80, web2:443
combine (Merge Dictionaries)
- vars:
defaults:
port: 80
debug: false
log_level: info
overrides:
port: 8080
debug: true
debug:
msg: "{{ defaults | combine(overrides) }}"
# { port: 8080, debug: true, log_level: info }
product with Multiple Lists
- vars:
envs: [dev, staging, prod]
regions: [us-east, eu-west]
sizes: [small, large]
debug:
msg: "{{ item.0 }}-{{ item.1 }}-{{ item.2 }}"
loop: "{{ envs | product(regions, sizes) | list }}"
# dev-us-east-small, dev-us-east-large, dev-eu-west-small, ...
Recursive combine
- vars:
base:
database:
host: localhost
port: 5432
cache:
enabled: false
env_specific:
database:
host: db.prod.example.com
cache:
enabled: true
debug:
msg: "{{ base | combine(env_specific, recursive=true) }}"
# { database: { host: db.prod.example.com, port: 5432 }, cache: { enabled: true } }
Real-World: Multi-Environment Config
- vars:
base_config:
app_name: myapp
log_level: info
workers: 2
env_configs:
production:
log_level: error
workers: 8
staging:
log_level: debug
workers: 4
set_fact:
final_config: "{{ base_config | combine(env_configs[env]) }}"
product for Test Matrix
# Generate all test combinations
- vars:
python_versions: ["3.10", "3.11", "3.12"]
ansible_versions: ["2.16", "2.17"]
debug:
msg: "Test Python {{ item.0 }} + Ansible {{ item.1 }}"
loop: "{{ python_versions | product(ansible_versions) | list }}"
Combine Multiple Dicts
- vars:
a: { x: 1 }
b: { y: 2 }
c: { z: 3 }
debug:
msg: "{{ a | combine(b, c) }}"
# { x: 1, y: 2, z: 3 }
FAQ
combine overwrites or merges nested dicts?
By default, it overwrites nested dicts entirely. Use recursive=true to deep-merge nested dictionaries.
product vs with_nested?
loop: "{{ a | product(b) | list }}" replaces the deprecated with_nested: [a, b]. Same result, modern syntax.
Can I combine lists?
Use + or union for lists: {{ list_a + list_b }} or {{ list_a | union(list_b) }}. combine is for dicts only.
Related Articles
• structuring playbooks with Ansible rolesCategory: troubleshooting