When working with Ansible, you'll frequently encounter situations where you need to dynamically modify list or dictionary variables across multiple roles or playbooks. The common approach using Jinja2 templates feels hacky and comes with several drawbacks:
- Lack of official documentation
- Potential side effects in complex playbooks
- Poor readability and maintainability
- No built-in idempotency guarantees
1. Using set_fact with Jinja2 Filters
While still using Jinja2, this approach is more explicit and maintainable:
- name: Append to list variable
set_fact:
originalvar: "{{ originalvar + ['new_item'] }}"
- name: Add dictionary key
set_fact:
originaldict: "{{ originaldict | combine({'new_key': 'value'}) }}"
2. Custom Filters for Complex Operations
For more complex scenarios, creating custom filters provides better organization:
# In filter_plugins/custom_filters.py
def append_filter(value, new_items):
if isinstance(value, list):
return value + new_items
return value
class FilterModule(object):
def filters(self):
return {'append': append_filter}
Usage in playbook:
- name: Use custom append filter
set_fact:
originalvar: "{{ originalvar | append(['x']) }}"
For Simple Config Files (CSV, INI)
The lineinfile module with regex can handle basic cases:
- name: Update shared_preload_libraries
lineinfile:
path: /etc/postgresql/postgresql.conf
regexp: "^shared_preload_libraries = "
line: "shared_preload_libraries = '{{ shared_preload_libraries | join(', ') }}'"
backrefs: yes
For Complex Config Files (XML, JSON)
Use specialized modules or template the entire file:
- name: Process XML configuration
xml:
path: /path/to/config.xml
xpath: /configurations/setting[@name='shared_libs']
attribute: value
value: "{{ current_value + ',new_lib' }}"
To ensure your modifications remain idempotent:
- Always check for item existence before appending
- Use unique identifiers in dictionary keys
- Implement custom logic in filters when needed
- name: Conditionally append to list
set_fact:
originalvar: "{{ originalvar + ['new_item'] if 'new_item' not in originalvar else originalvar }}"
- Define clear variable naming conventions
- Use group_vars or host_vars for shared configuration
- Document expected variable structures in role READMEs
- Consider using include_vars for complex configurations
Remember that while these solutions work, Ansible's philosophy favors immutable configurations where possible. For complex dynamic scenarios, you might want to evaluate whether a different configuration management tool would be more appropriate.
When working with Ansible, you'll often encounter situations where you need to dynamically modify lists or dictionaries. The common workaround using Jinja2 template expressions feels hacky and undocumented:
- name: This is a questionable approach
shell: echo "{% originalvar.append('x') %}New value of originalvar is {{originalvar}}"
This method raises several concerns:
- It relies on undocumented behavior of Jinja2 in Ansible
- It's not idempotent by default
- It can lead to unpredictable results in complex playbooks
Using the combine Filter
For dictionaries, the combine
filter provides a cleaner solution:
- name: Merge dictionaries properly
set_fact:
merged_dict: "{{ original_dict | combine(new_items) }}"
vars:
new_items:
key1: value1
key2: value2
List Concatenation with + Operator
For lists, you can use the +
operator:
- name: Combine lists safely
set_fact:
combined_list: "{{ original_list + ['new_item1', 'new_item2'] }}"
For your specific database extension use case, consider these approaches:
Template-Based Solution
Create a template that handles all possible extensions:
# postgresql.conf.j2
shared_preload_libraries = '{% if postgres_extensions %}{{ postgres_extensions | join(", ") }}{% endif %}'
Then in your playbook:
- name: Gather extensions from all roles
set_fact:
postgres_extensions: "{{ postgres_extensions | default([]) + [current_extension] }}"
- name: Configure PostgreSQL
template:
src: postgresql.conf.j2
dest: /etc/postgresql/12/main/postgresql.conf
Using lineinfile with Lookup Plugins
For more complex scenarios where you need to modify existing lines:
- name: Update shared_preload_libraries
lineinfile:
path: /etc/postgresql/12/main/postgresql.conf
regexp: '^shared_preload_libraries\s*='
line: "shared_preload_libraries = '{{ lookup('template', 'extensions.j2') }}'"
backrefs: yes
XML Configuration Handling
For XML files, use the xml
module:
- name: Modify XML configuration
xml:
path: /path/to/config.xml
xpath: /configuration/settings
attribute: value
value: "{{ new_value }}"
add_children:
- element: new_element
text: new_text
Custom Filters for Complex Logic
Create custom filters in Python when you need sophisticated processing:
# filter_plugins/custom_filters.py
def merge_config_values(value, new_values):
# Custom merge logic here
return merged_value
class FilterModule(object):
def filters(self):
return {
'merge_config': merge_config_values
}
Then use it in your playbook:
- name: Apply custom merge
set_fact:
final_config: "{{ original_config | merge_config(new_config) }}"
To make your configuration changes idempotent:
- Always use
changed_when
with proper checks - Implement proper state checking before modifications
- Consider using
assert
tasks to validate preconditions
- name: Verify extension is not already present
assert:
that: current_extension not in postgres_extensions
fail_msg: "Extension {{ current_extension }} is already configured"