Infra Notes

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

AutomationAnsibleConfig ManagementRolesMaintainability

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.yml
  • group_name.yml
  • default.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 service

The 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: true

Do 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_mode

Run 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.

References