Roles — A Reuse Unit Folded by Convention
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.
- Put a role's default values in
defaults/. If you put them invars/, the precedence is so high that users cannot override them. This is usually the cause of "I changed the role variable but it has no effect." -eis for debugging. If you put it in a script permanently, every inventory setting is ignored, and later nobody can find out why that value shows up.
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.
- Can it be used as is elsewhere? — If not, there is no reason to split it.
- Do dependencies run in one direction? — If role A calls B and B calls A, that is a cycle.
- Do variable names avoid overlapping? — Prefix each role's variables (
nginx_port).
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.