Every role can carry its own variables. The same variable name in defaults/main.yml
and vars/main.yml are not the same value — they sit at different levels in the
22-level precedence chain.
Lowest precedence. These are the values that almost any other variable can override —
inventory variables, play variables, CLI -e, role arguments, and so on. This is
where you publish the role's "knobs": settings the caller is expected to override.
---
# Defaults — caller is expected to override these
mysql_version: "8.0"
mysql_port: 3306
mysql_max_connections: 200
mysql_innodb_buffer_pool_size: "1G"
mysql_root_password: "" # caller MUST supply
mysql_root_password_update: false
mysql_databases: []
# example shape:
# - name: myapp
# encoding: utf8mb4
# collation: utf8mb4_unicode_ci
mysql_users: []
# example shape:
# - name: appuser
# password: "{{ vault_appuser_password }}"
# priv: "myapp.*:ALL"
# host: "%"
Higher precedence than defaults/ and most other levels. A vars/ value can only
be overridden by extra-vars (-e on the CLI), block-level vars:, task-level
vars:, or set_fact — not by inventory.
---
# Internal constants the caller has no business changing
mysql_socket_path: /var/lib/mysql/mysql.sock
mysql_pidfile_path: /var/run/mysqld/mysqld.pid
mysql_log_dir: /var/log/mysql
# OS-conditional values that change between distros
mysql_packages_RedHat:
- mysql-server
- mysql-libs
mysql_packages_Debian:
- mysql-server-{{{{ mysql_version }}}}
- mysql-client
defaults/. If changing it would actually break the role (socket path, package names per OS), put it in vars/.The HIGHEST precedence — beats everything inventory/group_vars/host_vars throws at it. The caller passes these directly when invoking the role. Use them for one-off overrides or to make the same role behave differently in two callers.
---
- hosts: databases
roles:
- role: mysql
vars:
mysql_root_password: "{{{{ vault_mysql_pwd }}}}"
mysql_max_connections: 800 # bigger than the default
mysql_databases:
- name: app_db
encoding: utf8mb4
mysql_users:
- name: appuser
password: "{{{{ vault_appuser_pwd }}}}"
priv: "app_db.*:ALL"
| Aspect | defaults/main.yml | vars/main.yml | Role args (vars: at call) |
|---|---|---|---|
| Precedence level | Lowest | Middle | Highest |
| Easy to override? | Yes — by anything | Hard — only -e & task vars | Caller passed it |
| Visible in role API? | Yes — public knobs | No — private internals | Caller decides |
| Use for… | Tunable settings | OS-specific paths, constants | Per-call customisation |
Roles that target multiple OS families often store OS-conditional values in
vars/<family>.yml and load the right one with include_vars:
roles/mysql/vars/
├── RedHat.yml
├── Debian.yml
└── main.yml
# roles/mysql/vars/RedHat.yml
mysql_packages: [mysql-server, mysql-libs]
mysql_service: mysqld
mysql_config_path: /etc/my.cnf
# roles/mysql/vars/Debian.yml
mysql_packages: [mysql-server, mysql-client]
mysql_service: mysql
mysql_config_path: /etc/mysql/my.cnf
# roles/mysql/tasks/main.yml — load the right file at the start
- name: Load OS-specific variables
ansible.builtin.include_vars: "{{{{ ansible_os_family }}}}.yml"
- name: Install MySQL packages
ansible.builtin.dnf:
name: "{{{{ mysql_packages }}}}"
state: present
when: ansible_os_family == "RedHat"
A role's README is the contract with the caller. Always document every variable in
defaults/main.yml — what it controls, what type it expects, and the default value.
# mysql
Installs and configures MySQL 8 on OEL 8.
## Required variables
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `mysql_root_password` | string | `""` | Root password (REQUIRED — must come from vault) |
## Optional variables
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `mysql_port` | int | `3306` | TCP port for mysqld |
| `mysql_max_connections` | int | `200` | Max simultaneous client connections |
| `mysql_innodb_buffer_pool_size` | string | `"1G"` | InnoDB cache size |
| `mysql_databases` | list of dict | `[]` | Databases to create — see below |
## Example invocation
```yaml
- hosts: databases
roles:
- role: mysql
vars:
mysql_root_password: "{{{{ vault_mysql_pwd }}}}"
mysql_max_connections: 800
```
vars/, 30 lines in defaults/, and a 50-line README. The shape is itself a signal that the role is properly designed.