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

Source: https://ai.techclick.in/blog_ansible_roles_best_practices
Markdown: https://ai.techclick.in/blog_ansible_roles_best_practices.md
Publisher: Techclick Infosec Pvt Ltd

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.

Ansible Roles &amp; Best Practices: — From Copy-Paste Tasks to Reusable Automation student learning map
                     A visual study map for Ansible Roles &amp; Best Practices: — From Copy-Paste Tasks to Reusable Automation showing learning path, evidence, traps, and practice sequence.

                     TECHCLICK STUDY MAP
                     Ansible Roles &amp; Best Practices: — From Copy-Paste...
                     Ansible · learn the flow, prove with evidence, avoid unsafe shortcuts

   1. Start
   🎯 By the end you will be able to

   2. Understand
   Pick where you want to start

   3. Prove
   ① Why roles — copy-paste tasks...

   4. Practice
   ② Inside a role — the folders,...

                     How to use this page
                     First build the mental model, then connect the concept to a realistic production decision. Finish by testing yourself.
                     Techclick Infosec Pvt Ltd | ai.techclick.in | Training Contact: WhatsApp +91 92772 29456

             Content-specific feature visual for this lesson: use it as the 60-second map before reading the full detail.

             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

 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 / drift   role / reusable   defaults / knobs   key insight   idempotent / 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?

    a) He edits the task once in the role, and every play that includes it gets the change     b) Ansible automatically runs the play faster     c) The block disappears from version control     d) He no longer needs an inventory
  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 &amp; 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.

   Reveal answer   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 vars  →  role vars  (high) →  role parameters  →  extra-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

 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.yml entry point — what the role does defaults/main.yml lowest precedence — the knobs vars/main.yml higher precedence — internal, don't touch handlers/main.yml notify → restart ntp / snmp templates/ Jinja2 .j2 — banner, snmp.conf files/ static files for copy module meta/main.yml dependencies + Galaxy info Ansible wires these by convention — no paths to write users override defaults/, never vars/ copy-paste / drift role / reusable defaults / knobs key insight idempotent / 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 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 override put 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 / drift role / reusable defaults / knobs key insight idempotent / 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" } ram@netauto:~/playbooks$ ansible-galaxy role init base_net , then tree roles/base_net . (Recreated for clarity — your terminal matches this.)"> 🖥️ 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? a) It's in defaults/main.yml; move it to vars/main.yml b) It's in tasks/main.yml; move it to handlers/main.yml c) It's in vars/main.yml (high precedence, beats group_vars); move it to defaults/main.yml d) It's in meta/main.yml; move it to templates/ 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. Reveal answer 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? a) Add changed_when: false so it never reports changed b) Replace it with ansible.builtin.service / handler that restarts only when notified — the idempotent module c) Add ignore_errors: true to silence the warning d) Move the task into vars/main.yml 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. Reveal answer 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 vars defaults/main.yml ntp=10.0.0.123, but group_vars/mumbai sets ntp= 10.10.1.123 → wins ▼ ② Run tasks named 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 handler end of play: Restart ntp fires once — only because the config task changed Press Play to step through the healthy path. Then press Break it . ▶ Play Next ▶ ⚠ Break it ↺ Reset 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 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 role ansible-galaxy role init base_net creates tasks/handlers/defaults/… no extra package needed Layout tasks · handlers · templates · files defaults · vars · meta auto-loaded by convention defaults vs vars defaults/ = lowest, user knobs vars/ = high, internal constants override via group_vars Handlers task: notify: Restart ntp runs once, at end, only if changed use for restarts, not in-line Best-practice checklist name every task · single-purpose role module > shell · tag tasks command → set changed_when Lint it ansible-lint roles/base_net catches no-name, no-changed-when run 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 RCE via src field in meta/requirements.yml trusted sources + patch ansible-core First commands ansible-galaxy install -r requirements.yml ansible-playbook site.yml --check ansible-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? a) Putting all variables in vars/main.yml so nothing changes b) Using raw shell commands so you control everything c) Idempotent, named tasks (modules over shell, changed_when on command) so a re-run shows changed=0 and reads clearly d) Skipping ansible-lint to save time in CI 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. defaults/ vs vars/ — how do I never mix these up? When do I write my own role vs pull one from Galaxy? Why use a handler instead of just restarting the service in the task? My command task always shows 'changed' — what's the clean fix? What's the difference between a role and a collection? How much of the RHCE EX294 does this lesson cover? 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? a) vars/main.yml b) tasks/main.yml c) defaults/main.yml d) meta/main.yml 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? a) ansible-galaxy role init base_net b) ansible-playbook init base_net c) ansible-lint --create base_net d) ansible base_net --new-role 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? a) ignore_errors: true b) changed_when: false c) become: true d) delegate_to: localhost 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? a) The variable is also defined in the role's vars/main.yml, which is higher precedence than inventory group_vars and silently wins b) group_vars files are never read by roles c) The role wasn't scaffolded with ansible-galaxy d) ansible-lint deleted the override 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? a) Both break, because requirements.yml ignores versions b) Team B breaks: with no version pinned, ansible-galaxy installs the latest (now 4.0) and inherits the breaking change; Team A stays on 3.3.0 c) Team A breaks, because pinning forces the newest version d) Neither installs at all without a version 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? a) A — tidiness is what teams care about most b) B — it names the properties (reuse, clean override surface, idempotency, pinned deps) that make a role safe to re-run and adopt across teams, not just neater c) A — the other points are optional polish d) Identical — both say the same thing 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. Submit all answers Try again 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. Compare with expert answer 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. Generate my one-liner 📩 Quiz me on this in 7 days. Opt in and we'll email 3 micro-questions on Roles & Best Practices at Day 1, Day 7 and Day 30 — spaced repetition is how this sticks. Un-tick any time. ### 📖 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 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

- 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

- 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/

- 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

- 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/

- 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.

                 Next · Ansible Inventory & Dynamic Inventory: Hosts That Never Go Stale →
                 Practice on exam.techclick.in →

---
Cite this Techclick lesson with the source URL. Do not invent fees, batch dates, or job guarantees.
Browse all lessons: https://ai.techclick.in/blogs
AI index: https://ai.techclick.in/llms.txt
