Ansible first_found: predictable host and environment config fallback
Use ansible.builtin.first_found to select host, group, environment, and default configuration files in an explicit, testable order.
Published · Updated
Problem
Ansible roles become difficult to maintain when each host or environment needs a slightly different configuration file.
Why it happens
Conditional branches accumulate inside tasks and templates, obscuring which file wins and making fallback behavior difficult to test.
Recommended approach
Use first_found to resolve files in an explicit order, from the most specific candidate to a safe default. Keep the precedence visible beside the lookup rather than distributing it across multiple tasks.
Example precedence
Example lookup order:
inventory_hostname.ymlgroup_name.ymldefault.yml
Verify the selected file with a debug task during development, and fail clearly when no default is acceptable.
Use this when
- Agent profiles
- Service config
- Environment-specific files
- Reusable roles
Avoid this when
- When config must be generated dynamically from variables
Template the first matching file
Use the fully qualified lookup name and keep the precedence in one task:
yaml
- name: Select the most specific service configuration
ansible.builtin.set_fact:
service_config_source: >-
{{ lookup('ansible.builtin.first_found', service_config_candidates) }}
vars:
service_config_candidates:
files:
- "{{ inventory_hostname }}.conf"
- "{{ environment_name }}.conf"
- "{{ ansible_os_family | lower }}.conf"
- default.conf
paths:
- "{{ role_path }}/files/service"
- name: Install the selected configuration
ansible.builtin.copy:
src: "{{ service_config_source }}"
dest: /etc/example/service.conf
owner: root
group: root
mode: "0644"
notify: Restart example serviceThe file list has precedence over searched paths. Keep a mandatory default when every supported host must resolve a configuration.
Optional lookup behavior
If no file is acceptable, let the lookup fail. If absence is an expected state, use skip: true deliberately and handle the empty result:
yaml
vars:
optional_candidates:
files:
- "{{ inventory_hostname }}.yml"
- "{{ environment_name }}.yml"
paths:
- "{{ role_path }}/vars/optional"
skip: trueDo not use errors='ignore' merely to hide a missing required default.
Verify the chosen file
yaml
- name: Show selected configuration during validation
ansible.builtin.debug:
var: service_config_source
when: ansible_check_modeRun representative hosts with --check --diff and test every fallback level. A lookup that works only for the most-specific host file has not proven the default path.