What is the set_fact Module?

The ansible.builtin.set_fact module lets you create or modify variables dynamically during playbook execution. Unlike variables defined in vars, group_vars, or host_vars, facts set with set_fact are computed at runtime and can depend on the results of previous tasks.

This makes set_fact essential for:

  • Transforming data from API responses or command output
  • Building conditional variables based on host properties
  • Creating computed values from multiple sources
  • Setting host-specific facts that persist across plays (with cacheable: true)

Basic Syntax

The simplest use of set_fact creates a variable with a static or computed value:

- name: Set a simple fact
  ansible.builtin.set_fact:
    my_environment: production

- name: Set a fact from another variable
  ansible.builtin.set_fact:
    config_path: "/etc/{{ app_name }}/config.yml"

You can set multiple facts in a single task:

- name: Set multiple facts
  ansible.builtin.set_fact:
    app_port: 8080
    app_host: "{{ ansible_default_ipv4.address }}"
    app_url: "http://{{ ansible_default_ipv4.address }}:8080"

set_fact vs register: When to Use Each

A common source of confusion is when to use set_fact versus register. Here's the distinction:

register captures the entire output of a task:

- name: Get disk usage
  ansible.builtin.command: df -h /
  register: disk_output
# disk_output.stdout contains the full output

set_fact creates a variable with a specific, often transformed, value:

- name: Get disk usage
  ansible.builtin.command: df -h / --output=pcent
  register: disk_output

- name: Extract disk usage percentage
  ansible.builtin.set_fact:
    disk_usage_percent: "{{ disk_output.stdout_lines[1] | trim | replace('%', '') | int }}"

Use register when you need the raw output of a task. Use set_fact when you need a clean, transformed value.

Practical Examples

Conditional Fact Setting

Set different values based on the operating system:

- name: Set package manager based on OS
  ansible.builtin.set_fact:
    pkg_manager: "{{ 'apt' if ansible_os_family == 'Debian' else 'yum' }}"
    service_manager: "{{ 'systemd' if ansible_service_mgr == 'systemd' else 'sysvinit' }}"

Building Dynamic Data Structures

Create lists and dictionaries dynamically:

- name: Build server configuration
  ansible.builtin.set_fact:
    server_config:
      hostname: "{{ inventory_hostname }}"
      ip: "{{ ansible_default_ipv4.address }}"
      environment: "{{ env | default('staging') }}"
      services:
        - nginx
        - postgresql
        - redis

Extracting Data from API Responses

A common pattern is querying an API and storing specific fields:

- name: Get cluster info
  ansible.builtin.uri:
    url: "https://api.example.com/clusters/{{ cluster_name }}"
    headers:
      Authorization: "Bearer {{ api_token }}"
  register: cluster_response

- name: Extract cluster details
  ansible.builtin.set_fact:
    cluster_endpoint: "{{ cluster_response.json.endpoint }}"
    cluster_version: "{{ cluster_response.json.version }}"
    cluster_nodes: "{{ cluster_response.json.nodes | length }}"

Accumulating Facts in a Loop

Build a list by appending items in a loop:

- name: Gather service ports
  ansible.builtin.set_fact:
    active_ports: "{{ active_ports | default([]) + [item.port] }}"
  loop:
    - { name: 'web', port: 80 }
    - { name: 'api', port: 8080 }
    - { name: 'db', port: 5432 }
  when: item.name in enabled_services

Using set_fact with Filters

Combine set_fact with Jinja2 filters for data transformation:

- name: Get available packages
  ansible.builtin.uri:
    url: "{{ centos_repo }}"
    return_content: true
  register: available_packages

- name: Extract kernel packages
  ansible.builtin.set_fact:
    kernel: "{{ available_packages.content | regex_replace('<.*?>') | regex_findall('kernel-[0-9].*rpm') }}"

- name: Display found packages
  ansible.builtin.debug:
    var: kernel

Date and Time Facts

Set facts using the current date and time:

- name: Set timestamp facts
  ansible.builtin.set_fact:
    backup_date: "{{ lookup('pipe', 'date +%Y-%m-%d') }}"
    backup_timestamp: "{{ lookup('pipe', 'date +%Y-%m-%d-%H%M%S') }}"
    backup_dir: "/backups/{{ lookup('pipe', 'date +%Y-%m-%d') }}"

Cacheable Facts

By default, facts set with set_fact only persist for the current playbook run. To make them survive across runs, use cacheable: true:

- name: Set a cacheable fact
  ansible.builtin.set_fact:
    last_deployment_time: "{{ ansible_date_time.iso8601 }}"
    cacheable: true

This requires a fact caching backend to be configured in ansible.cfg:

[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400

Variable Precedence

Facts set with set_fact have high precedence in Ansible's variable hierarchy. They override:

  • Inventory variables (host_vars, group_vars)
  • Variables from vars sections
  • Variables from vars_files
  • Role defaults

Only extra_vars (-e on command line) take higher precedence than set_fact.

Common Mistakes

Forgetting Jinja2 Quotes

# Wrong — interpreted as YAML, not a Jinja2 expression
- ansible.builtin.set_fact:
    my_list: [1, 2, 3]

# Correct — explicit Jinja2 expression
- ansible.builtin.set_fact:
    my_list: "{{ [1, 2, 3] }}"

Type Handling

Variables set with set_fact retain their Python type when using Jinja2 expressions:

- ansible.builtin.set_fact:
    count_string: "42"          # String
    count_int: "{{ 42 }}"       # Integer
    is_enabled: "{{ true }}"    # Boolean

Undefined Variable Errors

Always provide defaults when a variable might not exist:

- ansible.builtin.set_fact:
    app_port: "{{ custom_port | default(8080) }}"
    app_debug: "{{ debug_mode | default(false) }}"

Performance Considerations

  • set_fact runs on each host individually — in a play with 100 hosts, the task executes 100 times
  • Use run_once: true when the fact is the same for all hosts to reduce execution time
  • For large data structures, consider using vars or include_vars instead of set_fact
- name: Set a fact once for all hosts
  ansible.builtin.set_fact:
    shared_config_version: "{{ lookup('file', 'VERSION') }}"
  run_once: true

Conclusion

The ansible.builtin.set_fact module is one of Ansible's most versatile tools for dynamic playbook logic. It bridges the gap between static configuration and runtime computation, enabling playbooks that adapt to their environment.

Key takeaways:

  • Use set_fact for computed, transformed, or conditional values
  • Use register for raw task output, then set_fact to extract what you need
  • Enable cacheable: true when facts should persist across runs
  • Combine with run_once for facts that are identical across all hosts
  • Always provide sensible defaults with the default() filter