Introduction

Ansible variables can live in many places: group_vars/, host_vars/, role defaults/, role vars/, playbook vars:, and more. Putting variables in the wrong place leads to precedence confusion and hard-to-debug overrides. This guide shows exactly where each type of variable belongs.

Quick Comparison

LocationScopePrecedencePurpose
Role defaults/Role2 (lowest)Sensible defaults users should override
group_vars/allAll hosts4Organization-wide defaults
group_vars/<group>Group10-12Environment/team-specific values
host_vars/<host>Single host13Host-specific overrides
Play vars:Play14Play-specific values
Role vars/Role18Internal role constants (hard to override)
set_factHost19Runtime-computed values
Extra vars -eGlobal22 (highest)CLI overrides

Directory Structure

inventory/
├── production/
│   ├── hosts.yml
│   ├── group_vars/
│   │   ├── all.yml              # All production hosts
│   │   ├── all/
│   │   │   ├── vars.yml         # Split into files for organization
│   │   │   └── vault.yml        # Encrypted secrets
│   │   ├── webservers.yml       # Web server group
│   │   ├── dbservers.yml        # Database group
│   │   └── monitoring.yml       # Monitoring group
│   └── host_vars/
│       ├── web1.example.com.yml # Host-specific
│       └── db1.example.com.yml
├── staging/
│   ├── hosts.yml
│   ├── group_vars/
│   │   └── all.yml
│   └── host_vars/
│
roles/
├── nginx/
│   ├── defaults/main.yml        # User-overridable defaults
│   └── vars/main.yml            # Internal constants

group_vars — Environment & Team Variables

# group_vars/all.yml — applies to every host
---
ntp_servers:
  - 0.pool.ntp.org
  - 1.pool.ntp.org
dns_servers:
  - 8.8.8.8
  - 8.8.4.4
timezone: UTC
admin_email: ops@example.com

# group_vars/webservers.yml — only web servers
---
http_port: 80
https_port: 443
app_port: 8080
max_connections: 1024
ssl_cert_path: /etc/ssl/certs/site.crt

# group_vars/dbservers.yml — only database servers
---
pg_version: 16
pg_port: 5432
pg_max_connections: 200
pg_data_dir: /var/lib/postgresql/16/main
backup_schedule: "0 2 * * *"

Splitting Large group_vars

# group_vars/all/vars.yml — non-secret variables
---
env: production
deploy_user: deploy
app_version: "2.5.0"

# group_vars/all/vault.yml — encrypted with ansible-vault
---
vault_db_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  ...
vault_api_key: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  ...

host_vars — Host-Specific Overrides

# host_vars/web1.example.com.yml
---
# Override group default
max_connections: 2048  # This host handles more traffic

# Host-specific settings
ansible_host: 10.0.1.10
custom_vhosts:
  - name: api.example.com
    port: 8081
  - name: admin.example.com
    port: 8082

# host_vars/db1.example.com.yml
---
pg_max_connections: 500  # Primary DB needs more connections
pg_wal_level: replica
pg_replicas:
  - db2.example.com
  - db3.example.com

Role defaults — Overridable Defaults

# roles/nginx/defaults/main.yml
# LOWEST precedence — designed to be overridden
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: "10m"
nginx_gzip: true
nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_log_format: combined

# Users override in group_vars or host_vars:
# group_vars/webservers.yml:
#   nginx_client_max_body_size: "100m"

Role vars — Internal Constants

# roles/nginx/vars/main.yml
# HIGH precedence — not meant to be overridden
---
nginx_conf_dir: /etc/nginx
nginx_log_dir: /var/log/nginx
nginx_pid: /run/nginx.pid
nginx_user: www-data  # Platform-specific

# roles/nginx/vars/Debian.yml — OS-specific constants
---
nginx_package: nginx-full
nginx_service: nginx

When to Use Each

Role defaults/ (precedence 2)

# ✅ Values users SHOULD customize per environment
nginx_worker_connections: 1024
app_log_level: info
backup_retention_days: 30

group_vars/ (precedence 10-12)

# ✅ Environment-specific: prod vs staging vs dev
env: production
deploy_branch: main
monitoring_enabled: true

# ✅ Group-specific: webservers vs dbservers
http_port: 80
pg_version: 16

host_vars/ (precedence 13)

# ✅ Truly host-specific: IPs, special config, overrides
ansible_host: 10.0.1.10
special_cron: "0 */4 * * *"

# ✅ One-off overrides for specific machines
max_connections: 4096  # This box has 128GB RAM

Role vars/ (precedence 18)

# ✅ Internal constants users shouldn't change
config_dir: /etc/myapp
required_packages:
  - python3
  - python3-pip

Common Mistakes

# ❌ Putting secrets in group_vars without vault
# group_vars/all.yml
db_password: "s3cret123"  # Committed to git in plaintext!

# ✅ Use ansible-vault
# group_vars/all/vault.yml (encrypted)
vault_db_password: "s3cret123"

# group_vars/all/vars.yml (references vault)
db_password: "{{ vault_db_password }}"

# ❌ Using role vars/ for values users should customize
# roles/app/vars/main.yml
app_port: 8080  # Hard to override! (precedence 18)

# ✅ Use role defaults/ instead
# roles/app/defaults/main.yml
app_port: 8080  # Easy to override in group_vars (precedence 2)

# ❌ Duplicating the same variable in multiple places
# group_vars/all.yml:       http_port: 80
# group_vars/webservers.yml: http_port: 80  # Redundant!
# host_vars/web1.yml:        http_port: 80  # Even more redundant!

# ✅ Define once at the most general level, override only when different

Practical Example

# group_vars/all.yml
env: production
app_version: "2.5.0"
monitoring_enabled: true

# group_vars/webservers.yml  
app_port: 8080
nginx_worker_connections: 2048

# host_vars/web1.example.com.yml
nginx_worker_connections: 4096  # High-traffic host

# roles/nginx/defaults/main.yml
nginx_worker_connections: 1024  # Sensible default

# Resolution for web1: 4096 (host_vars wins)
# Resolution for web2: 2048 (group_vars wins over role defaults)
# Resolution for db1:  1024 (role default, no override)

Conclusion

Role defaults/ for values users should override (lowest precedence). group_vars/ for environment and team-specific settings. host_vars/ for individual host overrides. Role vars/ for internal constants that shouldn't change. The rule: define at the most general level, override at the most specific. Keep secrets in vault-encrypted files. Split large group_vars into separate files for readability.