Complete Guide to Ansible Variables and Precedence: Master Data Flow in Your Playbooks

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.

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)

  1. command line values (for example, -u my_user, these are not variables)
  2. role defaults (defined in role/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 (defined in role/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 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:

Was this article helpful?

RS

About the Author: Ramesh Sundararamaiah

Red Hat Certified Architect

Ramesh is a Red Hat Certified Architect with extensive experience in enterprise Linux environments. He specializes in system administration, DevOps automation, and cloud infrastructure. Ramesh has helped organizations implement robust Linux solutions and optimize their IT operations for performance and reliability.

Expertise: Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, System Administration, DevOps

Add Comment