Ansible set_fact vs vars vs extra_vars: Variable Types Compared
By Luca Berton · Published 2024-01-01 · Category: web-servers
Complete comparison of Ansible set_fact, vars, extra_vars, group_vars, host_vars, role defaults, and registered variables.
Ansible has at least 22 places to define variables. Understanding which type to use where prevents scope bugs, precedence conflicts, and maintainability nightmares. This guide compares the most common types with clear rules.
Variable Types at a Glance
| Type | Where Defined | Scope | Precedence | Persists Across Plays |
|------|-------------|-------|------------|----------------------|
| extra_vars (-e) | Command line | Global | Highest (22) | Yes |
| set_fact | Task | Per-host | 19 | Yes (via hostvars) |
| registered | Task (register:) | Per-host | 19 | Yes (via hostvars) |
| task vars | Task vars: | Single task | 18 | No |
| block vars | Block vars: | Block scope | 18 | No |
| play vars | Play vars: | Play scope | 14 | No |
| play vars_files | Play vars_files: | Play scope | 14 | No |
| host_vars | host_vars/hostname/ | Per-host | 10 | Yes |
| group_vars | group_vars/groupname/ | Per-group | 7-8 | Yes |
| role defaults | roles/x/defaults/ | Role scope | Lowest (1) | No |
See also: Ansible set_fact: Create & Set Runtime Variables (Complete Guide)
extra_vars: The Override
-e / --extra-vars has the highest precedence. Nothing overrides it.
# From command line
ansible-playbook site.yml -e "version=2.5.0 env=production"
# From JSON file
ansible-playbook site.yml -e @deploy-vars.json
# From YAML file
ansible-playbook site.yml -e @secrets.yml
# In playbook — extra_vars ALWAYS wins
- hosts: all
vars:
version: "1.0.0" # This is ignored if -e version=2.5.0 is passed
tasks:
- ansible.builtin.debug:
msg: "Deploying {{ version }}"
# Output: "Deploying 2.5.0" (extra_vars wins)
When to Use extra_vars
• Runtime parameters that change per execution (version, environment, ticket ID) • CI/CD pipeline variables • Emergency overrides • One-time operationsWhen NOT to Use
• Defaults (use role defaults instead) • Permanent config (use group_vars/host_vars) • Anything you'd forget to pass next timeset_fact: Runtime Variables
set_fact creates host-scoped variables during playbook execution.
- name: Calculate deployment path
ansible.builtin.set_fact:
deploy_path: "/opt/{{ app_name }}/releases/{{ version }}"
deploy_timestamp: "{{ ansible_date_time.iso8601 }}"
- name: Use calculated value
ansible.builtin.debug:
msg: "Deploying to {{ deploy_path }}"
set_fact Scope
# set_fact is per-host and persists across plays
- name: Play 1
hosts: webservers
tasks:
- ansible.builtin.set_fact:
my_fact: "from play 1"
- name: Play 2
hosts: webservers
tasks:
- ansible.builtin.debug:
msg: "{{ my_fact }}" # ✅ Works — "from play 1"
- name: Play 3
hosts: dbservers
tasks:
- ansible.builtin.debug:
msg: "{{ my_fact }}" # ❌ UNDEFINED — different host group
# To access: hostvars[groups['webservers'][0]].my_fact
When to Use set_fact
• Computed values from task output • Transforming registered variable data • Values that depend on runtime conditions • Cross-play variable sharing (via hostvars)set_fact vs register
# register — captures task output
- ansible.builtin.command: hostname -f
register: hostname_result
# hostname_result.stdout, .rc, .stderr, etc.
# set_fact — creates a clean variable
- ansible.builtin.set_fact:
fqdn: "{{ hostname_result.stdout }}"
# fqdn is just a string — cleaner to use downstream
See also: Ansible set_fact Module: Set Variables Dynamically (Complete Guide 2026)
vars: Play/Task-Level Variables
# Play-level vars — available to all tasks in this play
- hosts: webservers
vars:
http_port: 80
max_connections: 1000
tasks:
- ansible.builtin.debug:
msg: "Port: {{ http_port }}"
# Task-level vars — only for this task
- name: Deploy with specific settings
ansible.builtin.template:
src: config.j2
dest: /etc/app.conf
vars:
log_level: debug # Only available in this task
When to Use Play vars
• Values that apply to a specific play • Overriding role defaults for a specific playbook • Grouping related configurationgroup_vars and host_vars: Inventory Variables
inventory/
├── group_vars/
│ ├── all.yml # All hosts
│ ├── webservers.yml # Web server group
│ └── production.yml # Production environment
├── host_vars/
│ ├── web01.yml # Specific host
│ └── db01.yml
└── hosts.yml
# group_vars/webservers.yml
http_port: 80
nginx_worker_connections: 1024
ssl_enabled: true
# group_vars/production.yml
log_level: warn
backup_enabled: true
# host_vars/web01.yml
nginx_worker_connections: 2048 # Override for this specific host
When to Use group_vars / host_vars
• Infrastructure-specific configuration • Environment differences (dev/staging/production) • Host-specific overrides • Anything that should be version-controlled with inventorySee also: ansible_date_time: Access Date, Time & Timestamp Facts in Ansible
Role Defaults: The Base Layer
# roles/nginx/defaults/main.yml
nginx_port: 80
nginx_worker_connections: 1024
nginx_log_level: info
nginx_ssl_enabled: false
Role defaults have the lowest precedence — they're overridable by everything else.
When to Use Role Defaults
• Sensible defaults that work out of the box • Values users commonly customize • Documentation of all available role parametersPractical Decision Guide
Where should I put this variable?
Is it a runtime parameter (version, ticket ID)?
└── YES → extra_vars (-e)
Is it computed from task output?
└── YES → set_fact (or register + set_fact)
Does it differ between environments?
└── YES → group_vars (group_vars/production.yml vs group_vars/staging.yml)
Does it differ between individual hosts?
└── YES → host_vars (host_vars/web01.yml)
Is it a role configuration option?
└── YES → role defaults (roles/x/defaults/main.yml)
Is it only needed in one playbook?
└── YES → play vars
Is it only needed in one task?
└── YES → task vars
Common Mistakes
Mistake 1: Using set_fact for static values
# ❌ BAD — set_fact for a constant
- ansible.builtin.set_fact:
app_name: "myapp"
app_port: 8080
# ✅ GOOD — use vars or group_vars
- hosts: all
vars:
app_name: "myapp"
app_port: 8080
Mistake 2: Expecting extra_vars to be optional
# ❌ DANGEROUS — extra_vars override EVERYTHING
# If someone passes -e http_port=443, your playbook changes behavior
- hosts: webservers
vars:
http_port: 80
Mistake 3: Variable precedence confusion
# What value does http_port have?
# roles/nginx/defaults/main.yml → http_port: 80
# group_vars/webservers.yml → http_port: 8080
# play vars → http_port: 443
# Answer: 443 (play vars > group_vars > role defaults)
# With -e http_port=9090: 9090 (extra_vars > everything)
FAQ
What has the highest variable precedence in Ansible?
extra_vars (-e on the command line) have the absolute highest precedence (level 22). They override everything else, including task vars, set_fact, and host_vars. This is by design — they're the emergency override.
When should I use set_fact vs register?
Use register to capture the raw output of a task (stdout, stderr, rc, etc.). Use set_fact to create clean, named variables — often derived from registered output. Think of register as raw data capture and set_fact as data transformation.
Do play vars carry over to the next play?
No. Play-level vars: are scoped to that play only. To share variables across plays, use set_fact (persists in hostvars), group_vars/host_vars (inventory-level), or extra_vars (global).
How do I see all variables for a host?
Run ansible hostname -m debug -a "var=hostvars[inventory_hostname]" to dump all variables. In a playbook, use - debug: var=vars to see variables available to the current task.
Conclusion
extra_vars for runtime parameters. set_fact for computed values. group_vars/host_vars for infrastructure config. Role defaults for customizable base values. Play/task vars for playbook-specific settings. When in doubt, check the 22-level precedence order — the right choice prevents subtle bugs.
Related Articles
• Ansible Variable Precedence Guide • Ansible register: Save Task Output • Ansible vars_files Guide • Ansible default() Filter GuideCategory: web-servers