Variables & Facts in Ansible
Understanding Variables
Variables are fundamental to Ansible automation, enabling dynamic content, reusable playbooks, and flexible configurations across diverse environments. Proper variable management is crucial for maintainable automation.
Key Concepts:
- Simple Variables: Store single values
- Lists: Store multiple ordered values
- Dictionaries: Store key-value pairs
- Facts: Automatically discovered system information
Variable Types
1. Simple Variables
# Definition
remote_install_path: /opt/my_app
app_version: "1.2.3"
enable_feature: true
max_connections: 100
# Usage
- name: Deploy application
copy:
src: app.jar
dest: "{{ remote_install_path }}/app.jar"
- name: Set version
debug:
msg: "Deploying version {{ app_version }}"
YAML Quoting Rule:
If a value starts with
{{, you must quote the entire expression: dest: "{{ path }}/file"
2. List Variables
# Definition
packages:
- nginx
- postgresql
- redis
regions:
- northeast
- southeast
- midwest
# Access by index (0-based)
- debug:
msg: "First region: {{ regions[0] }}"
# Loop through list
- name: Install packages
apt:
name: "{{ item }}"
loop: "{{ packages }}"
3. Dictionary Variables
# Definition
database:
host: db.example.com
port: 5432
name: myapp
user: dbadmin
server_config:
max_clients: 100
timeout: 30
ssl_enabled: true
# Access with bracket notation
- debug:
msg: "DB host is {{ database['host'] }}"
# Access with dot notation
- debug:
msg: "DB host is {{ database.host }}"
# Use in tasks
- name: Configure database connection
template:
src: db.conf.j2
dest: /etc/app/db.conf
vars:
db_host: "{{ database.host }}"
db_port: "{{ database.port }}"
Pro Tip:
Use bracket notation when keys match Python methods (
add, keys, values) or start/end with double underscores.
Variable Naming Rules
# Valid variable names
app_name: myapp
server_1: web01
my_variable_123: value
# Invalid variable names (will cause errors)
123_server: invalid # Cannot start with number
app-name: invalid # Cannot use hyphens
my.var: invalid # Cannot use periods
my var: invalid # Cannot use spaces
async: invalid # Python keyword
environment: invalid # Playbook keyword
# Variable names are case-sensitive
MyVar != myvar != MYVAR
Registering Variables
Capture task output to use in subsequent tasks:
# Basic registration
- name: Check if service exists
shell: systemctl status myapp
register: service_status
ignore_errors: yes
- name: Show service status
debug:
var: service_status
# Using registered variables in conditionals
- name: Install service if not present
apt:
name: myapp
state: present
when: service_status.rc != 0
# Accessing registered variable fields
- debug:
msg: |
Return code: {{ service_status.rc }}
Output: {{ service_status.stdout }}
Error: {{ service_status.stderr }}
Changed: {{ service_status.changed }}
# Registration with loops
- name: Check multiple services
systemd:
name: "{{ item }}"
register: service_results
loop:
- nginx
- postgresql
- redis
- name: Show all results
debug:
msg: "{{ item.name }} is {{ item.state }}"
loop: "{{ service_results.results }}"
Variable Precedence
Variables in Ansible follow a strict precedence order (lowest to highest priority):
- Command-line values (e.g.,
-u my_user) - Role defaults (
roles/*/defaults/main.yml) - Inventory file or script group vars
- Inventory
group_vars/all - Playbook
group_vars/all - Inventory
group_vars/* - Playbook
group_vars/* - Inventory file or script host vars
- Inventory
host_vars/* - Playbook
host_vars/* - Host facts / cached
set_facts - Play vars
- Play
vars_prompt - Play
vars_files - Role vars (
roles/*/vars/main.yml) - Block vars (only for tasks in block)
- Task vars (only for the task)
include_varsset_facts/ registered vars- Role (and include_role) params
- Include params
- Extra vars (
-ein CLI) - Always wins!
Precedence Example
# group_vars/all.yml
app_port: 8080
# group_vars/production.yml
app_port: 80
# host_vars/web01.yml
app_port: 443
# playbook.yml
- hosts: web01
vars:
app_port: 9000
tasks:
- debug:
msg: "Port is {{ app_port }}"
# If web01 is in production group:
# Output: "Port is 9000" (play vars beats group_vars)
# Command line (beats everything)
ansible-playbook playbook.yml -e "app_port=5000"
# Output: "Port is 5000"
Variable Scoping
Global Scope
Set via configuration, environment variables, and command-line arguments.
# ansible.cfg
[defaults]
forks = 20
# Environment variable
export ANSIBLE_FORKS=30
# Command line
ansible-playbook playbook.yml --forks 40
Play Scope
Variables defined in plays, roles, and includes:
- name: Play with scoped variables
hosts: webservers
vars:
play_var: value
roles:
- role: webserver
role_var: value
tasks:
- name: Can access play_var and role_var
debug:
msg: "{{ play_var }} {{ role_var }}"
Host Scope
Variables associated with specific hosts:
# inventory/hosts
[webservers]
web01 ansible_host=192.168.1.10 server_id=1
web02 ansible_host=192.168.1.11 server_id=2
# host_vars/web01.yml
datacenter: us-east
environment: production
Where to Define Variables
Best Practices by Location
# 1. Role Defaults - Easily overridden defaults
# roles/webserver/defaults/main.yml
nginx_port: 80
nginx_worker_processes: auto
# 2. Group Variables - Shared across groups
# group_vars/production/all.yml
environment: production
log_level: warning
enable_monitoring: true
# 3. Host Variables - Host-specific overrides
# host_vars/web01.yml
server_id: 1
backup_enabled: true
# 4. Play Variables - Scoped to specific plays
- hosts: webservers
vars:
deployment_version: "{{ lookup('env', 'VERSION') }}"
# 5. Role Variables - Cannot be easily overridden
# roles/webserver/vars/main.yml
nginx_config_dir: /etc/nginx
nginx_log_dir: /var/log/nginx
# 6. Extra Variables - Runtime overrides
ansible-playbook deploy.yml -e "version=1.2.3"
Facts
Facts are system information automatically gathered by Ansible from managed nodes:
Gathering Facts
# Enable fact gathering (default)
- hosts: all
gather_facts: yes
tasks:
- debug:
var: ansible_facts
# Disable fact gathering for speed
- hosts: all
gather_facts: no
tasks:
- name: Manual fact gathering when needed
setup:
# Gather subset of facts
- hosts: all
gather_facts: yes
tasks:
- setup:
gather_subset:
- '!all'
- '!min'
- network
- virtual
# Filter facts
- hosts: all
tasks:
- setup:
filter: ansible_eth*
Common Facts
- name: Display common facts
debug:
msg: |
Hostname: {{ ansible_hostname }}
FQDN: {{ ansible_fqdn }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Architecture: {{ ansible_architecture }}
CPU Cores: {{ ansible_processor_vcpus }}
Memory: {{ ansible_memtotal_mb }} MB
Primary IP: {{ ansible_default_ipv4.address }}
All IPs: {{ ansible_all_ipv4_addresses }}
Python: {{ ansible_python_version }}
Mounts: {{ ansible_mounts }}
Devices: {{ ansible_devices }}
Custom Facts
# /etc/ansible/facts.d/custom.fact (on managed node)
[general]
app_version=1.2.3
environment=production
# Or as JSON
{
"general": {
"app_version": "1.2.3",
"environment": "production"
}
}
# Access in playbook
- debug:
msg: "App version: {{ ansible_local.custom.general.app_version }}"
Setting Facts
# Set fact for current host
- name: Calculate derived values
set_fact:
optimal_workers: "{{ ansible_processor_vcpus * 2 }}"
backup_dir: "/backup/{{ ansible_hostname }}"
- debug:
msg: "Workers: {{ optimal_workers }}"
# Set fact with cacheable option
- name: Cache expensive calculation
set_fact:
expensive_result: "{{ some_complex_calculation }}"
cacheable: yes
# Set fact for another host
- name: Set fact on web01
set_fact:
deployment_complete: true
delegate_to: web01
delegate_facts: true
Magic Variables
Ansible provides special variables automatically:
# Inventory and groups
hostvars # Access other hosts' variables
groups # All inventory groups
group_names # Groups current host belongs to
inventory_hostname # Name of current host
inventory_hostname_short # Short hostname
# Playbook context
ansible_play_hosts # All hosts in current play
ansible_play_batch # Hosts in current batch (with serial)
playbook_dir # Directory containing playbook
role_path # Path to current role
# Example usage
- name: Show inventory info
debug:
msg: |
I am {{ inventory_hostname }}
My groups: {{ group_names }}
All web servers: {{ groups['webservers'] }}
First web server IP: {{ hostvars[groups['webservers'][0]]['ansible_host'] }}
# Loop through all hosts
- name: Configure on all hosts
debug:
msg: "Configuring {{ item }}"
loop: "{{ groups['all'] }}"
# Access another host's variable
- name: Use web01's IP
debug:
msg: "Web01 IP is {{ hostvars['web01']['ansible_default_ipv4']['address'] }}"
Variable Manipulation
Combining Lists
- name: Combine lists
set_fact:
all_packages: "{{ base_packages + app_packages }}"
- name: Unique items only
set_fact:
unique_packages: "{{ (list1 + list2) | unique }}"
Combining Dictionaries
- name: Merge dictionaries
set_fact:
merged_config: "{{ default_config | combine(custom_config) }}"
# Recursive merge
- name: Deep merge
set_fact:
final_config: "{{ base | combine(override, recursive=True) }}"
Default Values
# Provide fallback for undefined variables
app_port: "{{ custom_port | default(8080) }}"
# Omit parameter if not defined
- name: Create user
user:
name: deploy
password: "{{ user_password | default(omit) }}"
# Fail if variable not defined
- name: Required variable
debug:
msg: "{{ required_var | mandatory }}"
Variable Files
Including Variable Files
# Static include (processed at parse time)
- hosts: all
vars_files:
- vars/common.yml
- vars/{{ environment }}.yml
# Dynamic include (processed at runtime)
- name: Load OS-specific variables
include_vars: "{{ ansible_os_family }}.yml"
# Load from directory
- name: Load all YAML files from directory
include_vars:
dir: vars/
extensions:
- yml
- yaml
Prompting for Variables
- hosts: all
vars_prompt:
- name: username
prompt: "Enter username"
private: no
- name: password
prompt: "Enter password"
private: yes
encrypt: sha512_crypt
confirm: yes
tasks:
- debug:
msg: "Creating user {{ username }}"
Best Practices
- Use Descriptive Names:
nginx_portnotport - Namespace Variables: Prefix with role name (
mysql_root_password) - Document Variables: Add comments explaining purpose
- Set Sensible Defaults: Use role defaults for common values
- Avoid Overriding: Don't fight variable precedence
- Keep Secrets Separate: Use vault for sensitive data
- Use Facts Wisely: Disable gathering when not needed
- Validate Input: Check variables are defined and valid
Common Patterns
Environment-Specific Variables
# group_vars/development.yml
environment: development
debug_mode: true
log_level: debug
# group_vars/production.yml
environment: production
debug_mode: false
log_level: warning
# Usage
- name: Configure based on environment
template:
src: app.conf.j2
dest: /etc/app/app.conf
Conditional Variable Setting
- name: Set OS-specific variables
set_fact:
package_manager: "{{ 'apt' if ansible_os_family == 'Debian' else 'yum' }}"
service_name: "{{ 'apache2' if ansible_os_family == 'Debian' else 'httpd' }}"
Quick Reference
# Define variables
vars:
key: value
# Access variables
"{{ variable }}"
# Register output
register: result
# Set facts
set_fact:
my_fact: value
# Default values
"{{ var | default('fallback') }}"
# Mandatory variables
"{{ var | mandatory }}"
# Access nested data
"{{ dict.key }}"
"{{ dict['key'] }}"
"{{ list[0] }}"
# Magic variables
{{ inventory_hostname }}
{{ groups['groupname'] }}
{{ hostvars['hostname']['var'] }}
# Extra vars (CLI)
-e "var=value"
-e "@vars.yml"
Next Steps
- Learn about Conditionals & Loops to use variables in control flow
- Explore Jinja2 Templating for variable manipulation
- Master Ansible Vault for securing sensitive variables
- Try the Playground to experiment with variables