Ansible playbook or role: which one you suppose use?
Know when flat Ansible playbook enough and when role make sense, with role layout, ansible-galaxy init, role calls, and variable precedence explained.
Ansible playbook vs role: wetin be di difference
Ansible playbook na file wey you run with ansible-playbook. E dey map one group of hosts to di work wey dem need. Ansible role na directory wey get fixed layout. E dey hold tasks, templates, handlers and default variables, and playbook dey call am by name. Di task syntax inside both na di same, so dis no be question of wetin you fit express. Na question of reuse.
Start with flat playbook. One site.yml wey hold tasks: list na di correct shape for your first automation, and e go remain correct pass wetin most people expect. Convert am to role when di same block of tasks need run for another group of hosts, or when di file don pass roughly 100 lines and you no fit find task again by scrolling.
If you never write one yet, start with your first playbook against one VPS and come back when e start grow.
When flat playbook na di right answer
Flat playbook correct when na one-time work, or na work for one host, or nobody else go read am. If you dey provision one application server, or patch one box before maintenance window, none of these work need directory tree. Role dey add seven directories and one extra layer wey you need pass through. If na only the playbook wey dey beside am go call am, that extra layer no give you anything. E only mean say you go jump one more time whenever you wan read wetin actually dey run.
Flat playbook stop to be the right choice for one clear moment, and e easy to notice. You copy one block of tasks enter another playbook. That copy na the signal. From that point, every fix go need happen twice, and one day you go make the fix for only one place.
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. E 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, and 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, you reference both with bare filename and no path, because Ansible first dey 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 look for files for these exact paths, so if you put template for roles/common/template/ (singular), Ansible simply no go find am.
Build the common role with ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonE write the complete skeleton under roles/common, including directories wey you no go use and main.yml stubs wey get only ---. 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 really matter.
Now fill the files wey dey do the work. Start with defaults, because dem na 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 quotes 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 go fail only when something actually change the template. Na why the problem normally 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 of 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 so one role fit serve two groups of hosts:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordOne ordering rule dey here wey 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:, the roles still go run first. So if something suppose happen before a role, put am 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 dey static. Ansible dey read the role for parse time, and the role tasks 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 dey dynamic. Nothing go read until the task runs, and na this dey allow you drive the role name with variable or loop. 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. Write when: common_packages | length > 0 for the include, and 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 go win: defaults, group_vars, vars, extra vars
Ansible document more than twenty levels of variable precedence. Four of dem settle almost every real argument, and na dem be these from weakest to strongest.
roles/<name>/defaults/main.ymldey near the bottom. Almost anything wey you set for 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 belong, and dem go cleanly override role defaults.roles/<name>/vars/main.ymldey abovehost_vars. Any value wey you put here no fit inventory 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 in about one minute. Give one 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 the role default but lose to the 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 go keep: e silently outrank every decision wey repository consider.
The working rule be this: if you want make people fit set one value, put am for defaults/. If you put am for vars/, you dey tell 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
Ansible run wey you fit trust go produce the same result the second time, and e go report say nothing change. 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 complete. changed=2 for 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 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 its result carry the message skipped, since /tmp/guarded.txt exists, because creates give the module one visible product to look for first. When command no leave that kind product, register its output and decide by yourself with changed_when.
ansible-playbook --check --diff site.yml predict changes without making dem, and --diff print the exact lines wey template go rewrite. Read the output with one thing for mind: shell and command tasks dey skip for check mode, so plan wey look clean fit still hide work.
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. This search dey follow the playbook, no be your shell.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyThis message mean say site.yml and roles/ no dey match again, and e show the paths wey e try. Keep both for the same directory. You fit run the command from a parent directory, because na the playbook path dey matter:
ansible-playbook -i infra/inventory.ini infra/site.ymlAnother quieter version of this same problem dey. Ansible dey ignore an ansible.cfg for the current directory when everybody for the system fit write to that directory, because any user for the machine fit drop 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 concern 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 whenever run dey behave as if your config no dey.
Role sharing: requirements.yml and pinned version
Role wey another person write dey get install, e no be something wey you copy. 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 wey you run the command. So 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_rolesRoles for roles/ wey dey beside the playbook still go dey found, because that path always dey searched together with roles_path. So your own roles stay committed and reviewed, while third-party roles remain reproducible downloads pinned to a tag.
Wia roles no longer be the answer
A role na one reusable unit inside one Ansible run. E no dey create servers or DNS records for your provider, and if you try make am do that, na so playbooks turn to something wey nobody wan maintain. the work division between Ansible and Terraform good make you read am before you start. Role no replace inventory design too: 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 too. The drop-in above set only two directives, 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 own.
FAQ
Role-ke anlisa turn to role?
When na the same block of tasks suppose run for another play, or against another group of hosts. If you copy tasks between playbooks, na sign say you need role, because from that point every fix suppose apply twice, and one day you go apply am only once. One playbook wey get roughly less than 100 lines 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 runs pre_tasks, then everything wey dey under roles:, then tasks:, then post_tasks:. E no follow the order wey those keys appear for your file. If you write tasks: above roles:, e no mean say those tasks go run first. If something must happen before a 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 instead of typo, run am once with -e name=value, wey get higher precedence than every other source.
Why Ansible dey say e no find the role?
The search starts 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 follows the playbook path, not your shell working directory. If you depend on roles_path from ansible.cfg, confirm say the file load with ansible --version, because Ansible go ignore am if the working directory dey writable by everybody.
I need ansible-galaxy init to create role?
No. Role na only directories wey get the expected names, so mkdir -p roles/common/tasks plus a tasks/main.yml don already make working role. ansible-galaxy init --init-path roles common saves typing and gives you the full skeleton, including meta/main.yml and a README stub. Delete directories wey you leave empty, because empty vars/main.yml fit hide which files for the role actually dey do anything.