TTechclick ⚡ XP 0% All lessons
Ansible · Structure · Roles & Best PracticesInteractive · L1 / L2 / L3

Ansible Roles & Best Practices: — From Copy-Paste Tasks to Reusable Automation

You have one 600-line playbook with the same install-and-restart block pasted three times, and a change means editing it in three places. Roles fix that: package tasks, handlers, templates and overridable defaults into one folder you reuse everywhere and pull from Galaxy. This lesson turns your copy-paste habit into shareable, RHCE-grade automation.

📅 2026-06-11 · ⏱ 13 min · 3 live demos · 4 infographics · 🏷 10-Q assessment + AI Tutor inline

⚡ Quick Answer

Ansible roles and best practices for L1/L2 engineers and the RHCE EX294: the role directory layout, defaults vs vars precedence, handlers, ansible-galaxy role init, naming tasks, changed_when, ansible-lint and pinning versions in requirements.yml.

🎯 By the end you will be able to

Read as:

Pick where you want to start

1

Why roles

Copy-paste doesn't scale; a role packages and shares.

2

Inside a role

Seven folders, defaults vs vars, precedence.

3

Best practices

Name, lint, tag, changed_when, pin versions.

4

A real role

Base-network role: NTP, banner, SNMP, AAA, tagged.

🧠 Warm-up — 3 questions, no score

Just notice which ones make you pause. We answer all three inside the lesson.

1. You have the same 'install + configure + restart' block pasted for web, app and db servers. One config change is needed. What's the cleanest fix?

Answered in Why roles.

2. Where in a role do you put a variable you WANT users to override per site (like an NTP server)?

Answered in Best practices.

3. A command task runs a 'show' command that never changes anything, but Ansible always reports it as 'changed'. What fixes that?

Answered in Inside a role.

Most engineers think…

Most engineers first hear "role" and think it's just "a folder to tidy my tasks into" — cosmetic housekeeping you do once the playbook gets long.

Wrong — and that mindset stops you using the part that matters. A role is a reusable, versioned unit with a contract: defaults/ are the knobs users override, vars/ are internal constants, handlers/ fire restarts only on change, meta/ declares dependencies, and the whole thing installs from Galaxy with a pinned version. Tidiness is a side effect; reuse, a clean override surface, and a shareable artifact are the actual win — and they're exactly what the RHCE exam tests.

① Why roles — copy-paste tasks don't scale or share

Picture Sneha, an L1 automation engineer at Infosys. She started with one tidy playbook to configure a router: set NTP, push a login banner, enable SNMP. It worked. Then she needed the same on app servers, then db servers, so she copy-pasted the block. Six months later her site.yml is 600 lines, the same three tasks appear in four places, and when the security team changes the SNMP community string she has to find and edit it in every copy — and inevitably misses one. That missed copy is now configuration drift.

This is the wall every beginner hits. A long playbook with repeated blocks has three problems baked in: a single change means editing many places (and missing some), you can't lift the logic into the next project without copy-pasting again, and there's no clean way for someone else to tweak one setting without rewriting your tasks. The fix isn't discipline — it's structure.

A role packages all the moving parts of one job — tasks + handlers + templates + defaults + vars + files + meta — into a single folder with a fixed layout. You write the logic once, then include the role from any playbook in any project. Better still, the community publishes roles on Ansible Galaxy, so for common jobs (NTP, nginx, Docker) you often don't write a role at all — you pull a battle-tested one and just override its knobs.

👉 So far: copy-paste playbooks drift and can't be shared. A role packages one job into a reusable folder. Next: the exact directory layout and how to scaffold it in one command.

You don't hand-create those folders. The bundled ansible-galaxy tool scaffolds the whole skeleton for you: ansible-galaxy role init base_net creates roles/base_net/ with tasks/, handlers/, defaults/, vars/, templates/, files/, meta/ and a starter main.yml in each, plus a README.md. From there you fill in only the folders you actually use — empty ones can be deleted. That convention is the whole point: Ansible knows where to look, so you never write file paths for handlers, templates or defaults.

