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):

  1. Command-line values (e.g., -u my_user)
  2. Role defaults (roles/*/defaults/main.yml)
  3. Inventory file or script group vars
  4. Inventory group_vars/all
  5. Playbook group_vars/all
  6. Inventory group_vars/*
  7. Playbook group_vars/*
  8. Inventory file or script host vars
  9. Inventory host_vars/*
  10. Playbook host_vars/*
  11. Host facts / cached set_facts
  12. Play vars
  13. Play vars_prompt
  14. Play vars_files
  15. Role vars (roles/*/vars/main.yml)
  16. Block vars (only for tasks in block)
  17. Task vars (only for the task)
  18. include_vars
  19. set_facts / registered vars
  20. Role (and include_role) params
  21. Include params
  22. Extra vars (-e in 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

  1. Use Descriptive Names: nginx_port not port
  2. Namespace Variables: Prefix with role name (mysql_root_password)
  3. Document Variables: Add comments explaining purpose
  4. Set Sensible Defaults: Use role defaults for common values
  5. Avoid Overriding: Don't fight variable precedence
  6. Keep Secrets Separate: Use vault for sensitive data
  7. Use Facts Wisely: Disable gathering when not needed
  8. 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