Managing Complex YAML Structures Dynamically with Ansible

In modern infrastructure automation, working with structured data is essential. YAML, due to its readability and simplicity, is the format of choice for tools like Ansible, Kubernetes, and many others. As your automation tasks grow, you may find yourself needing to dynamically manipulate YAML files – particularly to extend or modify nested data structures.

Problem Scenario

Imagine you have a YAML (for example inventory) file with a complex structure like this:

hosts:
  ansible:
    docker:
      ansible_host: 1.2.3.4
    podman:
      ansible_host: 3.4.5.6

Your goal is to dynamically add a new entry to the ansible dict, such as:

{kvm: {ansible_host: 5.6.7.8}}

Rather than manually updating the file, Ansible can automate this modification by reading the YAML file, appending new data, and saving the updated structure back to disk.

Solution Overview

We can achieve this task using the following steps:

  1. Load the existing YAML file using the Ansible lookup function and the from_yaml filter.
  2. Modify the nested structure by adding new data to the desired list.
  3. Save the updated YAML data back to disk using the copy module.

Let’s break this down step-by-step.

Step 1: Load the YAML file

To load a YAML file into an Ansible playbook, we can use the lookup function combined with the from_yaml filter. This allows Ansible to treat the YAML content as a dictionary (or list, depending on the structure), making it easy to manipulate in subsequent tasks.

- name: Load the YAML file
  ansible.builtin.set_fact:
    yaml_data: "{{ lookup('file', 'path_to_yaml_file.yaml') | from_yaml }}"

In this example, the YAML file is loaded from the specified path, and its content is stored in the variable yaml_data.

Step 2: Modify the nested structure

Once the YAML data is loaded, the next step is to modify the hosts.ansible list by adding a new entry. Using Ansible’s powerful templating engine, we can append new data to the existing list:

- name: Add new host to ansible list
  ansible.builtin.set_fact:
    updated_ansible_list: "{{ yaml_data.hosts.ansible + [{'kvm': {'ansible_host': '5.6.7.8'}}] }}"

In this task:

  • We access the nested hosts.ansible list using dot notation (yaml_data.hosts.ansible).
  • We append a new dictionary ({'kvm': {'ansible_host': '5.6.7.8'}}) to the existing list using the + operator.

The updated list is stored in the variable updated_ansible_list.

Step 3: Update the original data structure

Now that we have modified the list, the next step is to integrate this updated list back into the original data structure. Ansible’s combine filter allows us to merge dictionaries efficiently, ensuring that only the modified part of the structure is updated:

- name: Update the ansible hosts in the YAML data
  ansible.builtin.set_fact:
    updated_yaml_data: "{{ yaml_data | combine({'hosts': {'ansible': updated_ansible_list}}) }}"

Here, we merge the updated ansible list back into the hosts dictionary, ensuring that the original structure remains intact except for the modified portion.

Step 4: Save the updated YAML to disk

Finally, we can write the updated YAML data back to disk using the copy module. The to_nice_yaml filter ensures that the YAML is saved in a human-readable format.

- name: Save the updated YAML to disk
  ansible.builtin.copy:
    dest: "path_to_save_yaml_file.yaml"
    owner: tmolnar
    group: tmolnar
    mode: '0600'
    content: "{{ updated_yaml_data | to_nice_yaml }}"

This task writes the updated structure to the specified file, preserving the changes we made.

Full Example Playbook

Below is the complete Ansible playbook that implements all the steps discussed:

---
- name: Add a new host to the ansible list
  hosts: localhost
  tasks:
    - name: Load the YAML file
      ansible.builtin.set_fact:
        yaml_data: "{{ lookup('file', 'path_to_yaml_file.yaml') | from_yaml }}"

    - name: Add new host to ansible list
      ansible.builtin.set_fact:
        updated_ansible_list: "{{ yaml_data.hosts.ansible + [{'kvm': {'ansible_host': '5.6.7.8'}}] }}"

    - name: Update the ansible hosts in the YAML data
      ansible.builtin.set_fact:
        updated_yaml_data: "{{ yaml_data | combine({'hosts': {'ansible': updated_ansible_list}}) }}"

    - name: Save the updated YAML to disk
      ansible.builtin.copy:
        dest: "path_to_save_yaml_file.yaml"
        owner: tmolnar
        group: tmolnar
        mode: '0600'
        content: "{{ updated_yaml_data | to_nice_yaml }}"

Key Points to Remember

  • Reading and writing YAML: Use the lookup function with from_yaml to load YAML files and the copy module with to_nice_yaml to save them.
  • Modifying nested data: Access and modify complex structures by combining Ansible’s set_fact with filters like combine and list operators like +.
  • Preserving structure: When modifying parts of a deeply nested structure, ensure that only the relevant portion is changed by merging the updated data back into the original structure.

Conclusion

Ansible makes it easy to manage complex data structures within YAML files. By leveraging built-in functions and filters, you can read, modify, and save YAML data in a programmatic and repeatable way. This approach not only reduces manual intervention but also enhances the consistency and scalability of your automation workflows.

In environments where configuration changes are frequent, this technique is invaluable for ensuring that structured data—like infrastructure inventories—remains up-to-date and well-organized.

Leave a comment