Figure 1 — One giant playbook vs one role
One giant copy-pasted playbook drifts site by site; a role is one reusable unit you share everywhere A two-column comparison for the same automation job. Left, in red, is a single 600-line site.yml where the same install-and-configure tasks are copy-pasted for web, app and db servers; one edit must be repeated three times and the copies drift out of sync. Right, in blue and green, is a role: tasks, handlers, templates, defaults, vars, files and meta packaged into one folder that every project includes and that can be pulled from Ansible Galaxy. Red marks copy-paste and drift; blue marks the reusable role; green marks the shared, single-source-of-truth outcome. One giant playbook vs one role — same job, very different to maintain site.yml — copy-pasted tasks # web block: install + template + restart # app block: same tasks, pasted again # db block: same tasks, pasted a 3rd time ✗ one change = edit it in 3 places ✗ copies drift; can't share to next project ✗ no defaults a user can override cleanly 600 lines no one wants to touch roles/base_net/ — one reusable unit tasks/ handlers/ templates/ defaults/ vars/ files/ · meta/ ↑ the knobs users override ✓ include it from any playbook ✓ ansible-galaxy install pulls it from Galaxy edit once → every site gets the fix single source of truth A role turns copy-paste into a unit you reuse, version and share copy-paste / driftrole / reusabledefaults / knobskey insightidempotent / OK
Read both columns for the same job. Left (red) = the same tasks copy-pasted per server type, so one change means three edits and the copies drift. Right (blue/green) = one role folder you include everywhere and pull from Galaxy — edit once, every site gets the fix.

Why a role beats a long playbook — one tap each

Tap each card. These four are the reasons every team eventually moves repeated tasks into roles.

♻️
Reuse
tap to flip

Write the logic once, include it from any playbook or project. So: no more pasting the same block per server type.

🎚️
Clean override surface
tap to flip

defaults/ exposes knobs users change without touching your tasks. So: one role, many sites, zero forks.

📦
Shareable artifact
tap to flip

Push to Galaxy or a Git repo, install with a pinned version. So: the whole team and CI use the exact same role.

🧭
Convention wiring
tap to flip

Ansible auto-loads tasks/handlers/templates by folder name. So: no file paths to write, no glue to forget.

Daily-life analogy — the dabbawala tiffin system, not a fresh lunchbox each day

Copy-pasting tasks is like packing a brand-new tiffin from scratch every morning and hoping you remember every item. A role is the Mumbai dabbawala system: a standard, colour-coded box with a fixed layout everyone understands, filled once and reused on every route. Change the menu in one place and every box on every train follows the same plan. The structure is what makes it repeatable at scale — exactly like a role's fixed folder layout.

Quick check · Q1 of 10

Rahul at TCS has the same 'install ntp + push config + restart' block pasted across web, app and db plays. The NTP server IP must change. What does moving it into a role buy him most directly?

Correct: a. The whole point of a role is single-source reuse: the tasks live once in roles/base_net/tasks/main.yml, and every play that includes the role picks up the edit. Roles don't change run speed, they don't remove anything from Git (they add a folder), and you still need an inventory to know which hosts to target.

Pause & Predict

Predict: you scaffold a role with ansible-galaxy role init but only ever put content in tasks/ and defaults/. What happens to the empty templates/ and files/ folders when you run the role? Type your guess.

Answer: Nothing — they're harmless. Ansible only loads a directory's main.yml or referenced files if they exist; empty role folders are simply ignored, and none of them are strictly required. Many teams delete the unused folders to keep the role tidy. The layout is a convention Ansible looks for, not a contract you must fully fill.

② Inside a role — the folders, defaults vs vars, and precedence

Open any role and you meet the same folders. tasks/main.yml is the entry point — the list of tasks Ansible runs when the role is invoked. handlers/main.yml holds handlers (the restart-style tasks fired by notify). templates/ holds Jinja2 .j2 files that the template module renders; files/ holds static files the copy module ships as-is. meta/main.yml declares the role's dependencies and its Galaxy metadata.

The two folders people confuse are defaults/main.yml and vars/main.yml — and getting them right is the single most useful thing in this lesson. defaults/ holds the role's knobs: sensible default values that any user can override from inventory, a playbook, or the command line, because they sit at the very bottom of the precedence ladder. vars/ holds internal constants the role needs but users shouldn't change — they sit high in precedence, so they're hard to override on purpose.

👉 So far: seven folders, each with one job, and the defaults-vs-vars split. Next: where each one lands on Ansible's precedence ladder, so you know which value actually wins.

