Effective Ways to Modify Lists and Dictionaries Dynamically in Ansible Playbooks


2 views

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 }}"
  1. Define clear variable naming conventions
  2. Use group_vars or host_vars for shared configuration
  3. Document expected variable structures in role READMEs
  4. 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"