Variables are the backbone of flexible, reusable Ansible playbooks. Understanding variable types, scoping, and precedence is crucial for creating maintainable automation. This comprehensive guide covers everything you need to know about Ansible variables.
📑 Table of Contents
- 1. Variable Basics and Syntax
- 2. Variable Types and Sources
- Command Line Variables (-e/–extra-vars)
- Playbook Variables
- Inventory Variables
- Host Variables and Group Variables Files
- Role Variables
- 3. Variable Precedence Order
- Complete Precedence Order (Lowest to Highest)
- Precedence in Practice
- 4. Inventory Variables
- 5. Playbook and Task Variables
- 6. Dynamic Variables with set_fact
- 7. Registered Variables
- 8. Magic Variables
- 9. Environment Variables
- 10. Lookup Plugins
- 11. Ansible Vault for Sensitive Data
- 12. Nested Variables and Complex Data
- 13. Variable Filters and Transformations
- 14. Best Practices
- 1. Naming Conventions
- 2. Variable Organization
- 3. Documentation
- 4. Security
- 5. Testing and Validation
- 6. Performance
- Conclusion
1. Variable Basics and Syntax
Ansible variables use Jinja2 templating syntax and can be defined in multiple locations.
---
- name: Variable Basics
hosts: localhost
vars:
# Simple variables
app_name: myapp
app_port: 8080
enable_ssl: true
# Lists
packages:
- nginx
- mysql
- redis
# Dictionaries
database:
host: db.example.com
port: 3306
name: production_db
# Multi-line strings
welcome_message: |
Welcome to our application!
This is a multi-line message.
tasks:
# Variable usage with {{ }}
- name: Print simple variable
debug:
msg: "Application name is {{ app_name }}"
# Accessing list items
- name: Print first package
debug:
msg: "First package: {{ packages[0] }}"
# Accessing dictionary values - dot notation
- name: Print database host
debug:
msg: "Database: {{ database.host }}"
# Accessing dictionary values - bracket notation
- name: Print database port
debug:
msg: "Port: {{ database['port'] }}"
# Variables in task parameters (no quotes needed)
- name: Create directory
file:
path: "/opt/{{ app_name }}"
state: directory
# Full line variables need quotes
- name: Debug with full line variable
debug:
msg: "{{ welcome_message }}"
2. Variable Types and Sources
Variables can come from many sources, each with different use cases.
Command Line Variables (-e/–extra-vars)
# Highest precedence - always wins
ansible-playbook site.yml -e "environment=production"
ansible-playbook site.yml -e '{"environment":"production","debug":true}'
ansible-playbook site.yml -e "@vars.json"
ansible-playbook site.yml -e "@vars.yml"
Playbook Variables
---
- name: Playbook with variables
hosts: webservers
vars:
http_port: 80
https_port: 443
vars_files:
- vars/common.yml
- vars/{{ ansible_os_family }}.yml
vars_prompt:
- name: db_password
prompt: "Enter database password"
private: yes
tasks:
- name: Use variables
debug:
msg: "HTTP: {{ http_port }}, HTTPS: {{ https_port }}"
Inventory Variables
# INI format inventory
[webservers]
web1.example.com http_port=8080 ansible_user=deploy
web2.example.com http_port=8081 ansible_user=deploy
[webservers:vars]
http_port=80
app_name=mywebapp
# YAML format inventory
all:
children:
webservers:
hosts:
web1.example.com:
http_port: 8080
web2.example.com:
http_port: 8081
vars:
app_name: mywebapp
Host Variables and Group Variables Files
# Directory structure
inventory/
hosts
group_vars/
all.yml # Variables for all hosts
webservers.yml # Variables for webservers group
dbservers.yml # Variables for dbservers group
host_vars/
web1.example.com.yml # Variables for specific host
db1.example.com.yml # Variables for specific host
# group_vars/all.yml
---
ntp_server: time.example.com
dns_servers:
- 8.8.8.8
- 8.8.4.4
# group_vars/webservers.yml
---
http_port: 80
https_port: 443
max_clients: 200
# host_vars/web1.example.com.yml
---
max_clients: 500 # Override for this specific host
server_role: primary
Role Variables
# roles/nginx/defaults/main.yml (lowest role precedence)
---
nginx_port: 80
nginx_worker_processes: auto
nginx_worker_connections: 1024
# roles/nginx/vars/main.yml (higher role precedence)
---
nginx_config_path: /etc/nginx/nginx.conf
nginx_service_name: nginx
# Using role variables in tasks
# roles/nginx/tasks/main.yml
---
- name: Install nginx
package:
name: nginx
state: present
- name: Configure nginx
template:
src: nginx.conf.j2
dest: "{{ nginx_config_path }}"
notify: Reload nginx
3. Variable Precedence Order
Understanding precedence is crucial when the same variable is defined in multiple places. Variables with higher precedence override those with lower precedence.
Complete Precedence Order (Lowest to Highest)
- command line values (for example, -u my_user, these are not variables)
- role defaults (defined in role/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 (defined in role/vars/main.yml)
- block vars (only for tasks in block)
- task vars (only for the task)
- include_vars
- set_facts / registered vars
- role (and include_role) params
- include params
- extra vars (-e in command line) always win
Precedence in Practice
---
# Demonstrating precedence
- name: Variable Precedence Demo
hosts: localhost
vars:
my_var: "from play vars"
vars_files:
- precedence_vars.yml # contains: my_var: "from vars_files"
tasks:
- name: Show variable from play
debug:
msg: "{{ my_var }}" # Shows: from vars_files (higher precedence)
- name: Override with task vars
debug:
msg: "{{ my_var }}"
vars:
my_var: "from task vars" # Shows: from task vars
- name: Override with set_fact
set_fact:
my_var: "from set_fact"
- name: Show set_fact result
debug:
msg: "{{ my_var }}" # Shows: from set_fact
# Run with: ansible-playbook test.yml -e "my_var='from extra vars'"
# Result: Always shows "from extra vars" (highest precedence)
4. Inventory Variables
Organize variables by host and group for scalable infrastructure management.
# Complex inventory with variables
# inventory/production
[webservers]
web[01:03].prod.example.com
[appservers]
app[01:05].prod.example.com
[dbservers]
db01.prod.example.com mysql_role=master
db02.prod.example.com mysql_role=slave
db03.prod.example.com mysql_role=slave
[loadbalancers]
lb01.prod.example.com lb_weight=100
lb02.prod.example.com lb_weight=100
[production:children]
webservers
appservers
dbservers
loadbalancers
[production:vars]
environment=production
monitoring_enabled=true
backup_enabled=true
ansible_user=ansible
ansible_ssh_private_key_file=~/.ssh/production.pem
---
# group_vars/production.yml
---
# Environment settings
environment: production
datacenter: us-east-1
# Monitoring
monitoring:
enabled: true
alerting: true
metrics_retention_days: 90
# Backup configuration
backup:
enabled: true
schedule: "0 2 * * *"
retention_days: 30
destination: s3://backups-prod
# Security settings
security:
firewall_enabled: true
ssl_required: true
allowed_ips:
- 10.0.0.0/8
- 172.16.0.0/12
---
# group_vars/webservers.yml
---
nginx:
version: 1.20.1
worker_processes: auto
worker_connections: 2048
keepalive_timeout: 65
php:
version: 8.1
memory_limit: 256M
max_execution_time: 60
upload_max_filesize: 20M
application:
name: webapp
document_root: /var/www/html
log_level: warning
---
# host_vars/db01.prod.example.com.yml
---
mysql:
role: master
server_id: 1
binlog_format: ROW
innodb_buffer_pool_size: 8G
max_connections: 500
# Master-specific settings
replication:
enabled: true
binlog_do_db:
- production_db
- analytics_db
# Usage in playbook
---
- name: Configure MySQL
hosts: dbservers
tasks:
- name: Configure MySQL replication
template:
src: mysql.cnf.j2
dest: /etc/mysql/mysql.conf.d/replication.cnf
when: mysql.replication.enabled | default(false)
5. Playbook and Task Variables
Define variables at play and task level for specific use cases.
---
- name: Playbook and Task Variables
hosts: all
vars:
global_var: "I am global to this play"
vars_files:
- vars/common.yml
- "vars/{{ ansible_distribution }}.yml"
tasks:
- name: Task with its own variables
debug:
msg: "Task var: {{ task_var }}, Global: {{ global_var }}"
vars:
task_var: "I only exist in this task"
- name: Include variables dynamically
include_vars:
file: "vars/{{ environment }}.yml"
name: env_vars
- name: Use included variables
debug:
msg: "Environment config: {{ env_vars }}"
- name: Block with block-level variables
block:
- name: Task in block
debug:
msg: "Block var: {{ block_var }}"
- name: Another task in block
debug:
msg: "Also sees block var: {{ block_var }}"
vars:
block_var: "I exist for all tasks in this block"
- name: Loop with loop variables
debug:
msg: "Item: {{ item.name }}, Value: {{ item.value }}"
loop:
- { name: "var1", value: 100 }
- { name: "var2", value: 200 }
loop_control:
loop_var: item
- name: Import tasks with variables
import_tasks: subtasks.yml
vars:
import_var: "Available in imported tasks"
- name: Include tasks with variables
include_tasks: subtasks.yml
vars:
include_var: "Available in included tasks"
6. Dynamic Variables with set_fact
Create and modify variables during playbook execution.
---
- name: Dynamic Variables with set_fact
hosts: localhost
tasks:
# Simple set_fact
- name: Set a simple fact
set_fact:
deployment_time: "{{ ansible_date_time.iso8601 }}"
# Set multiple facts
- name: Set multiple facts
set_fact:
app_version: "2.5.1"
app_name: "myapp"
app_env: "production"
# Computed fact based on other variables
- name: Set computed fact
set_fact:
app_full_name: "{{ app_name }}-{{ app_version }}-{{ app_env }}"
# Conditional fact
- name: Determine server type
set_fact:
server_type: "{{ 'high_memory' if ansible_memory_mb.real.total > 16000 else 'standard' }}"
# Building lists dynamically
- name: Initialize empty list
set_fact:
installed_packages: []
- name: Add to list
set_fact:
installed_packages: "{{ installed_packages + [item] }}"
loop:
- nginx
- mysql
- redis
# Building dictionaries dynamically
- name: Initialize empty dict
set_fact:
server_config: {}
- name: Build configuration dict
set_fact:
server_config: "{{ server_config | combine({item.key: item.value}) }}"
loop:
- { key: 'port', value: 8080 }
- { key: 'workers', value: 4 }
- { key: 'timeout', value: 30 }
# Using set_fact with filters
- name: Transform data
set_fact:
hostname_short: "{{ inventory_hostname | regex_replace('\..*$', '') }}"
hostname_upper: "{{ inventory_hostname | upper }}"
# Cache fact across plays
- name: Set cacheable fact
set_fact:
deployment_id: "{{ ansible_date_time.epoch }}"
cacheable: yes
# Complex computed fact
- name: Calculate deployment strategy
set_fact:
deployment_strategy: |
{% if environment == 'production' %}
{% if ansible_date_time.weekday in ['5', '6'] %}
manual_approval
{% else %}
rolling_update
{% endif %}
{% else %}
blue_green
{% endif %}
vars:
environment: production
- name: Show all computed facts
debug:
msg:
- "Deployment time: {{ deployment_time }}"
- "App: {{ app_full_name }}"
- "Server type: {{ server_type }}"
- "Packages: {{ installed_packages }}"
- "Config: {{ server_config }}"
- "Strategy: {{ deployment_strategy | trim }}"
7. Registered Variables
Capture task output for use in subsequent tasks.
---
- name: Registered Variables Examples
hosts: localhost
tasks:
# Basic registration
- name: Run command
command: hostname
register: hostname_output
- name: Show registered variable
debug:
var: hostname_output
# Common registered variable attributes
- name: Check service status
command: systemctl is-active nginx
register: service_status
failed_when: false
changed_when: false
- name: Use registered variable attributes
debug:
msg:
- "Return code: {{ service_status.rc }}"
- "Stdout: {{ service_status.stdout }}"
- "Stderr: {{ service_status.stderr }}"
- "Changed: {{ service_status.changed }}"
- "Failed: {{ service_status.failed }}"
# Register with loops
- name: Check multiple services
command: "systemctl is-active {{ item }}"
register: services_status
failed_when: false
changed_when: false
loop:
- nginx
- mysql
- redis
- name: Show results from loop
debug:
msg: "{{ item.item }}: {{ 'running' if item.rc == 0 else 'stopped' }}"
loop: "{{ services_status.results }}"
# Register with stat module
- name: Check if file exists
stat:
path: /etc/app/config.conf
register: config_file
- name: Use stat results
debug:
msg:
- "Exists: {{ config_file.stat.exists }}"
- "Size: {{ config_file.stat.size | default(0) }}"
- "Mode: {{ config_file.stat.mode | default('unknown') }}"
when: config_file.stat.exists
# Register with URI module
- name: Check API endpoint
uri:
url: http://localhost:8080/api/health
return_content: yes
register: api_response
failed_when: false
- name: Parse API response
debug:
msg:
- "Status: {{ api_response.status }}"
- "Content: {{ api_response.content }}"
- "JSON: {{ api_response.json }}"
when: api_response.status == 200
# Register with shell module for complex parsing
- name: Get disk usage
shell: df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
register: disk_usage
changed_when: false
- name: Alert if disk usage high
debug:
msg: "WARNING: Disk usage at {{ disk_usage.stdout }}%"
when: disk_usage.stdout | int > 80
# Using until with registered variables
- name: Wait for service to be healthy
uri:
url: http://localhost:8080/health
status_code: 200
register: health_check
until: health_check.status == 200
retries: 10
delay: 5
failed_when: false
# Complex data extraction
- name: Get system information
command: lscpu
register: cpu_info
changed_when: false
- name: Extract CPU count
set_fact:
cpu_count: "{{ cpu_info.stdout | regex_search('CPU\(s\):\s+(\d+)', '\1') | first }}"
- name: Show extracted info
debug:
msg: "System has {{ cpu_count }} CPUs"
8. Magic Variables
Ansible provides special variables with information about the playbook execution environment.
---
- name: Magic Variables Examples
hosts: all
gather_facts: yes
tasks:
# Inventory and host information
- name: Current host information
debug:
msg:
- "Current host: {{ inventory_hostname }}"
- "Short hostname: {{ inventory_hostname_short }}"
- "All groups: {{ group_names }}"
- "All groups this host belongs to: {{ groups.keys() | list }}"
# Access variables from other hosts
- name: Show all hosts in inventory
debug:
msg: "All hosts: {{ groups['all'] }}"
run_once: true
- name: Access another host's variables
debug:
msg: "Web server IP: {{ hostvars['web1.example.com']['ansible_default_ipv4']['address'] }}"
run_once: true
when: "'web1.example.com' in groups['all']"
# Play and playbook information
- name: Playbook information
debug:
msg:
- "Playbook directory: {{ playbook_dir }}"
- "Role path: {{ role_path | default('N/A') }}"
- "Inventory directory: {{ inventory_dir }}"
- "Inventory file: {{ inventory_file }}"
# Ansible version and environment
- name: Ansible environment
debug:
msg:
- "Ansible version: {{ ansible_version.full }}"
- "Python version: {{ ansible_python_version }}"
- "User running playbook: {{ ansible_user_id }}"
run_once: true
# Loop through all hosts and gather IPs
- name: Build inventory of all IPs
set_fact:
all_host_ips: |
{% for host in groups['all'] %}
{{ host }}: {{ hostvars[host]['ansible_default_ipv4']['address'] | default('N/A') }}
{% endfor %}
run_once: true
- name: Show all IPs
debug:
msg: "{{ all_host_ips }}"
run_once: true
# Using groups and hostvars for configuration
- name: Generate backend server list
set_fact:
backend_servers: |
{% for host in groups['appservers'] %}
server {{ host }} {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ hostvars[host]['app_port'] | default(8080) }};
{% endfor %}
when: "'loadbalancers' in group_names"
# Delegate information
- name: Delegate task
debug:
msg:
- "Delegating from: {{ inventory_hostname }}"
- "Delegating to: {{ ansible_delegated_vars.keys() | list }}"
delegate_to: localhost
# Play hosts information
- name: Play execution info
debug:
msg:
- "All play hosts: {{ ansible_play_hosts }}"
- "Batch play hosts: {{ ansible_play_batch }}"
run_once: true
9. Environment Variables
Use environment variables in playbooks and access system environment variables.
---
- name: Environment Variables
hosts: localhost
environment:
HTTP_PROXY: http://proxy.example.com:8080
HTTPS_PROXY: http://proxy.example.com:8080
NO_PROXY: localhost,127.0.0.1
tasks:
# Task-level environment variables
- name: Run command with environment
command: env
environment:
CUSTOM_VAR: "custom_value"
PATH: "/usr/local/bin:{{ ansible_env.PATH }}"
register: env_output
- name: Access system environment variables
debug:
msg:
- "Home directory: {{ ansible_env.HOME }}"
- "Path: {{ ansible_env.PATH }}"
- "User: {{ ansible_env.USER }}"
- "Shell: {{ ansible_env.SHELL }}"
# Lookup environment variables
- name: Get environment variable with lookup
debug:
msg: "Java Home: {{ lookup('env', 'JAVA_HOME') | default('not set') }}"
# Set environment for entire play
- name: Task uses play-level environment
shell: echo $HTTP_PROXY
register: proxy_output
- name: Show proxy setting
debug:
msg: "Proxy is: {{ proxy_output.stdout }}"
# Environment variables in templates
- name: Create script with environment
copy:
content: |
#!/bin/bash
export APP_ENV="{{ app_environment }}"
export DB_HOST="{{ database_host }}"
export ADMIN_EMAIL="{{ ansible_env.USER }}@example.com"
echo "Starting application..."
/opt/app/start.sh
dest: /tmp/start_app.sh
mode: '0755'
vars:
app_environment: production
database_host: db.example.com
# Proxy configuration pattern
- name: Configure application with proxy
template:
src: app_config.j2
dest: /etc/app/config.yml
environment:
HTTP_PROXY: "{{ proxy_server | default('') }}"
HTTPS_PROXY: "{{ proxy_server | default('') }}"
vars:
proxy_server: http://proxy.example.com:8080
# Database connection with environment
- name: Run database migration
command: /opt/app/migrate.sh
environment:
DATABASE_URL: "postgresql://{{ db_user }}:{{ db_pass }}@{{ db_host }}/{{ db_name }}"
RAILS_ENV: production
vars:
db_user: appuser
db_pass: "{{ vault_db_password }}"
db_host: db.example.com
db_name: production_db
10. Lookup Plugins
Lookup plugins retrieve data from external sources during playbook execution.
---
- name: Lookup Plugins Examples
hosts: localhost
tasks:
# File lookup
- name: Read file contents
debug:
msg: "{{ lookup('file', '/etc/hostname') }}"
# Environment variable lookup
- name: Get environment variable
debug:
msg: "Home: {{ lookup('env', 'HOME') }}"
# Password generation and storage
- name: Generate password
debug:
msg: "{{ lookup('password', '/tmp/passwordfile chars=ascii_letters,digits length=16') }}"
# Template lookup
- name: Render template
debug:
msg: "{{ lookup('template', 'config.j2') }}"
# Pipe lookup (run command)
- name: Get command output
debug:
msg: "{{ lookup('pipe', 'date +%Y-%m-%d') }}"
# Lines lookup (iterate over file lines)
- name: Read file lines
debug:
msg: "{{ item }}"
with_lines:
- cat /etc/hosts
# First found lookup
- name: Find first existing file
debug:
msg: "{{ lookup('first_found', findme) }}"
vars:
findme:
- /etc/app/config.production.yml
- /etc/app/config.staging.yml
- /etc/app/config.default.yml
# Dictionary to items
- name: Convert dict to items
debug:
msg: "{{ item.key }}: {{ item.value }}"
with_dict:
name: myapp
version: 2.5.1
port: 8080
# CSV file lookup
- name: Read CSV data
debug:
msg: "{{ lookup('csvfile', 'server1 file=/path/to/servers.csv delimiter=,') }}"
# INI file lookup
- name: Read INI value
debug:
msg: "{{ lookup('ini', 'port section=database file=/etc/app.ini') }}"
# URL lookup (fetch content from URL)
- name: Fetch from URL
debug:
msg: "{{ lookup('url', 'https://api.example.com/config') }}"
# DNS lookup
- name: DNS query
debug:
msg: "{{ lookup('dig', 'example.com') }}"
# Redis lookup (if redis is configured)
- name: Get from Redis
debug:
msg: "{{ lookup('redis_kv', 'redis://localhost:6379,mykey') }}"
when: false # Example only
# Nested lookup
- name: Dynamic lookup
debug:
msg: "{{ lookup('file', lookup('env', 'HOME') + '/.ssh/id_rsa.pub') }}"
# Combined with filters
- name: Process lookup result
debug:
msg: "{{ lookup('file', '/etc/hostname') | upper }}"
# Using query (always returns list)
- name: Query returns list
debug:
msg: "{{ query('env', 'HOME', 'USER', 'PATH') }}"
# Fileglob lookup
- name: Find all matching files
debug:
msg: "{{ item }}"
with_fileglob:
- /etc/ansible/*.conf
# Together lookup (zip lists)
- name: Combine lists
debug:
msg: "{{ item.0 }} -> {{ item.1 }}"
with_together:
- ['a', 'b', 'c']
- [1, 2, 3]
# Subelements (nested iteration)
- name: Iterate nested data
debug:
msg: "{{ item.0.name }}: {{ item.1 }}"
with_subelements:
- users:
- name: john
groups: [admin, developers]
- name: jane
groups: [developers]
- groups
11. Ansible Vault for Sensitive Data
Encrypt sensitive variables using Ansible Vault.
# Create encrypted file
ansible-vault create secrets.yml
# Edit encrypted file
ansible-vault edit secrets.yml
# Encrypt existing file
ansible-vault encrypt vars/production.yml
# Decrypt file
ansible-vault decrypt vars/production.yml
# View encrypted file
ansible-vault view secrets.yml
# Rekey (change password)
ansible-vault rekey secrets.yml
---
# secrets.yml (encrypted)
db_password: SuperSecretPassword123!
api_key: abcdef1234567890
aws_secret_key: AKIAIOSFODNN7EXAMPLE
---
# Using vault in playbook
- name: Deploy with Vault
hosts: all
vars_files:
- secrets.yml
tasks:
- name: Configure database
template:
src: database.yml.j2
dest: /etc/app/database.yml
no_log: true # Don't log sensitive data
# Run playbook with vault password
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass.txt
---
# Inline vault encryption for single variables
- name: Use vault inline
hosts: localhost
vars:
public_key: ssh-rsa AAAAB3...
private_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653063336164663966303231363934653561363964363833313662
6431626536303530376336343832656537303632313433360a626438346336353331386135323034
tasks:
- name: Deploy SSH keys
copy:
content: "{{ private_key }}"
dest: /home/user/.ssh/id_rsa
mode: '0600'
no_log: true
---
# Best practices for vault
- name: Vault Best Practices
hosts: all
vars_files:
- vars/common.yml # Unencrypted vars
- vars/vault.yml # Encrypted secrets only
tasks:
# Reference vault variables
- name: Use secret in configuration
template:
src: config.j2
dest: /etc/app/config.yml
vars:
db_connection: "postgresql://{{ db_user }}:{{ vault_db_password }}@{{ db_host }}"
no_log: true
# Group vault files by environment
# vars/vault_production.yml
# vars/vault_staging.yml
# vars/vault_development.yml
- name: Include environment vault
include_vars:
file: "vars/vault_{{ environment }}.yml"
no_log: true
# Multiple vault passwords
ansible-playbook site.yml --vault-id prod@~/.vault_prod.txt --vault-id dev@prompt
---
# vars/vault_prod.yml (encrypted with prod password)
!vault |
$ANSIBLE_VAULT;1.2;AES256;prod
...
# vars/vault_dev.yml (encrypted with dev password)
!vault |
$ANSIBLE_VAULT;1.2;AES256;dev
...
12. Nested Variables and Complex Data
Work with complex nested data structures efficiently.
---
- name: Complex Data Structures
hosts: localhost
vars:
# Nested dictionaries
application:
name: myapp
version: 2.5.1
config:
database:
host: db.example.com
port: 5432
credentials:
username: appuser
password: secret123
cache:
type: redis
host: cache.example.com
port: 6379
features:
authentication: true
analytics: true
api_v2: false
# List of dictionaries
servers:
- name: web01
ip: 192.168.1.10
roles: [web, app]
specs:
cpu: 4
memory: 8192
- name: db01
ip: 192.168.1.20
roles: [database]
specs:
cpu: 8
memory: 32768
# Complex deployment config
environments:
production:
region: us-east-1
instances: 10
auto_scaling:
min: 5
max: 20
cpu_threshold: 70
staging:
region: us-west-2
instances: 3
auto_scaling:
min: 2
max: 5
cpu_threshold: 80
tasks:
# Access nested values - dot notation
- name: Access deep nested value
debug:
msg: "DB User: {{ application.config.database.credentials.username }}"
# Access nested values - bracket notation
- name: Access with brackets
debug:
msg: "DB Port: {{ application['config']['database']['port'] }}"
# Safe access with default
- name: Safe nested access
debug:
msg: "API v3: {{ application.config.features.api_v3 | default('not configured') }}"
# Loop through list of dicts
- name: Process servers
debug:
msg: "{{ item.name }} ({{ item.ip }}): {{ item.specs.cpu }} CPUs, {{ item.specs.memory }}MB RAM"
loop: "{{ servers }}"
# Filter list of dicts
- name: Get web servers only
debug:
msg: "Web server: {{ item.name }}"
loop: "{{ servers | selectattr('roles', 'contains', 'web') | list }}"
# Access nested dict in loop
- name: Show environment configs
debug:
msg: "{{ item.key }}: {{ item.value.instances }} instances in {{ item.value.region }}"
loop: "{{ environments | dict2items }}"
# Merge dictionaries
- name: Merge configs
set_fact:
final_config: "{{ default_config | combine(application.config, recursive=True) }}"
vars:
default_config:
database:
pool_size: 10
cache:
ttl: 3600
# Extract values from nested structure
- name: Get all server IPs
set_fact:
server_ips: "{{ servers | map(attribute='ip') | list }}"
- name: Show extracted IPs
debug:
msg: "All IPs: {{ server_ips }}"
# Complex Jinja2 in nested data
- name: Build deployment matrix
set_fact:
deployment_matrix: |
{% for env_name, env_config in environments.items() %}
Environment: {{ env_name }}
Region: {{ env_config.region }}
Instances: {{ env_config.instances }}
Scaling: {{ env_config.auto_scaling.min }}-{{ env_config.auto_scaling.max }}
{% endfor %}
- name: Show matrix
debug:
msg: "{{ deployment_matrix }}"
# Update nested values
- name: Update nested config
set_fact:
application: "{{ application | combine({'config': {'database': {'port': 3306}}}, recursive=True) }}"
# JSON path style access
- name: Use json_query filter
debug:
msg: "High memory servers: {{ servers | json_query('[?specs.memory > `16000`].name') }}"
13. Variable Filters and Transformations
Transform variables using Jinja2 filters for data manipulation.
---
- name: Variable Transformations
hosts: localhost
vars:
server_name: " Web-Server-01 "
port: "8080"
version: "2.5.1"
tags: ["web", "production", "primary"]
config_string: '{"host": "example.com", "port": 443}'
tasks:
# String transformations
- name: String filters
debug:
msg:
- "Trimmed: '{{ server_name | trim }}'"
- "Lower: {{ server_name | lower | trim }}"
- "Upper: {{ server_name | upper | trim }}"
- "Replace: {{ server_name | replace('-', '_') | trim }}"
- "Regex replace: {{ server_name | regex_replace('Server', 'Host') | trim }}"
# Type conversions
- name: Type conversion
debug:
msg:
- "String to int: {{ port | int }}"
- "Int to string: {{ port | int | string }}"
- "To bool: {{ 'yes' | bool }}"
- "To float: {{ '3.14' | float }}"
# List operations
- name: List transformations
debug:
msg:
- "First: {{ tags | first }}"
- "Last: {{ tags | last }}"
- "Length: {{ tags | length }}"
- "Join: {{ tags | join(', ') }}"
- "Sorted: {{ tags | sort }}"
- "Unique: {{ (tags + ['web', 'backup']) | unique }}"
# JSON/YAML conversions
- name: Data format conversions
debug:
msg:
- "Parse JSON: {{ config_string | from_json }}"
- "To JSON: {{ tags | to_json }}"
- "To YAML: {{ tags | to_yaml }}"
- "To nice JSON: {{ tags | to_nice_json }}"
# Default values
- name: Default filters
debug:
msg:
- "With default: {{ undefined_var | default('default_value') }}"
- "Boolean default: {{ undefined_var | default(false) }}"
- "Omit if undefined: {{ undefined_var | default(omit) }}"
# Path operations
- name: Path filters
debug:
msg:
- "Basename: {{ '/opt/app/config.yml' | basename }}"
- "Dirname: {{ '/opt/app/config.yml' | dirname }}"
- "Expand home: {{ '~/config' | expanduser }}"
- "Real path: {{ '.' | realpath }}"
# Math operations
- name: Math filters
debug:
msg:
- "Absolute: {{ -5 | abs }}"
- "Round: {{ 3.14159 | round(2) }}"
- "Min: {{ [5, 2, 8, 1] | min }}"
- "Max: {{ [5, 2, 8, 1] | max }}"
- "Sum: {{ [1, 2, 3, 4] | sum }}"
# Encoding/Decoding
- name: Encoding filters
debug:
msg:
- "Base64: {{ 'hello' | b64encode }}"
- "Decode: {{ 'aGVsbG8=' | b64decode }}"
- "URL encode: {{ 'hello world' | urlencode }}"
- "Hash: {{ 'password' | hash('sha256') }}"
# Version comparison
- name: Version filters
debug:
msg:
- "Is version 2+: {{ version is version('2.0', '>=') }}"
- "Is version 3-: {{ version is version('3.0', '<') }}"
# Ternary operator
- name: Conditional assignment
debug:
msg: "Environment: {{ (port | int == 443) | ternary('production', 'development') }}"
# Combining filters
- name: Filter chain
debug:
msg: "{{ server_name | trim | lower | replace('-', '_') }}"
# Custom data transformations
- name: Complex transformation
set_fact:
processed_tags: "{{ tags | map('upper') | map('regex_replace', '^(.*)$', 'TAG_\1') | list }}"
- name: Show processed tags
debug:
msg: "{{ processed_tags }}"
14. Best Practices
1. Naming Conventions
# Good - Clear, descriptive names
---
vars:
nginx_worker_processes: 4
mysql_max_connections: 500
app_deployment_version: "2.5.1"
enable_ssl_certificate_validation: true
# Bad - Unclear, abbreviated names
vars:
wp: 4
mc: 500
ver: "2.5.1"
ssl: true
# Use prefixes for role variables
# roles/nginx/defaults/main.yml
nginx_port: 80
nginx_user: www-data
nginx_log_dir: /var/log/nginx
# Use snake_case consistently
good_variable_name: value
bad-variable-name: value # Avoid hyphens
2. Variable Organization
# Directory structure
inventory/
group_vars/
all/
00_common.yml # Common variables
01_network.yml # Network configuration
02_security.yml # Security settings
production/
main.yml # Production settings
vault.yml # Encrypted secrets
host_vars/
web01.example.com/
main.yml
# Separate encrypted and unencrypted variables
# group_vars/all/common.yml
db_host: db.example.com
db_port: 5432
db_name: production
# group_vars/all/vault.yml (encrypted)
vault_db_password: secret123
vault_api_key: abcdef
# Reference vault variables with prefix
# tasks/main.yml
- name: Configure database
template:
src: database.yml.j2
dest: /etc/app/database.yml
vars:
db_password: "{{ vault_db_password }}"
3. Documentation
# Document variables in defaults
# roles/nginx/defaults/main.yml
---
# Nginx worker processes (auto = number of CPU cores)
nginx_worker_processes: auto
# Maximum number of simultaneous connections per worker
nginx_worker_connections: 1024
# User under which nginx runs
nginx_user: www-data
# Enable SSL/TLS
# Set to true to enable HTTPS
nginx_ssl_enabled: false
# SSL certificate path (required if nginx_ssl_enabled is true)
nginx_ssl_certificate: /etc/ssl/certs/server.crt
# Use README for complex variables
# group_vars/production/README.md
## Production Variables
### Database Configuration
- `db_host`: Production database hostname
- `db_port`: Database port (default: 5432)
- `vault_db_password`: Encrypted database password (in vault.yml)
### Deployment Settings
- `deploy_version`: Application version to deploy
- `deploy_strategy`: Deployment strategy (rolling_update, blue_green, recreate)
4. Security
# Always use no_log for sensitive data
- name: Set database password
set_fact:
db_connection_string: "postgresql://user:{{ vault_db_password }}@host/db"
no_log: true
# Don't define secrets in playbooks
# Bad
- name: Deploy
hosts: all
vars:
api_key: "hardcoded-secret-key" # Never do this
# Good - use vault
- name: Deploy
hosts: all
vars_files:
- vars/vault.yml
vars:
api_key: "{{ vault_api_key }}"
# Use separate vault files per environment
group_vars/
production/
vault.yml # Encrypted with production password
staging/
vault.yml # Encrypted with staging password
5. Testing and Validation
# Validate required variables
- name: Validate required variables
assert:
that:
- app_name is defined
- app_version is defined
- environment in ['development', 'staging', 'production']
fail_msg: "Required variables are missing or invalid"
# Provide defaults in role
# roles/myapp/defaults/main.yml
myapp_port: 8080
myapp_workers: 4
# Override in inventory
# group_vars/production.yml
myapp_workers: 16
# Use --check mode to test
ansible-playbook site.yml --check --diff
# Debug variable precedence
- name: Show variable sources
debug:
msg:
- "my_var value: {{ my_var }}"
- "From group_vars: {{ lookup('file', 'group_vars/all.yml') | from_yaml }}"
6. Performance
# Cache facts for reuse
- name: Cache deployment time
set_fact:
deployment_timestamp: "{{ ansible_date_time.epoch }}"
cacheable: yes
# Don't gather facts if not needed
- name: Simple file copy
hosts: all
gather_facts: no
tasks:
- copy:
src: file.txt
dest: /tmp/
# Use hostvars carefully (can be slow)
# Bad - iterates all hosts
- name: Get all IPs
debug:
msg: "{{ hostvars[item]['ansible_default_ipv4']['address'] }}"
loop: "{{ groups['all'] }}"
# Good - limit to needed hosts
- name: Get web server IPs
debug:
msg: "{{ hostvars[item]['ansible_default_ipv4']['address'] }}"
loop: "{{ groups['webservers'] }}"
run_once: true
Conclusion
Understanding Ansible variables and their precedence is fundamental to creating flexible, maintainable automation. Key takeaways:
- Precedence Matters: Know the order - extra vars always win, role defaults are lowest
- Organize Well: Use group_vars, host_vars, and roles effectively
- Stay Secure: Use Ansible Vault for sensitive data, always use no_log
- Be Dynamic: Leverage set_fact, registered variables, and lookups
- Use Magic Variables: hostvars, groups, and inventory_hostname provide powerful capabilities
- Document Everything: Clear variable names and documentation prevent confusion
- Test Thoroughly: Validate variables with assert and use --check mode
With mastery of Ansible variables, you can build sophisticated, data-driven automation that adapts to any infrastructure.
Related Articles:
- What is Ansible? Complete Guide
- Complete Guide to Ansible Playbooks
- Complete Guide to Ansible Conditionals
Was this article helpful?