Ansible resolves a variable by walking a precedence order of about 22 levels. You don't memorise all 22 — you anchor the four that matter for roles. From weakest to strongest: role defaults (the floor) → inventory group_vars / host_vars (how each site overrides) → play varsrole vars (high) → role parametersextra-vars with -e (always wins). So a value in defaults/main.yml is beaten by almost anything, which is exactly why your overridable knobs go there; a value in vars/main.yml beats inventory, which is why user knobs must not go there.

Figure 2 — The role layout, wired by convention
Inside a role: seven folders, each with one job, that Ansible auto-wires by convention A diagram of the standard role layout produced by ansible-galaxy role init. A central role folder fans out to seven directories. tasks main.yml is the entry point that Ansible runs. defaults main.yml holds the lowest-precedence variables, the user-facing knobs. vars main.yml holds higher-precedence internal variables. handlers main.yml holds restart-style handlers fired by notify. templates holds Jinja2 dot j2 files used by the template module. files holds static files used by copy. meta main.yml declares dependencies and Galaxy metadata. Amber marks defaults as the override point; blue marks the reusable role parts. Inside roles/base_net/ — each folder has exactly one job roles/base_net/one folder = one reusable role tasks/main.ymlentry point — what the role does defaults/main.ymllowest precedence — the knobs vars/main.ymlhigher precedence — internal, don't touch handlers/main.ymlnotify → restart ntp / snmp templates/Jinja2 .j2 — banner, snmp.conf files/static files for copy module meta/main.ymldependencies + Galaxy info Ansible wires theseby convention — no paths to write users override defaults/, never vars/ copy-paste / driftrole / reusabledefaults / knobskey insightidempotent / OK
Look at the fan-out: tasks/ is the entry point, defaults/ (amber) is the override point, vars/ is internal, handlers/ fire on notify, templates/ and files/ feed the template and copy modules, and meta/ declares dependencies. Ansible loads all of these by folder name — you never write the paths.
Figure 3 — Variable precedence — where a value wins
Role defaults sit at the bottom of the precedence ladder so anyone can override them; extra-vars always win A vertical precedence ladder for Ansible variables, lowest at the bottom, highest at the top. From the bottom: role defaults are the weakest. Above them sit inventory group_vars and host_vars, then play vars, then role vars from vars main.yml which is much stronger, then set_fact and registered vars, then role parameters, and at the very top extra-vars passed with dash e on the command line, which always win. Amber marks the role defaults override point; red marks role vars as the hard-to-override trap; lime marks the key insight that defaults are meant to be overridden. Variable precedence — where a value wins (low → high) ▲ stronger — wins ▼ weaker — easily overridden extra-vars (ansible-playbook -e key=val) — ALWAYS wins role / include parameters (passed as vars: on the role) set_fact / registered vars · include_vars role vars (roles/x/vars/main.yml) — high & hard to overrideput internal constants here, NOT user knobs play vars / vars_files / vars_prompt host facts / cached set_fact inventory host_vars / group_vars (per-site overrides)this is how each site overrides the role's defaults role defaults (roles/x/defaults/main.yml)LOWEST precedence — built to be overridden by anyone above copy-paste / driftrole / reusabledefaults / knobskey insightidempotent / OK
Read bottom to top. Role defaults (amber) are the lowest, so they're the override surface. Inventory group_vars/host_vars (lime) is where each site overrides them. Role vars (red) sit high — never put user knobs there. extra-vars (-e) always wins.
roles/base_net/defaults/main.yml — the knobs a user overrides
# Lowest precedence — sensible defaults, overridden per site
ntp_server: 10.0.0.123
snmp_community: techclick-ro
banner_text: "Authorized access only."
syslog_host: 172.16.5.10
Expected output
# (data file — no output. Confirm Ansible sees it:)
$ ansible -m debug -a "var=ntp_server" localhost
localhost | SUCCESS => {
    "ntp_server": "10.0.0.123"
}
🖥️ This is what ansible-galaxy scaffolds for you — run it in your project root: ram@netauto:~/playbooks$ ansible-galaxy role init base_net, then tree roles/base_net. (Recreated for clarity — your terminal matches this.)
ram@netauto:~/playbooks
1
$ command
ansible-galaxy role init base_net
2
result
- Role base_net was created successfully
3
$ tree roles/base_net
tasks/ handlers/ defaults/ vars/
more folders
templates/ files/ meta/ README.md
4
each holds
main.yml (starter file)
▶ run
Common mistake — "I set the variable in group_vars but the role ignores it"

