Ansible Playbook vs Role: Which One You Go Use?
Know when one flat playbook enough and when role make sense, with role layout, ansible-galaxy init, role calls, and variable precedence explained.
Ansible playbook versus role: wetin be the difference
Ansible playbook na the file wey you run with ansible-playbook. E map one group of hosts to the work wey dem need. Ansible role na directory wey get fixed layout, and e hold tasks, templates, handlers and default variables. Playbook dey call am by name. The task syntax inside both na the same, so no be question of wetin you fit express. Na question of reuse.
Start with one flat playbook. One site.yml wey hold tasks: list na the correct shape for your first automation, and e go remain correct for longer than most people expect. Convert am to role when the same block of tasks need run for another group of hosts, or when the file pass roughly 100 lines and you no fit find task again by scrolling.
If you never write one before, start with your first playbook against one VPS and come back when e start grow.
When flat playbook na the correct answer
Flat playbook correct when na one-time work, work for one host, or nobody else go read am. If you dey provision one application server, or patch one box before maintenance window, you no need directory tree. Role go add seven directories and one extra layer wey dey hide the real work. If na only the playbook wey dey beside am go call am, this extra layer no give you any benefit. E only make you jump around every time you want read wetin actually go run.
Flat playbook stop to be correct for one clear moment, and e easy to notice. You copy one block of tasks enter second playbook. That copy na the signal. From that time, you must apply every fix two times. One day, you go apply am only once.
Wetin role directory really dey hold
roles/common/
defaults/main.yml
vars/main.yml
tasks/main.yml
handlers/main.yml
templates/99-hardening.conf.j2
files/
meta/main.ymltasks/main.ymlna the entry point. Ansible dey run this file when dem call the role, and every other directory na optional.defaults/main.ymldey hold variables wey caller suppose override. Na the source wey get the lowest priority for Ansible, so almost anything else go override am.vars/main.ymldey hold variables wey caller no suppose override. E dey above inventory for priority, so na strong decision to make. Use am rarely.handlers/main.ymldey hold tasks weynotifytrigger. Handler dey run for the end of the play, once, no matter how many tasks notify am.files/dey hold files weycopymodule copy exactly as dem be, whiletemplates/dey hold Jinja2 templates weytemplatemodule render. Inside role, reference both with bare filename and no path, because Ansible dey first search the role own directories.meta/main.ymldey declare role dependencies and the metadata wey Ansible Galaxy dey read.
This layout no be matter of style preference. Ansible dey check these exact paths, so if you put template for roles/common/template/ (singular), Ansible simply no go find am.
Build common role with ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonThis one go write the complete skeleton under roles/common, including directories wey you no go use and main.yml stubs wey only contain ---. Delete the ones wey remain empty. Empty vars/main.yml no dey cause problem for Ansible, but e dey hide which files for the role actually matter.
Now fill the files wey dey do the work. Start with defaults, because dem be the role public interface.
# roles/common/defaults/main.yml
---
common_packages:
- ufw
- fail2ban
- unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"Put quote around "no" and "yes". Ansible dey parse YAML with PyYAML. PyYAML dey read bare no as boolean false, so the rendered config line go become PermitRootLogin False and sshd go reject am. The quotes make the value remain string.
# roles/common/tasks/main.yml
---
- name: Install the base packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Create the admin group
ansible.builtin.group:
name: "{{ common_admin_group }}"
state: present
- name: Install the sshd hardening drop-in
ansible.builtin.template:
src: 99-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/99-hardening.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart sshd# roles/common/handlers/main.yml
---
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}For Debian and Ubuntu, the systemd unit name na ssh. For RHEL family systems, na sshd. Handler wey name the wrong one no go fail until something actually change the template. Na why the problem usually dey show weeks later.
The validate line na the most useful part of that task. Ansible go render the template to temporary file, replace %s with the path to that file, then run the command. E go replace the destination only if the command exits 0. Put nonsense directive inside the template and run am again. The task go fail with failed to validate, the real /etc/ssh/sshd_config.d/99-hardening.conf no go change, and you still get server wey you fit log into. Remember say the check dey test more than your syntax. If sshd -t no fit read the host keys, e go exit with sshd: no hostkeys available -- exiting. and Ansible go report the same failed to validate. So read the module msg before you blame the template.
How playbook dey call role
# site.yml
---
- name: Base configuration for every server
hosts: all
become: true
roles:
- common# inventory.ini
[local]
localhost ansible_connection=localansible-playbook -i inventory.ini site.ymlThe play suppose end with failed=0 for the recap. Pass parameters for the call site with the expanded form. Na this one make one role fit serve two groups of hosts:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordOne ordering rule dey surprise almost everybody. A play fit get pre_tasks, roles, tasks and post_tasks, and Ansible go run dem for that order no matter the order wey you write dem for the file. Put tasks: above roles: and the roles still go run first. So if something must happen before a role, e belong for pre_tasks:, no be for the top of tasks:.
- name: Ordering demonstration
hosts: local
gather_facts: false
pre_tasks:
- name: Runs first
ansible.builtin.debug:
msg: pre
roles:
- common
tasks:
- name: Runs after the role
ansible.builtin.debug:
msg: task
post_tasks:
- name: Runs last
ansible.builtin.debug:
msg: postTo call a role from inside task list instead of the roles: key, use import_role or include_role.
tasks:
- name: Static, read when the playbook is parsed
ansible.builtin.import_role:
name: common
- name: Dynamic, resolved when the task runs
ansible.builtin.include_role:
name: postgres
when: "'db' in group_names"import_role na static. Ansible go read the role for parse time, and the tasks go become part of the play. So ansible-playbook --list-tasks site.yml go list dem, and tag for the import go apply to every task inside. include_role na dynamic. Nothing go read until the task run. Na this one allow you drive the role name from variable or loop. But the cost be say those tasks no dey visible to --list-tasks and --start-at-task.
One trap dey here. A when: for an include_role task dey evaluate before the included role's defaults/main.yml enter scope. If you write when: common_packages | length > 0 for the include, the run go stop with 'common_packages' is undefined, even though that variable dey defined inside the role wey you dey include. The fix na to move the toggle comot from the role. Put am for group_vars/all.yml, where e dey in scope everywhere, and leave the role's defaults for values wey the role itself dey use.
Which variable wins: defaults, group_vars, vars, extra vars
Ansible dey document more than twenty levels of variable precedence. Four of dem dey settle almost every real argument, and na these be dem from weakest to strongest.
roles/<name>/defaults/main.ymldey near the bottom. Almost anything wey you set anywhere else go beat am, and na exactly why e be the correct place for role tunable knobs.group_vars/andhost_vars/dey for the middle. Na here your site own answers suppose dey, and dem go cleanly override role defaults.roles/<name>/vars/main.ymldey abovehost_vars. Any value wey you put here inventory no fit override. Keep am for things wey role need to keep internally consistent, like package name wey must match service name.- Role parameter wey you pass for call site go beat
vars/main.yml, and-efor command line go beat everything, including role parameters.
You fit watch how this one resolve within about one minute. Give small role one default and one role var, then set the same names for host_vars.
# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults# roles/prec/vars/main.yml
---
prec_internal: from-rolevars# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars# roles/prec/tasks/main.yml
---
- name: Show which value survived
ansible.builtin.debug:
msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cliThe first run go print tunable=from-hostvars internal=from-rolevars. Inventory beat role default and lose to role var. The second run go print internal=from-cli, because extra vars dey for the very top and nothing below fit push back. Na also why -e dey okay for one-off run but wrong for script wey you dey keep: e silently outrank every decision wey repository consider.
The working rule be say: if you want make people fit set a value, put am for defaults/. Putting am for vars/ tells every future user of the role say inventory no fit change am. Sometimes na wetin you mean, but most times na mistake.
Prove say the role dey idempotent: run am two times
A trustworthy Ansible run go produce the same result the second time and report say e change nothing. Run the playbook two times and read the recap.
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.ymlThe second recap suppose look like this:
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 mean say every module inspect the current state and find say the work don already finish. changed=2 for a second run mean say two tasks no fit tell the difference, so dem go continue rewrite files and restart services forever. The usual cause na command or shell, because Ansible no get way to know wetin an arbitrary command do.
# traps.yml
---
- name: Command modules do not know what they changed
hosts: local
gather_facts: false
tasks:
- name: This appends a line on every run
ansible.builtin.shell: "echo run >> /tmp/grow.txt"
- name: This appends a line only once
ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
args:
creates: /tmp/guarded.txtRun that playbook two times, then count the lines wey get wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt get two lines and /tmp/guarded.txt get one. For the second run, the guarded task no execute at all, and e result carry the message skipped, since /tmp/guarded.txt exists, because creates give the module one visible product to look for first. When a command no leave such product, register its output and decide by yourself with changed_when.
ansible-playbook --check --diff site.yml dey predict changes without making dem, and --diff dey print the exact lines wey a template go rewrite. Read the output with one thing for mind: shell and command tasks dey skip for check mode, so a plan wey look clean fit still hide work.
One more column for that recap need the same care: host wey Ansible no fit connect to dey count under unreachable instead of failed, and none of its tasks run at all. So decide ahead whether one unreachable host suppose stop the whole run before you point this role to more than a couple of machines.
Why Ansible dey talk say e no find the role
Ansible dey first look for a roles/ directory beside the playbook file, then e go check roles_path. The search dey follow the playbook, no be your shell.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyThat message mean say site.yml and roles/ don separate, and e show the paths wey e try. Keep the two for the same directory. You fit run am from a parent directory, because na the playbook path dey matter:
ansible-playbook -i infra/inventory.ini infra/site.ymlAnother quieter form of the same problem dey. Ansible dey ignore ansible.cfg for the current directory when that directory dey world writable, because any user for the box fit put config there and change wetin your run go do.
[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.Your roles_path and inventory settings go then disappear silently, and the role lookup go fail for reason wey no get anything to do with roles. ansible --version dey print the config file wey e actually load, while ansible-config dump --only-changed dey print every setting wey different from the built-in defaults. Check both anytime run behave as if your config no dey exist.
Role sharing: requirements.yml and one fixed version
Person wey write a role don install am; you no copy am. Declare am once:
# requirements.yml
---
roles:
- name: postgres
src: https://github.com/example/ansible-role-postgres
scm: git
version: v1.4.0ansible-galaxy install -r requirements.yml -p galaxy_rolesAlways set version. If you no set am, you go get anything wey default branch hold on the day you run the command. This mean deployment wey work last month fit break without any change for your own repository. Point roles_path to the download directory, and keep that directory out of git:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesAnsible still go find roles for roles/ beside the playbook, because e always search that path together with roles_path. So, keep your own roles committed and reviewed, while third-party roles remain reproducible downloads pinned to one tag.
Where roles no longer be the answer
Role na one unit wey you fit reuse inside one Ansible run. E no create servers or DNS records for your provider. If you try make am do that, your playbooks fit turn to something wey nobody wan maintain. the way work dey split between Ansible and Terraform good make you read am before you start. Role still no replace inventory design. Once your machines pass small number, how you group and reach those servers matter pass how you arrange the tasks.
The hardening wey this common role install get decisions of its own. The drop-in above set only two directives. E no set anything else. So read which SSH settings really worth changing and how to make Ubuntu apply security updates by itself before you decide wetin belong inside the role for every host wey you manage.
FAQ
I go know when I suppose turn Ansible playbook to role?
When the same block of tasks need run for another play, or against another group of hosts. If you dey copy tasks between playbooks, na signal say you suppose use role. From that point, every fix go need apply two times, and one day you fit apply am only once. One playbook wey get around 100 lines or less and dey target only one group no gain anything from role. The extra directories fit make am harder to read.
Roles dey run before tasks for the same play?
Yes. Ansible dey run pre_tasks first, then everything wey dey under roles:, then tasks:, then post_tasks:. E no dey follow the order wey those keys appear for your file. If you write tasks: above roles:, that no mean say those tasks go run first. If something must happen before role, put am for pre_tasks:.
Why my group_vars value no override the role?
Check whether you set the variable for the role's vars/main.yml instead of defaults/main.yml. vars/ dey above group_vars and host_vars for Ansible precedence order, so inventory no fit override am. Move the variable go defaults/main.yml. E dey near the bottom of the order and na the correct place for anything wey caller suppose fit change. To confirm say precedence cause the problem and no be typo, run am once with -e name=value. E get higher precedence than every other source.
Why Ansible dey talk say e no find the role?
The search dey start beside the playbook file, so site.yml and roles/ must dey inside the same directory. The error go print the paths wey e try, like the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. You fit run the playbook from a parent directory, because the search dey follow the playbook path, not your shell working directory. If you depend on roles_path from ansible.cfg, confirm say you load that file with ansible --version, because Ansible go ignore a world writable working directory.
I need ansible-galaxy init before I fit create role?
No. Role na only directories wey get the expected names, so mkdir -p roles/common/tasks plus tasks/main.yml already be a working role. ansible-galaxy init --init-path roles common dey save typing and give you the complete skeleton, including meta/main.yml and a README stub. Delete directories wey you leave empty, because empty vars/main.yml fit hide which files inside the role dey actually do something.