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:
- Load the existing YAML file using the Ansible
lookupfunction and thefrom_yamlfilter. - Modify the nested structure by adding new data to the desired list.
- Save the updated YAML data back to disk using the
copymodule.
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.ansiblelist 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
lookupfunction withfrom_yamlto load YAML files and thecopymodule withto_nice_yamlto save them. - Modifying nested data: Access and modify complex structures by combining Ansible’s
set_factwith filters likecombineand 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.