Ansible Custom Facts — Create Local Facts for Host-Specific Data

Introduction

Ansible gathers system facts automatically (OS, IP, memory, etc.), but you often need host-specific data that doesn't come from the system — application version, deployment environment, business unit, or custom health metrics. Custom facts let you define this data on each host and use it in playbooks just like built-in facts.

Local Facts (fact.d)

Place .fact files in /etc/ansible/facts.d/ on the remote host. Ansible reads them during gather_facts and makes them available as ansible_local.

INI Format

# /etc/ansible/facts.d/app.fact
[general]
version=2.5.1
environment=production
deployed_by=ansible

[database]
host=db01.example.com
port=5432
name=myapp_production
- name: Use local facts
  hosts: all
  tasks:
    - name: Show app version
      ansible.builtin.debug:
        msg: "App v{{ ansible_local.app.general.version }} in {{ ansible_local.app.general.environment }}"

    - name: Show database config
      ansible.builtin.debug:
        msg: "DB: {{ ansible_local.app.database.host }}:{{ ansible_local.app.database.port }}"

JSON Format

{
  "app_name": "myapp",
  "version": "2.5.1",
  "features": ["api", "websocket", "caching"],
  "limits": {
    "max_connections": 1000,
    "timeout_seconds": 30
  }
}

Save as /etc/ansible/facts.d/app.fact — Ansible detects JSON automatically.

    - name: Use JSON fact
      ansible.builtin.debug:
        msg: "Features: {{ ansible_local.app.features | join(', ') }}"

Executable Fact Scripts

Make the .fact file executable — Ansible runs it and parses the JSON output:

#!/bin/bash
# /etc/ansible/facts.d/health.fact (chmod +x)
LOAD=$(cat /proc/loadavg | cut -d' ' -f1)
MEM_PCT=$(free | awk '/Mem:/ {printf "%.1f", $3/$2*100}')
DISK_PCT=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
UPTIME=$(cat /proc/uptime | cut -d' ' -f1 | cut -d'.' -f1)

cat <<EOF
{
  "load": $LOAD,
  "memory_percent": $MEM_PCT,
  "disk_percent": $DISK_PCT,
  "uptime_seconds": $UPTIME,
  "healthy": $([ $(echo "$LOAD < 4" | bc) -eq 1 ] && echo "true" || echo "false")
}
EOF
    - name: Check host health from custom fact
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }}: load={{ ansible_local.health.load }}, healthy={{ ansible_local.health.healthy }}"

Deploy Custom Facts with Ansible

- name: Set up custom facts
  hosts: all
  become: true
  tasks:
    - name: Create facts directory
      ansible.builtin.file:
        path: /etc/ansible/facts.d
        state: directory
        mode: '0755'

    - name: Deploy application fact
      ansible.builtin.template:
        src: templates/app.fact.j2
        dest: /etc/ansible/facts.d/app.fact
        mode: '0644'

    - name: Deploy health check script
      ansible.builtin.copy:
        src: files/health.fact
        dest: /etc/ansible/facts.d/health.fact
        mode: '0755'

    - name: Re-read facts after deploying
      ansible.builtin.setup:
        filter: ansible_local

    - name: Verify custom facts
      ansible.builtin.debug:
        var: ansible_local

Template for Dynamic Facts

{# templates/app.fact.j2 #}
[deployment]
version={{ app_version }}
environment={{ env }}
deployed_at={{ ansible_date_time.iso8601 }}
deployed_by=ansible
host={{ inventory_hostname }}

[config]
workers={{ ansible_processor_vcpus }}
port={{ app_port | default(8080) }}

Custom Facts Module (set_fact)

For facts that don't need to persist on disk:

    - name: Compute dynamic facts
      ansible.builtin.set_fact:
        is_primary: "{{ inventory_hostname == groups['db_cluster'][0] }}"
        memory_tier: "{{ 'large' if ansible_memtotal_mb > 16384 else 'medium' if ansible_memtotal_mb > 4096 else 'small' }}"
        app_url: "https://{{ inventory_hostname }}.{{ domain }}"

Local Facts vs set_fact

FeatureLocal Facts (facts.d)set_fact
Stored onRemote host diskIn-memory
Persists across runs✅❌ (unless cacheable)
Needs gather_facts✅❌
Namespaceansible_local.*Top-level
Dynamic computationVia executable scriptsVia Jinja2
Use caseHost identity, app versionRuntime calculations

Practical Patterns

Application Version Tracking

    # After deployment, update the version fact
    - name: Deploy application
      ansible.builtin.unarchive:
        src: "app-{{ version }}.tar.gz"
        dest: /opt/app/

    - name: Update version fact
      ansible.builtin.copy:
        content: |
          [app]
          version={{ version }}
          deployed={{ ansible_date_time.iso8601 }}
        dest: /etc/ansible/facts.d/app.fact
      become: true

    # Next playbook run can check: ansible_local.app.app.version

Server Role Classification

    # /etc/ansible/facts.d/role.fact
    # [server]
    # role=webserver
    # tier=frontend
    # datacenter=us-east-1

    - name: Configure based on server role
      ansible.builtin.include_role:
        name: "{{ ansible_local.role.server.role }}"
      when: ansible_local.role is defined

Troubleshooting

IssueSolution
ansible_local is emptyCreate /etc/ansible/facts.d/ and add .fact files
Fact file not parsedCheck file extension is .fact (not .json or .ini)
Executable fact failsCheck chmod +x, valid JSON output, and shebang line
Stale facts after deployRun ansible.builtin.setup: filter=ansible_local to re-read
Permission deniedFacts directory needs to be readable by the Ansible user

Best Practices

  1. Use facts.d for persistent host identity — app version, environment, role
  2. Use set_fact for runtime calculations — computed values, conditionals
  3. Deploy facts with Ansible — template .fact files during provisioning
  4. Re-read after changes — setup: filter=ansible_local after updating facts
  5. Executable facts for dynamic data — health metrics, service status
  6. Namespace with sections — [app], [database], [network] for organization

Conclusion

Custom facts extend Ansible's knowledge of your hosts beyond OS-level data. Local fact files in /etc/ansible/facts.d/ persist across runs and provide host identity (version, role, environment). Executable fact scripts compute dynamic data like health metrics. Together with set_fact for runtime calculations, custom facts give you a complete data layer for intelligent automation decisions.