Ansible OEL 8 DevOps · OEL 8 · Roles

Ansibledefaults/ vs vars/ vs Role-Args

Inside a role you can declare variables in three places — defaults/main.yml, vars/main.yml, and as arguments at invocation. Each has different precedence, and picking the wrong one is the most common role-design mistake.

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.

defaults/main.yml LOWEST precedence overridable by almost anything use for: sensible defaults a caller can change vars/main.yml MIDDLE precedence harder to override (needs extra-vars) use for: role-internal "constants" role: { name: ..., x: 1 } HIGHEST precedence passed by caller at invocation use for: per-call overrides ("install with X=2") overridability ←——————————————————— →

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.

YAML — roles/mysql/defaults/main.yml
---
# 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.

YAML — roles/mysql/vars/main.yml
---
# 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
💡 Tip: Rule of thumb: if a sensible adopter might want to change a value (port, buffer-pool size, list of users), put it in 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.

YAML — passing role arguments
---
- 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"
Aspectdefaults/main.ymlvars/main.ymlRole args (vars: at call)
Precedence levelLowestMiddleHighest
Easy to override?Yes — by anythingHard — only -e & task varsCaller passed it
Visible in role API?Yes — public knobsNo — private internalsCaller decides
Use for…Tunable settingsOS-specific paths, constantsPer-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:

DIR + YAML — OS-specific vars files
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.

MARKDOWN — example role README
# 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
```
✅ Tip: A clean role has 5 lines in vars/, 30 lines in defaults/, and a 50-line README. The shape is itself a signal that the role is properly designed.