Symptom: an engineer puts the override (say snmp_community) in group_vars/mumbai.yml, but the role keeps using its built-in value. Cause: the same variable was defined in the role's vars/main.yml, which sits above inventory in precedence — so vars/ wins and your group_vars override is silently ignored. Fix: move user-overridable knobs out of vars/main.yml and into defaults/main.yml (the lowest precedence). Reserve vars/ strictly for internal constants the role owns.

Quick check · Q2 of 10

Aditya wants every site to be able to override the SNMP community string from its group_vars file, but the role keeps using its own value. Where did he most likely put snmp_community, and where should it go?

Correct: c. vars/main.yml is high precedence and sits above inventory group_vars, so it silently wins and blocks the override. User-overridable knobs belong in defaults/main.yml, the lowest precedence, which inventory group_vars can beat. The other options confuse folders that hold tasks, handlers or metadata, not variables.

Pause & Predict

Predict: you define ntp_server in defaults/main.yml AND in group_vars/all.yml AND pass -e ntp_server=... on the command line. Which value does the play actually use? Type your guess.

Answer: The -e (extra-vars) value. Extra-vars sit at the very top of the precedence ladder and always win — nothing in defaults, vars, inventory or play vars can override them. Below that, group_vars/all beats role defaults. So the order here is: command-line -e > group_vars/all > defaults/main.yml. That's also why -e is great for a one-off run but dangerous as a permanent setting — it overrides everything.

③ Best practices — name, lint, tag, and pin

A role that works is not the same as a role that's good. The habits below are what separate a junior's first role from one a team will actually adopt — and they're exactly the things ansible-lint and the RHCE exam check. Start with the cheapest, highest-value one: name every task. An unnamed task shows up as an opaque TASK [command] line in the output; a named task reads like documentation and is what ansible-lint's name[missing] rule enforces.

Use handlers for restarts. Don't restart a service inline after every change — notify a handler instead, so the restart runs once, at the end of the play, and only if something actually changed. Keep each role single-purpose. A role called base_net sets baseline network config; it shouldn't also deploy your app. Small, focused roles compose; a do-everything role can't be reused. Tag your tasks so an operator can run just one part: ansible-playbook site.yml --tags ntp re-applies only the NTP tasks during a 2 a.m. change window instead of the whole role.

Prefer real modules over command/shell. Modules like ansible.builtin.lineinfile or ansible.builtin.service are idempotent out of the box: they only change things when needed and report ok otherwise. A raw command or shell task can't know whether it changed anything, so Ansible flags it changed every run — which breaks idempotency and trips the no-changed-when lint rule. When you genuinely must use command, tell Ansible the truth with changed_when (and failed_when for what counts as failure).

👉 So far: name, handlers, single-purpose, tags, modules-over-shell, changed_when. Next: lint it automatically, then distribute and pin it so every machine gets the same version.

Lint with ansible-lint. It's a free command-line tool that checks your role against a default ruleset — missing names, non-idempotent commands, deprecated modules, risky shell usage — and you wire it into CI so problems are caught before merge, not in production. Distribute via collections and roles. A collection bundles roles + modules + plugins under a namespace (e.g. cisco.ios); you list both roles and collections in a requirements.yml and install them with ansible-galaxy install -r requirements.yml.

Pin versions in requirements.yml. Installing a role without a version is like running apt install with no version — fine today, broken six months from now when an upstream change lands. Lock the exact version: for every role and collection so your automation is reproducible on any machine at any time. This isn't just stability — it's security, as the next callout shows.

roles/base_net/tasks/main.yml — named, tagged, idempotent, changed_when on command
---
- name: Set the NTP server
  ansible.builtin.template:
    src: ntp.conf.j2
    dest: /etc/ntp.conf
  notify: Restart ntp
  tags: [ntp]

- name: Push the login banner
  ansible.builtin.copy:
    src: banner.txt
    dest: /etc/issue.net
  tags: [banner]

- name: Confirm NTP is in sync (read-only check, never a change)
  ansible.builtin.command: ntpq -p
  register: ntp_status
  changed_when: false
  failed_when: "'no association' in ntp_status.stdout"
  tags: [ntp, verify]
