TT Lab
Get started
Learn Learning paths Courses

Ansible in Practice

Roles — A Reuse Unit Folded by Convention

Continue in TT Lab

Summary in one line

A role splits pieces of a playbook across fixed directory names, and because of that convention it can be reused without any configuration.

Why this is needed

Put nginx installation, app deployment, log rotation, and a monitoring agent into a single playbook and it grows to 300 lines. Then a second service appears and people copy that file. From the moment it is copied, the two files age at different speeds. Six months later, the answer to "why aren't logs being cleaned up on server A only?" is always "because the copy was never updated."

Roles solve this by "enforcing the unit of reuse through file system structure." Inside a role, tasks, defaults, handlers, and templates each have their own place, so even a role someone else wrote tells you where everything is without opening it.

How it works

Directory What it holds Auto-loaded
tasks/main.yml Tasks to run When the role is called
defaults/main.yml Default values that are expected to be overridden Lowest priority
vars/main.yml Internal values that must not change High priority
handlers/main.yml Handlers Registered automatically
templates/ Jinja2 templates The template module finds them without a path
files/ Files to copy as they are The copy module finds them without a path
meta/main.yml Dependent roles, metadata Runs dependent roles first

The key point is the distinction between defaults and vars. Always put values a user is likely to change in defaults. If you put them in vars, their priority is so high that overriding them from outside becomes practically impossible, and the role can no longer be reused.

dependencies in meta/main.yml declares "this role needs that role before it." Even if the same dependent role appears several times, by default it runs only once.

What it looks like in the field

First, name collisions. With several roles, variable names overlap. That is why the convention is to prefix role variables with the role name — webapp_port, not port.

Second, never use absolute paths inside a role. templates/ and files/ are searched automatically relative to the role, so you only need to write the file name. If you hard-code an absolute path, the role works only on that one server.

Third, roles must be idempotent too. The habit of running a playbook that uses a role twice and checking that you get changed=0 applies to roles as well.

Variable precedence causes incidents

Ansible variables have twenty-two levels of precedence. You do not need to memorize them all, but you should know the order of the five you run into most often (stronger as you go down).

1. 롤의 defaults/main.yml     ← 가장 약하다. 덮어쓰라고 있는 것
2. inventory group_vars
3. inventory host_vars
4. play 의 vars
5. -e (extra vars)            ← 가장 강하다. 무엇도 못 이긴다

Two rules follow from this.

You can simply ask where a value came from.

ansible -i inv host -m debug -a "var=app_port"
ansible-inventory -i inv --host web-01 --yaml     # 그 호스트에 적용된 전부

Modules that break idempotency

The value of Ansible is "running it twice gives the same result." A few things break this.

What not to use Use instead
shell: echo x >> /etc/hosts lineinfile, blockinfile
command: mkdir /opt/app file: state=directory
shell: sed -i … lineinfile, template
command: curl … | bash get_url + command (after verification)

If you really must use the shell, guard it with a condition using creates or removes.

- name: 설치 스크립트 (한 번만)
  command: /opt/setup.sh
  args:
    creates: /opt/.installed      # 이 파일이 있으면 건너뛴다

changed_when: false is a different matter. It is a declaration that "this command does not change state," so use it only on lookup commands. If you attach it to a command that really does change things, the report starts to lie.

Criteria for splitting roles

When a role grows large you want to split it, but without criteria you just end up with more pieces.

The third one is the practical trap. If two roles use a variable called port, the later one wins. Ansible does not warn you. The convention of prefixing variables with the role name prevents this.

What you will do in the next lab

You build the webapp role from the skeleton up, fill in defaults, tasks, a template, and a handler, and attach the baseline role as a dependency to check the execution order. You call the same role twice with different variables to prove its reusability, and make the second run produce changed=0.