Introduction
Variables make playbooks reusable — define values once, use them everywhere. Ansible variables work in playbooks, templates, inventories, and command line. Understanding where to define them and how precedence works is key to writing clean automation.
Define Variables
In a Playbook
---
- name: Deploy application
hosts: webservers
vars:
app_name: myapp
app_port: 8080
app_env: production
app_user: deploy
tasks:
- name: Show config
ansible.builtin.debug:
msg: "Deploying {{ app_name }} on port {{ app_port }}"
In a Vars File
# vars/production.yml
app_name: myapp
app_port: 8080
app_env: production
db_host: db.internal
db_port: 5432
db_name: myapp_prod
# playbook.yml
---
- hosts: webservers
vars_files:
- vars/production.yml
tasks:
- debug:
msg: "DB: {{ db_host }}:{{ db_port }}/{{ db_name }}"
In Inventory (group_vars / host_vars)
project/
├── inventory/
│ ├── production
│ ├── group_vars/
│ │ ├── all.yml ← All hosts
│ │ ├── webservers.yml ← Web server group
│ │ └── dbservers.yml ← DB server group
│ └── host_vars/
│ ├── web-01.yml ← Specific host
│ └── db-01.yml
# group_vars/all.yml — applies to all hosts
ntp_server: time.google.com
dns_servers:
- 8.8.8.8
- 8.8.4.4
# group_vars/webservers.yml
nginx_worker_processes: 4
app_port: 8080
# host_vars/web-01.yml
nginx_worker_processes: 8 # Override for this specific host
On the Command Line
# Extra vars (highest precedence!)
ansible-playbook site.yml -e "app_version=2.1.0 app_env=staging"
# JSON format
ansible-playbook site.yml -e '{"app_version": "2.1.0", "debug_mode": true}'
# From a file
ansible-playbook site.yml -e @vars/deploy.yml
With set_fact (Runtime)
- name: Set variable at runtime
ansible.builtin.set_fact:
app_url: "https://{{ app_domain }}:{{ app_port }}"
total_memory_gb: "{{ ansible_memtotal_mb / 1024 | round(1) }}"
With register (Task Output)
- name: Get current version
ansible.builtin.command: /opt/app/version
register: version_result
- debug:
msg: "Current version: {{ version_result.stdout }}"
Use Variables
# In task parameters
- ansible.builtin.template:
src: "{{ app_name }}.conf.j2"
dest: "/etc/{{ app_name }}/config.conf"
owner: "{{ app_user }}"
# In conditions
- ansible.builtin.service:
name: nginx
state: started
when: install_nginx | default(true)
# In loops
- ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
loop: "{{ users }}"
# In templates (Jinja2)
# server {
# listen {{ app_port }};
# server_name {{ app_domain }};
# root {{ app_root }};
# }
Variable Precedence (Low to High)
From lowest to highest priority:
- Role defaults (
roles/x/defaults/main.yml) - Inventory file group vars
- Inventory
group_vars/all - Inventory
group_vars/<group> - Inventory file host vars
- Inventory
host_vars/<host> - Play
vars - Play
vars_files - Play
vars_prompt - Task
vars set_fact/register- Role
vars(roles/x/vars/main.yml) - Block
vars - Include params
- Extra vars (
-e) ← ALWAYS WINS
In practice, remember three levels:
- Defaults (role defaults) — easily overridden
- Standard (group_vars, host_vars, play vars) — normal usage
- Force (extra vars
-e) — always wins, use for CLI overrides
Data Types
# String
app_name: myapp
app_name: "my app" # Quotes for special chars
# Number
app_port: 8080
timeout: 30.5
# Boolean
debug_mode: true
ssl_enabled: false
# List
packages:
- nginx
- redis
- postgresql
# Or inline: packages: [nginx, redis, postgresql]
# Dictionary
database:
host: db.internal
port: 5432
name: myapp
credentials:
user: admin
password: "{{ vault_db_password }}"
# Access dict values
# {{ database.host }}
# {{ database['port'] }}
# {{ database.credentials.user }}
Default Values
# Use default filter to prevent undefined errors
- debug:
msg: "Port: {{ app_port | default(8080) }}"
# Default for boolean
- service:
name: nginx
state: started
when: install_nginx | default(true)
# Default empty list
- user:
name: "{{ item }}"
loop: "{{ extra_users | default([]) }}"
# Default empty string
- debug:
msg: "Prefix: {{ prefix | default('') }}"
Common Filters
# Type conversion
port: "{{ input_port | int }}"
enabled: "{{ input_flag | bool }}"
items: "{{ input_csv | split(',') }}"
# String manipulation
lower: "{{ name | lower }}"
upper: "{{ name | upper }}"
replaced: "{{ path | replace('old', 'new') }}"
trimmed: "{{ input | trim }}"
# List operations
first: "{{ servers | first }}"
last: "{{ servers | last }}"
count: "{{ servers | length }}"
joined: "{{ servers | join(',') }}"
unique: "{{ tags | unique }}"
sorted: "{{ names | sort }}"
# Dict operations
keys: "{{ config | dict2items }}"
merged: "{{ defaults | combine(overrides) }}"
# Math
rounded: "{{ value | round(2) }}"
absolute: "{{ number | abs }}"
max_val: "{{ [10, 20, 5] | max }}"
Practical Patterns
Environment-Specific Config
group_vars/
├── all.yml # Shared defaults
├── production.yml # Production overrides
└── staging.yml # Staging overrides
# group_vars/all.yml
app_port: 8080
log_level: info
replicas: 1
# group_vars/production.yml
log_level: warn
replicas: 3
ssl_enabled: true
# group_vars/staging.yml
log_level: debug
replicas: 1
ssl_enabled: false
Conditional Variables
vars:
# OS-specific package names
nginx_package: "{{ 'nginx' if ansible_os_family == 'Debian' else 'nginx' }}"
php_package: "{{ 'php-fpm' if ansible_os_family == 'Debian' else 'php-fpm' }}"
# Environment-based settings
worker_count: "{{ ansible_processor_vcpus * 2 }}"
cache_size: "{{ (ansible_memtotal_mb * 0.25) | int }}m"
Vault-Encrypted Variables
# group_vars/production/vault.yml (encrypted)
vault_db_password: supersecret
vault_api_key: abc123
# group_vars/production/vars.yml (references vault)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
# Encrypt
ansible-vault encrypt group_vars/production/vault.yml
# Run with vault
ansible-playbook site.yml --ask-vault-pass
Troubleshooting
"undefined variable"
# Check if defined before using
- debug:
msg: "{{ my_var }}"
when: my_var is defined
# Or use default
- debug:
msg: "{{ my_var | default('fallback') }}"
Variable Not Overriding
Check precedence. extra_vars (-e) always wins. Role defaults are easily overridden. If a variable seems stuck, check if it's set at a higher precedence level:
# Debug: show where a variable comes from
ansible -m debug -a "var=hostvars[inventory_hostname]" web-01
String vs Number
# YAML auto-types: 8080 is an int, "8080" is a string
port: 8080 # int
port: "8080" # string
# Force type with filters
- debug:
msg: "{{ port | int + 1 }}"
Related Articles
- Ansible Vault for Secrets
- Ansible Inventory Guide
- Ansible set_fact Module
- Ansible register Variables
- Ansible Roles Guide
- Ansible Facts
Conclusion
Variables are defined in playbooks (vars), files (vars_files), inventory (group_vars/host_vars), runtime (set_fact/register), or CLI (-e). Extra vars (-e) always win. Use group_vars/ for environment-specific config, host_vars/ for host-specific overrides, and role defaults for sensible fallbacks. Always provide | default() for optional variables to prevent undefined errors. Encrypt sensitive values with Ansible Vault.