Skip to main content

Variables & Outputs

Ansible has a complex variable precedence system. Understanding and respecting that system prevents subtle bugs and makes playbooks easier to reason about.

Variable Precedence

Ansible evaluates variables in the following order (highest precedence last):

PrecedenceSource
1 (lowest)Role defaults (roles/<role>/defaults/main.yml)
2Inventory group variables (group_vars/)
3Inventory host variables (host_vars/)
4Playbook vars: block
5Role variables (roles/<role>/vars/main.yml)
6Task vars:
7set_fact / registered variables
8 (highest)Extra variables (-e / --extra-vars)

Use role defaults for values that operators are expected to override. Use role vars for internal constants that should not be overridden.

Defining Defaults

Every role must declare defaults for all variables it uses. This documents the role's interface and prevents undefined variable errors at runtime.

Follow these guidelines when writing defaults:

  • Declare every variable the role uses. If a task references a variable, it must have a default. Undeclared variables cause hard-to-debug failures when the role is run in a new context.
  • Do not put everything in defaults. Defaults are for values operators are expected to customize. Internal constants that must not be overridden belong in vars/main.yml, where they carry higher precedence and signal that the value is not a public interface.
  • Use safe, functional values. A default should allow the role to run in a basic environment without modification. Avoid defaults that are environment-specific (e.g. a production hostname) or that would cause harm if applied blindly.
  • Document non-obvious defaults. Add an inline comment when the chosen value requires context, such as a specific port required by a firewall rule or a timeout tuned for a particular workload.
# roles/postgresql_server/defaults/main.yml
postgresql_server_version: "15"
postgresql_server_port: 5432
postgresql_server_max_connections: 100
postgresql_server_data_dir: /var/lib/postgresql/{{ postgresql_server_version }}/main

Group Variables

Organize group_vars/ to mirror the inventory hierarchy:

inventories/production/group_vars/
├── all.yml # Applies to every host
├── webservers.yml # Applies to the webservers group
└── databases.yml # Applies to the databases group

Avoid deeply nested YAML in group vars. Flat structures are easier to override and debug.

Registered Variables

Register task output only when a subsequent task depends on it. Unset registered variables go out of scope at the end of a play:

- name: Check if the database is initialized
ansible.builtin.stat:
path: /var/lib/postgresql/15/main/PG_VERSION
register: pg_data_dir

- name: Initialize the database
ansible.builtin.command: pg_createcluster 15 main
when: not pg_data_dir.stat.exists

Outputs and Return Values

Ansible does not have native outputs like Terraform. Share data between plays using:

  • set_fact with cacheable: true for cross-play variables within a single run.
  • Inventory files or dynamic inventories for values that must persist across runs.
  • External stores (e.g. HashiCorp Vault, AWS SSM Parameter Store) for secrets and shared configuration that multiple playbooks consume.

Variable Typing

Use explicit YAML types to avoid string-vs-boolean ambiguities:

# Explicit, unambiguous
postgresql_server_ssl_enabled: true
postgresql_server_port: 5432
postgresql_server_version: "15" # String, not integer

# Risky: YAML may coerce "yes"/"no" depending on parser version
postgresql_server_ssl_enabled: yes

Always quote version numbers and other values that look numeric but must remain strings.