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 |
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 operations
When NOT to Use
- Defaults (use role defaults instead)
- Permanent config (use group_vars/host_vars)
- Anything you'd forget to pass next time
set_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_factWhen 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 downstreamSee also: Ansible set_fact: Create & Set Runtime Variables (Complete Guide)
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 taskWhen to Use Play vars
- Values that apply to a specific play
- Overriding role defaults for a specific playbook
- Grouping related configuration
group_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 hostWhen to Use group_vars / host_vars
- Infrastructure-specific configuration
- Environment differences (dev/staging/production)
- Host-specific overrides
- Anything that should be version-controlled with inventory
Role Defaults: The Base Layer
# roles/nginx/defaults/main.yml
nginx_port: 80
nginx_worker_connections: 1024
nginx_log_level: info
nginx_ssl_enabled: falseRole 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 parameters
Practical 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 varsCommon 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: 8080Mistake 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: 80Mistake 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)See also: Ansible set_fact Module: Set Variables Dynamically (Complete Guide 2026)
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.
See also: ansible_date_time: Access Date, Time & Timestamp Facts in Ansible
Related Articles
Category: web-servers