Ansible - Using the set_fact module
Refreshed June 2026: the original solved this with six
set_facttasks, one per OS and webserver combination, each gated by awhen. The cleaner pattern now is to put the values in a data structure and resolve them with a singleset_factthat indexes into it. Same module, far fewer tasks. Module names are shown with FQCN and the output below was produced on ansible-core 2.19.
Background
I was updating an Ansible role to install Cacti and wanted to let the user pick the back-end webserver: Apache, NGINX, or Lighttpd. Each choice, on each OS family, needs a different group, owner, web root, and service handler. Spreading those across a pile of user-defined variables got messy fast, so I reached for set_fact to compute them from one simple choice.
What I needed to resolve for any given run:
cacti_web_groupandcacti_web_owner: the webserver’s group and user, which differ by OS and webserver.cacti_web_root: the default document root.cacti_webserver_handler: the service name to notify for a restart.
The Original Approach
The 2016 version of this post wrote one set_fact task per combination, six in total, each with a when like this:
- name: setting fact Debian nginx
ansible.builtin.set_fact:
cacti_web_group: "www-data"
cacti_web_owner: "www-data"
cacti_web_root: "/usr/share/nginx/html"
cacti_webserver_handler: "nginx"
when: >
ansible_os_family == "Debian" and
cacti_webserver_type == "nginx"
It works, but it scales badly. Three webservers across two OS families is six tasks. Add a webserver or an OS and you are writing more nearly identical blocks.
The Cleaner Way: Data Plus One Lookup
Put the same information in a nested dictionary keyed by OS family and webserver type, then let one set_fact task select the right branch. Define the data once, for example in vars/main.yml:
cacti_webserver_settings:
Debian:
apache2: {web_group: www-data, web_owner: www-data, web_root: /var/www/html, webserver_handler: apache2}
lighttpd: {web_group: www-data, web_owner: www-data, web_root: /var/www, webserver_handler: lighttpd}
nginx: {web_group: www-data, web_owner: www-data, web_root: /usr/share/nginx/html, webserver_handler: nginx}
RedHat:
apache2: {web_group: apache, web_owner: apache, web_root: /var/www/html, webserver_handler: httpd}
lighttpd: {web_group: lighttpd, web_owner: lighttpd, web_root: /srv/www, webserver_handler: lighttpd}
nginx: {web_group: nginx, web_owner: nginx, web_root: /usr/share/nginx/html, webserver_handler: nginx}
Then the entire set_facts.yml task file becomes one task:
---
- name: Resolve webserver settings for this OS and type
ansible.builtin.set_fact:
cacti_web_group: "{{ _s.web_group }}"
cacti_web_owner: "{{ _s.web_owner }}"
cacti_web_root: "{{ _s.web_root }}"
cacti_webserver_handler: "{{ _s.webserver_handler }}"
vars:
_s: "{{ cacti_webserver_settings[ansible_os_family][cacti_webserver_type] }}"
The user still sets one variable to choose the webserver:
cacti_webserver_type: nginx # apache2 | lighttpd | nginx
And the lookup resolves the rest. On a Debian host with nginx selected, the facts come out as:
TASK [Show resolved facts] *****************************************************
ok: [localhost] => {
"msg": "group=www-data owner=www-data root=/usr/share/nginx/html handler=nginx"
}
Six tasks became one task and a block of data. Adding a webserver is now a new key in the dictionary, not another task.
Using The Resolved Facts
From here the resolved facts are used like any other variable. A task that sets permissions on the web root:
- name: config_cacti | setting site permissions
ansible.builtin.file:
path: "{{ cacti_web_root }}/cacti-{{ cacti_version }}"
state: directory
recurse: true
owner: "{{ cacti_web_owner }}"
group: "{{ cacti_web_group }}"
And a task that needs to notify the right service handler:
- name: debian | installing php
ansible.builtin.apt:
name: php
state: present
notify:
- "restart {{ cacti_webserver_handler }}"
Wiring It Into The Role
The role’s main task file runs the resolver first, with include_tasks (the bare include from 2016 is deprecated):
---
- ansible.builtin.include_tasks: set_facts.yml
- ansible.builtin.include_tasks: debian.yml
when: ansible_os_family == "Debian"
- ansible.builtin.include_tasks: redhat.yml
when: ansible_os_family == "RedHat"
- ansible.builtin.include_tasks: users.yml
- ansible.builtin.include_tasks: database.yml
- ansible.builtin.include_tasks: config_cacti.yml
- ansible.builtin.include_tasks: templates.yml
when: cacti_import_templates
An example playbook applying the role with Apache as the back end:
---
- hosts: all
become: true
vars:
cacti_webserver_type: apache2
pri_domain_name: vagrant.local
roles:
- role: ansible-apache2
when: cacti_webserver_type == "apache2"
- role: ansible-lighttpd
when: cacti_webserver_type == "lighttpd"
- role: ansible-nginx
when: cacti_webserver_type == "nginx"
- role: ansible-mariadb-mysql
- role: ansible-cacti
Conclusion
set_fact is still the right tool for computing derived variables at run time. The shift since 2016 is to feed it from a data structure instead of writing one conditional task per case. See the set_fact module docs for the current reference.
Enjoy!
Comments