Ansible variable precedence, drawn
Seven requirement-driven Ansible labs - inventory and config, ad-hoc administration, your first playbook, variable precedence, loops, handlers and error handling - graded on idempotence.
Canonical home: blog.overflowbyte.cloud
Week 5 of [[rhcsa-to-rhce-a-60-day-lab-curriculum|RHCSA to RHCE - A 60-Day Lab Curriculum]], and where the RHCE half properly begins. Seven labs, each stated as a requirement.
The only reference you are allowed is
ansible-doc. Not the web, not a cheat sheet. The exam gives you the installed documentation and nothing else, so build the habit now:ansible-doc -l | grep -i firewall ansible-doc ansible.builtin.user ansible-doc -s ansible.builtin.dnf # ready-to-paste snippet
ansible-doc -sis the single highest-value command in this entire week.
All work happens on the control node in ~/ansible/, against the two managed
nodes from [[building-a-three-node-rhel-lab-on-kvm|Building a Three-Node RHEL Lab on KVM]]. Checkers:
./verify 5.1 … 5.7
(the harness).
These ones run your playbooks, twice, because idempotence is the requirement
and it cannot be checked any other way.
Lab 5.1 — Make the control node work (60 min)
Requirement
~/ansible/ansible.cfgused automatically when you runansiblefrom that directory — prove Ansible is reading yours, not/etc/ansible/ansible.cfg.- An inventory defining groups
web(rhel01),db(rhel02),prod(rhel01),dev(rhel02), and a parent groupmanagedcontaining web and db. - Passwordless SSH and passwordless privilege escalation to both nodes.
- Prove connectivity with an ad-hoc ping.
- Show which host belongs to which groups without reading the file.
Acceptance criteria
-
ansible --versionreports your config file path -
ansible managed --list-hostsreturns both nodes -
ansible all -m pingreturns SUCCESS for both -
ansible all -m command -a id --becomereturns uid=0 -
ansible-inventory --graphshows the group tree
The config-file trap. Ansible ignores an
ansible.cfgin a world-writable directory, silently. If your config seems to be ignored, check the directory permissions before anything else.
Lab 5.2 — A full day of admin without a playbook (60 min)
Requirement. Using only ad-hoc commands, on all managed nodes:
- Install
httpdandfirewalld. - Start and enable both.
- Create user
webadminwith a specific UID. - Copy a file to
/etc/motdwith defined content. - Open the http service in the firewall permanently.
- Gather and display only the
ansible_distribution*facts. - Reboot one node and wait for it to return.
Acceptance criteria
- Every task done ad-hoc, no playbook file
- The second run of each command reports
ok, notchanged - You found each module with
ansible-doc, not from memory
Idempotence is the whole idea. Run every command twice. The second run must be green
ok, not orangechanged. A task that reportschangedon every run is a broken task —commandandshellare the usual culprits, which is exactly why you avoid them whenever a real module exists.
Lab 5.3 — Construct, don't copy (75 min)
Requirement. Write ~/ansible/web.yml that configures the web group as a
working web server. Do not copy this from anywhere — build it task by task,
finding each module with ansible-doc.
- Install httpd and firewalld.
- Deploy
/var/www/html/index.htmlcontaining the host's own hostname and IP, sourced from facts. - Start and enable httpd.
- Permit http through the firewall, permanently.
- Be fully idempotent.
Acceptance criteria
-
ansible-playbook --syntax-check web.ymlpasses -
ansible-playbook --check web.ymlruns clean against a configured host -
curl rhel01returns the correct hostname - Second run: zero changed tasks
- No
commandorshellmodule used anywhere
[!tip]- YAML failures that will cost you marks Tabs are illegal — spaces only.
become: trueat play level versus task level. A colon inside an unquoted value breaks the parse.state: presentandstate: latestare not the same answer.
Lab 5.4 — Same playbook, different results per host (60 min)
Requirement
group_vars/web.ymlsets the served port to 80;group_vars/db.ymlsets database-related variables.host_vars/rhel01.ymloverrides one group variable — prove which wins.- A playbook that uses
ansible_factsto install the correct package name depending on the distribution major version. - Register the output of a command and use it in a later task.
- Create a custom fact under
/etc/ansible/facts.d/on one node and consume it. - Prompt for a variable at runtime, with a sensible default.
Acceptance criteria
- Host var demonstrably beats group var
- The custom fact appears under
ansible_local - The registered variable is used, and you can print its
.stdout - You can state the precedence order from memory
[!tip]- Precedence, roughly lowest to highest role defaults →
group_vars/all→group_vars/<group>→host_vars→ play vars → task vars →-eextra vars always win.Knowing that
-ewins is a frequent exam question, usually shaped as "why is my variable being ignored".
Lab 5.5 — Stop repeating yourself (60 min)
Requirement
- Create five users from a list of dictionaries, each with their own UID, group and comment — in a single task.
- Install a list of packages in one task.
- Start a service only if the host is in the
webgroup. - Deploy a config file only when a given file does not already exist.
- Apply a task only when a registered command succeeded.
- Loop over a dictionary and print key/value pairs.
- Skip a task entirely on RHEL 8, run it on RHEL 9 and above.
Acceptance criteria
- No copy-pasted near-identical tasks anywhere
- Conditionals use facts and group membership, not hardcoded hostnames
- Skipped tasks report
skipping, not failure - The playbook is idempotent
[!tip]- Hint
loop:with a list of dicts and{{ item.name }}.when:takes a bare expression — no{{ }}around the whole condition. For the version test:when: ansible_facts['distribution_major_version'] | int >= 9.
The checker counts your user: tasks. If there is more than one, you solved it
by copy-paste and the lab is not done.
Lab 5.6 — Behave correctly when things change or fail (60 min)
Requirement
- Deploy an httpd config file; restart httpd only when the file changes, via a handler.
- Validate the config before it is put in place — a broken config must never reach the server.
- A task that is allowed to fail without stopping the play.
- A task whose failure condition you define yourself, based on its output.
- A block that runs cleanup even when an earlier task fails.
- Force handlers to run even if a later task fails.
Acceptance criteria
- Unchanged config → the handler does not fire
- Changed config → the handler fires exactly once
- A deliberately broken config is rejected before deployment
- The play continues past the permitted failure
- Rescue and always blocks demonstrably execute
[!tip]- Hint
notify:plushandlers:;validate: 'httpd -t -f %s'on the template or copy module;ignore_errors: true;failed_when:;changed_when:;block/rescue/always;--force-handlers.
The checker proves handler behaviour properly: it disturbs the deployed file on a target, re-runs your playbook, and asserts the handler fired exactly once. It does not put the file back — your playbook is supposed to.
Lab 5.7 — Timed build (90 min, closed-book except ansible-doc)
Revert both managed nodes to clean. One playbook, run once, that takes both
nodes from fresh to:
- A
sysadminsgroup and three users in it, with SSH keys deployed. - Passwordless sudo for that group, with the sudoers file validated before deployment.
- httpd installed, configured, running and enabled — on
webonly. - A templated index page naming the host.
- firewalld permitting http on web, and nothing extra anywhere.
- A daily systemd timer running a maintenance script.
- Handlers restarting services only on real change.
- Idempotent: a second run reports zero changes.
- Completed inside 90 minutes
-
ansible-docwas the only reference used - Second run:
changed=0on every host
The sudoers validate trap.
validate: 'visudo -cf %s'on thecopyortemplatemodule. Deploying a broken sudoers file with no validation locks you out of root entirely — a genuinely unrecoverable mistake on a machine without console access.
The bridge
Do not treat Ansible as a separate subject. Every RHCSA task has a module, and the mapping is the most valuable reference in this curriculum: [[manual-to-ansible-a-module-map|Manual to Ansible - A Module Map]].
Next week you re-do every Month 1 lab through Ansible, with one rule: you may not log into a managed node to fix anything.
→ Next: [[automating-the-rhcsa-set-with-ansible|Automating the RHCSA Set with Ansible]]