Expected output
TASK [base_net : Set the NTP server] *******************
changed: [br-mumbai-rtr]
TASK [base_net : Push the login banner] ****************
ok: [br-mumbai-rtr]
TASK [base_net : Confirm NTP is in sync ...] ***********
ok: [br-mumbai-rtr]
RUNNING HANDLER [base_net : Restart ntp] ***************
changed: [br-mumbai-rtr]
ansible-lint — catch missing names and non-idempotent commands before merge
ram@netauto:~/playbooks$ ansible-lint roles/base_net
Expected output
WARNING  Listing 1 violation(s) that are fatal
no-changed-when: Commands should not change things if nothing needs doing
roles/base_net/tasks/main.yml:14 Task/Handler: Restart snmpd

Read documentation for instructions on how to ignore specific rule violations.
Failed: 1 failure(s), 0 warning(s) on 12 files.
Recent CVE — why "only install roles from trusted sources" is not boilerplate

Symptom: a teammate runs ansible-galaxy role install from an untrusted Git URL and the control node is quietly compromised. CVE-2026-11332 (CVSS 7.8, High, June 2026) is an argument-injection flaw: the src field in a role's meta/requirements.yml isn't sanitised, so a malicious role can inject git config flags (like -c) and run code during the clone. Fix: patch ansible-core (fixed in 2.16.19, 2.18.18, 2.19.11, 2.20.7, 2.21.1), install roles only from sources you trust, pin exact versions in requirements.yml, and run installs in an isolated build container. Your control node runs everything — treat it as crown-jewel infrastructure.

Quick check · Q3 of 10

Priya's role has a task: ansible.builtin.command: systemctl restart snmpd. ansible-lint flags it and every run shows 'changed'. What's the best fix?

Correct: b. The right fix is the idempotent module: use ansible.builtin.service (or notify a handler) so the restart happens only when a config task actually changed. changed_when: false would lie — a real restart IS a change — and it doesn't make the action conditional. ignore_errors hides problems, and vars/main.yml holds variables, not tasks.

Pause & Predict

Predict: two engineers run the same playbook from requirements.yml, but one wrote 'name: geerlingguy.ntp' with no version and the other pinned 'version: 3.3.0'. Six months later an upstream 4.0 ships with a breaking change. Whose run breaks? Type your guess.

Answer: The one with no version. ansible-galaxy installs the latest available role when no version is pinned, so the unpinned engineer silently jumps to 4.0 and inherits the breaking change. The pinned engineer keeps getting exactly 3.3.0 — reproducible and predictable — until they deliberately bump the version. Pinning is what makes 'works on my machine' also true on the CI runner and six months later.

④ A real role — base network config, overridden per site

Let's build the role Sneha actually needed: a single-purpose base_net role that applies baseline config to every network device — NTP, a login banner, SNMP, and AAA — and that each site can tune via group_vars without forking the role. This is the shape of question the RHCE EX294 puts in front of you, and the pattern you'll use on day one of a support desk.

The role exposes its knobs in defaults/main.yml (NTP server, SNMP community, banner text, syslog host — the file you saw in section 2). The Mumbai site overrides two of them in group_vars/mumbai.yml; the Pune site overrides different ones in group_vars/pune.yml. Same role, two behaviours, zero copy-paste. Each task is named, tagged, uses a module (so it's idempotent), and the one command verify task carries changed_when: false. Restarts go through a handler.

👉 So far: the role's shape and per-site overrides. Next: the per-site group_vars, the playbook that includes the role, a real run, the gotchas to expect, and the cheat-sheet.
group_vars/mumbai.yml — Mumbai overrides the defaults for its site
# These beat roles/base_net/defaults/main.yml (inventory > role defaults)
ntp_server: 10.10.1.123
snmp_community: mumbai-ro
syslog_host: 10.10.1.10
# banner_text not set here → role default is used
Expected output
# (data file — confirm the effective value on a Mumbai host:)
$ ansible br-mumbai-rtr -m debug -a "var=ntp_server"
br-mumbai-rtr | SUCCESS => {
    "ntp_server": "10.10.1.123"
}
site.yml — include the role against the device group
---
- name: Apply baseline network config to all edge routers
  hosts: edge_routers
  gather_facts: false
  roles:
    - role: base_net
      tags: [base_net]
Expected output
PLAY [Apply baseline network config to all edge routers] *
TASK [base_net : Set the NTP server] *********************
changed: [br-mumbai-rtr]
ok: [br-pune-rtr]
TASK [base_net : Configure SNMP] ************************
changed: [br-mumbai-rtr]
changed: [br-pune-rtr]

▶ Watch the role run against the Mumbai router

Sneha runs site.yml. Follow how Ansible resolves the variables, runs the named tasks idempotently, and fires the handler only on a real change. Press Play for the healthy path, then Break it to see the failure.

① Resolve varsdefaults/main.yml ntp=10.0.0.123, but group_vars/mumbai sets ntp=10.10.1.123 → wins
② Run tasksnamed tasks run via modules; NTP config differs → task reports changed, notify Restart ntp queued
③ Verify (read-only)command: ntpq -p runs with changed_when: false → reports ok, not changed
④ Flush handlerend of play: Restart ntp fires once — only because the config task changed
Press Play to step through the healthy path. Then press Break it.

Karthik at HCL faces this

Karthik, an L1 engineer, runs the base_net role and the play succeeds — but his per-site override in group_vars/pune.yml is ignored: every Pune router still gets the role's default SNMP community, not the Pune one.

Likely cause

The role author put snmp_community in roles/base_net/vars/main.yml (high precedence) instead of defaults/main.yml (lowest). vars/ sits above inventory group_vars in the precedence ladder, so the role's internal value silently wins and the group_vars override never takes effect.

Diagnosis

He proves it's a precedence issue, not a typo: he prints the effective value on a Pune host and sees the role's value, not the group_vars one — then greps the role to find the variable defined in vars/main.yml.

ansible br-pune-rtr -m debug -a "var=snmp_community" → grep -r snmp_community roles/base_net/
Fix

Move snmp_community (and any other user-overridable knob) out of roles/base_net/vars/main.yml into roles/base_net/defaults/main.yml. Keep vars/main.yml only for internal constants the role owns and users should never change.

Verify

Re-run: ansible br-pune-rtr -m debug -a "var=snmp_community" now shows the Pune value from group_vars, and the play applies the correct community per site.

Prove the role is idempotent before you call it done

Run the playbook a second time and read the PLAY RECAP: a correct, idempotent role shows changed=0 on the re-run (everything ok, nothing changed) because the modules saw the desired state already matched. If you still see changed>0 on an unchanged system, you have a non-idempotent task — almost always a raw command/shell without changed_when, or a module being mis-used. Also dry-run first with ansible-playbook site.yml --check to preview changes without touching anything.

Figure 4 — Roles & best practices — the cheat-sheet
Ansible roles on one card — layout, the knobs vs internal split, the best-practice checklist and first commands A nine-tile cheat sheet for roles and best practices. Tiles cover the ansible-galaxy role init command, the directory layout, defaults versus vars, handlers and notify, the best-practices checklist of naming tasks and using changed_when, ansible-lint, requirements.yml version pinning, the CVE-2026-11332 supply-chain warning, and the first commands to run a role. Each tile has a short role line. Ansible roles & best practices — your one-glance card Scaffold a roleansible-galaxy role init base_netcreates tasks/handlers/defaults/…no extra package needed Layouttasks · handlers · templates · filesdefaults · vars · metaauto-loaded by convention defaults vs varsdefaults/ = lowest, user knobsvars/ = high, internal constantsoverride via group_vars Handlerstask: notify: Restart ntpruns once, at end, only if changeduse for restarts, not in-line Best-practice checklistname every task · single-purpose rolemodule > shell · tag taskscommand → set changed_when Lint itansible-lint roles/base_netcatches no-name, no-changed-whenrun in CI before merge Pin in requirements.yml- name: geerlingguy.ntp version: "3.3.0"exact version = reproducible Supply-chain ⚠CVE-2026-11332 — role install RCEvia src field in meta/requirements.ymltrusted sources + patch ansible-core First commandsansible-galaxy install -r requirements.ymlansible-playbook site.yml --checkansible-playbook site.yml --tags ntp
Your one-card map of this whole lesson — scaffold command, layout, defaults vs vars, handlers, the best-practice checklist, ansible-lint, requirements.yml pinning, the CVE warning, and the first commands. Keep it open in your first week and before any RHCE attempt.

For the cert path, this lesson maps straight onto the RHCE EX294 blueprint. "Create and use roles" and "Install roles and use them in playbooks" are explicit objectives, and the modern exam expects you to scaffold with ansible-galaxy, override variables cleanly, and write idempotent, named, tagged tasks — exactly what you just did. The 2026 exam also leans on the newer toolset (ansible-navigator and execution environments) for running and inspecting content, but the role structure itself is unchanged. Nail roles and a sizeable chunk of the practical exam is muscle memory.

Daily-life analogy — Aadhaar e-KYC form fields vs filled values

Think of defaults/main.yml as the blank Aadhaar e-KYC form with default placeholder values, and group_vars/mumbai.yml as the actual details you fill in at your local centre. The form (role) is the same everywhere; what you write on it (group_vars) makes it yours. And just as you'd never let the form's pre-printed defaults overwrite the details you typed, you never put user values in vars/main.yml — that would be like the form ignoring what you wrote.

Next: Inventory & Dynamic Inventory — hosts that never go stale
Quick check · Q4 of 10

An interviewer asks Meera: "Give me the single habit that most makes an Ansible role safe to re-run and adopt across teams." Best answer?

Correct: c. Idempotency plus clear names is the core habit: idempotent tasks make re-runs safe (changed=0 on no-op) and named tasks make output and intent readable for the whole team. Putting everything in vars/ blocks overrides; raw shell breaks idempotency; skipping lint removes the safety net that enforces these habits.

🤖 Ask the AI Tutor

Tap any question — instant, scoped to this lesson. No login, no waiting.

Pre-curated from Ansible docs + community Q&A, scoped to this lesson. For a live prod issue, paste your export into chat.techclick.in.

📝 Wrap-up assessment — six more

You've answered 4 inline. Six left. 70% (7 of 10) marks the lesson complete on your profile. Tap Submit all answers at the end.

Q5 · Remember

In an Ansible role, which directory's main.yml holds the lowest-precedence variables — the user-facing knobs meant to be overridden?

Correct: c. defaults/main.yml is the lowest precedence in Ansible — the role's overridable knobs. vars/main.yml is high precedence (internal constants), tasks/main.yml holds the tasks the role runs, and meta/main.yml declares dependencies and Galaxy metadata.
Q6 · Apply

You want to scaffold a new role named base_net with the standard directory skeleton in one command. What do you run?

Correct: a. ansible-galaxy role init base_net creates roles/base_net/ with tasks, handlers, defaults, vars, templates, files and meta plus starter main.yml files. ansible-playbook runs plays (no init subcommand), ansible-lint checks content, and the last option isn't a real command.
Q7 · Apply

A role task runs ansible.builtin.command: ntpq -p only to read NTP status. Every run reports 'changed' and ansible-lint flags it. What's the correct addition?

Correct: b. A read-only command never changes state, so changed_when: false tells Ansible the truth and satisfies the no-changed-when rule. ignore_errors hides failures, become escalates privileges, and delegate_to changes where the task runs — none address the false 'changed' report.
Q8 · Analyze

An engineer sets snmp_community in group_vars/mumbai.yml, but every Mumbai host still uses the role's built-in value. Control flow and syntax are fine. Most likely root cause?

Correct: a. Role vars/main.yml sits above inventory group_vars in the precedence ladder, so a value there overrides the group_vars override. The fix is to move user knobs to defaults/main.yml (lowest precedence). group_vars are absolutely read; scaffolding tool and lint don't affect precedence at runtime.
Q9 · Analyze

Two teams install the same role from requirements.yml. Team A pinned 'version: 3.3.0'; Team B left the version off. Upstream later ships a breaking 4.0. What happens, and why?

Correct: b. Without a pinned version, ansible-galaxy installs whatever is latest, so Team B jumps to 4.0 and gets the breaking change; the pinned Team A keeps getting 3.3.0 until they deliberately bump it. Pinning is exactly what makes installs reproducible. requirements.yml does honour versions, and a missing version installs the latest rather than failing.
Q10 · Evaluate

Two ways to describe a 'good' role to a hiring manager: (A) "it's a folder that keeps my tasks tidy"; (B) "it's a single-purpose, reusable unit with overridable defaults, idempotent named tasks, handlers for restarts, and pinned dependencies." Which is stronger and why?

Correct: b. B describes the actual engineering contract — single-purpose, overridable defaults, idempotent/named tasks, handlers, pinned versions — which is what makes a role reusable, safe to re-run (changed=0 on no-op) and adoptable, and what the RHCE and real teams test. A captures only the cosmetic side effect and misses why roles matter.
Lesson complete — saved to your profile.
Almost! You need 70% (7 of 10) — re-read the path that tripped you up and tap "Try again".

🧠 In your own words

Type one line: In one line, why does a user-overridable role variable belong in defaults/main.yml and not vars/main.yml? Then compare to the expert version.

Expert version: Because defaults/main.yml is the lowest precedence in Ansible, so inventory group_vars, play vars or -e can override it cleanly — whereas vars/main.yml is high precedence and sits above inventory, so a value there silently wins and blocks the user's override.

🗣 Teach a friend

Best way to lock it in — explain it in one line to a teammate. Tap to generate a paste-ready summary.

📖 Glossary

Role
A reusable unit packaging tasks, handlers, templates, defaults, vars, files and meta into one folder Ansible loads by convention.
ansible-galaxy role init
The bundled command that scaffolds an empty role's standard directory skeleton plus a starter main.yml in each folder.
tasks/main.yml
The role's entry point — the list of tasks Ansible runs when the role is invoked.
defaults/main.yml
The lowest-precedence variables in Ansible — the role's user-facing knobs, meant to be overridden from inventory or the command line.
vars/main.yml
High-precedence internal variables for the role's own use; sits above inventory, so keep user-overridable knobs out of here.
handlers/main.yml
Restart-style tasks fired by notify; they run once, at the end of the play, and only if a task reported changed.
meta/main.yml
Declares the role's dependencies (run before the role) and its Ansible Galaxy metadata.
Variable precedence
The ~22-level order Ansible uses to decide which value wins; role defaults are the floor, extra-vars (-e) always win.
Idempotent
A task safe to run repeatedly — it changes state only when needed and reports 'ok' otherwise; the goal of every good role.
changed_when / failed_when
Conditionals that tell Ansible when a command task should count as changed or failed — needed because raw commands can't know on their own.
ansible-lint
A command-line tool that checks roles/playbooks against best-practice rules (missing names, non-idempotent commands, deprecated modules).
requirements.yml
A file declaring external role and collection dependencies with pinned versions; ansible-galaxy install -r reads it.
Collection
A namespaced package bundling roles, modules and plugins (e.g. cisco.ios, community.general), distributed and versioned via Galaxy.

📚 Sources

  1. Ansible Community Documentation — "Roles" (the standard role directory structure: tasks/handlers/defaults/vars/templates/files/meta; ansible-galaxy role init; using roles via the roles keyword, include_role and import_role; dependencies in meta/main.yml; tags applied to all tasks in a role; argument_specs validation). docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_reuse_roles.html
  2. Ansible Community Documentation — "Using variables: variable precedence" (the ~22-level precedence order; role defaults are lowest, role vars are high, extra-vars with -e always win; inventory group_vars/host_vars override role defaults). docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_variables.html
  3. Ansible Lint Documentation — rules "name" (every task must be named, start with a capital) and "no-changed-when" (commands should not report changed if nothing changed; use changed_when). docs.ansible.com/projects/lint/rules/name/ · docs.ansible.com/projects/lint/rules/no-changed-when/
  4. Red Hat — "Red Hat Certified Engineer (RHCE) exam — EX294" objectives (create and use roles; install roles and use them in playbooks; manage variables; the 2026 exam's move to ansible-navigator and execution environments). redhat.com/en/services/training/ex294-red-hat-certified-engineer-rhce-exam-red-hat-enterprise-linux
  5. CVE-2026-11332 — argument-injection / RCE in 'ansible-galaxy role install' via the src field in a role's meta/requirements.yml (CVSS 7.8 High, June 2026); fixed in ansible-core 2.16.19 / 2.18.18 / 2.19.11 / 2.20.7 / 2.21.1; mitigations: install only from trusted sources, pin versions, isolate installs. threat-modeling.com/ansible-galaxy-role-install-rce-cve-2026-11332/
  6. Ansible best practices (community) — prefer modules over command/shell for idempotency, keep roles single-purpose, define user-overridable variables in defaults/ not vars/, and pin role/collection versions in requirements.yml for reproducibility. jeffgeerling.com/blog/2020/ansible-best-practices-using-project-local-collections-and-roles · github.com/ansible/ansible/issues/69388 (role params precedence)

What's next?

You can now package automation into reusable roles. But a role is only as good as the list of hosts you point it at — and hand-maintained inventory files go stale the moment someone spins up a new VM. Next we make the host list build itself.