RHCE (EX294) — Footguns & Pitfalls¶
Things that fail silently or waste exam time.
1. Forgetting FQCN¶
# WRONG — works in older Ansible, ambiguous
- copy:
src: file.txt
dest: /tmp/file.txt
# RIGHT — always use fully qualified collection name
- ansible.builtin.copy:
src: file.txt
dest: /tmp/file.txt
On the exam, use FQCN for everything. ansible.builtin.*, ansible.posix.*,
community.general.*. If you just write copy, it may work but ambiguous module
resolution can bite you with collections.
2. Handlers Never Run¶
Handlers only run when the task that notifys them reports changed. If the
task reports ok (no change), the handler is skipped.
# This handler will NOT run on second playbook execution
- name: Deploy config
ansible.builtin.copy:
src: httpd.conf
dest: /etc/httpd/conf/httpd.conf
notify: Restart httpd # Only fires on "changed"
# If you need the handler to run NOW
- ansible.builtin.meta: flush_handlers
Exam trap: You update a template, but the handler doesn't fire because
the file hasn't changed since the last run. Use --check --diff to verify
what would change.
3. Variable Precedence Surprises¶
# Role defaults (roles/web/defaults/main.yml):
http_port: 80
# Group vars (group_vars/webservers.yml):
http_port: 8080
# Extra vars on command line:
# ansible-playbook -e "http_port=9090"
Result: http_port = 9090 (extra vars always win).
Exam trap: You set a variable in defaults/main.yml but it's overridden by
group_vars/. Or worse, you set it in vars/main.yml (high precedence) and
can't override it from the inventory.
Rule: Use defaults/main.yml for role variables you want users to override.
Use vars/main.yml only for internal constants.
4. command vs shell Module¶
# command module — no shell features (no pipes, redirects, env vars)
- ansible.builtin.command: echo $HOME
# Prints literal "$HOME" — not expanded!
# shell module — full shell features
- ansible.builtin.shell: echo $HOME
# Prints /root (or whatever HOME is)
# command module — DOES NOT expand globs
- ansible.builtin.command: ls /tmp/*.log
# FAILS — no glob expansion
# shell module — expands globs
- ansible.builtin.shell: ls /tmp/*.log
# Works as expected
Rule: Use command by default (safer, no injection risk). Use shell only
when you need pipes, redirects, or variable expansion.
5. Forgetting become Scope¶
# become at play level — ALL tasks run as root
- hosts: all
become: true # Every task in this play uses sudo
tasks:
- ansible.builtin.dnf: ...
# become at task level — only that task
- hosts: all
tasks:
- name: This runs as remote_user
ansible.builtin.command: whoami
- name: This runs as root
ansible.builtin.dnf:
name: httpd
state: present
become: true
Exam trap: You forget become: true and package installs fail with
permission denied. Or you set become globally when some tasks shouldn't
run as root.
6. YAML Gotchas¶
# WRONG — YAML interprets "yes", "no", "true", "false" as booleans
enabled: yes # This is boolean true, not the string "yes"
state: true # Same
# When you need the literal string, quote it
answer: "yes"
# WRONG — colon in value without quotes
message: Error: something broke # YAML parse error!
# RIGHT
message: "Error: something broke"
# WRONG — leading spaces in multiline
content: |
line1
line2 # This has an extra leading space!
# WRONG — tabs in YAML
name: Install # If this is a tab, YAML parser explodes
7. ansible-vault Password Mismatch¶
# You encrypted with one password
ansible-vault encrypt secrets.yml
# But run with a different password file
ansible-playbook site.yml --vault-password-file wrong_file
# ERROR! Decryption failed
# Or you forgot to pass the vault flag entirely
ansible-playbook site.yml
# ERROR! Attempting to decrypt but no vault secrets found
Exam tip: Write the vault password to a file immediately:
8. Template Validation Failures¶
# Using validate to check config before deploying
- ansible.builtin.template:
src: httpd.conf.j2
dest: /etc/httpd/conf/httpd.conf
validate: httpd -t -f %s # %s = temp file path
# If validation fails, the file is NOT deployed (good!)
# But the error message may be confusing
Gotcha: The %s is replaced with the temp file path, not the final
destination. Some validators care about the file path (e.g., nginx needs
the file in a specific location). If validate fails unexpectedly, try
without validate first, then add it back.
9. Inventory Variable vs Playbook Variable¶
Playbook vars beats inventory host_vars in precedence. This can
surprise you if you expect the inventory value to stick.
Rule: For per-host settings, use host_vars/ directory. For defaults,
use role defaults/. For overrides, use extra vars.
10. Forgetting permanent: true on Firewall Rules¶
# WRONG — rule disappears on reboot
- ansible.posix.firewalld:
service: http
state: enabled
immediate: true
# RIGHT — persists across reboots AND applies now
- ansible.posix.firewalld:
service: http
state: enabled
permanent: true
immediate: true
Always set both permanent: true AND immediate: true. The exam will
reboot your systems to verify persistence.
11. Forgetting restorecon After sefcontext¶
# This sets the POLICY but doesn't change existing files
- community.general.sefcontext:
target: '/srv/myapp(/.*)?'
setype: httpd_sys_content_t
state: present
# You MUST also run restorecon to apply to existing files
- ansible.builtin.command: restorecon -Rv /srv/myapp
changed_when: true
sefcontext updates the policy database. restorecon applies it to the
filesystem. Without both, SELinux will still deny access.
12. Loop vs Passing a List¶
# SLOW — runs module once per item
- name: Install packages (loop)
ansible.builtin.dnf:
name: "{{ item }}"
state: present
loop:
- httpd
- php
- firewalld
# FAST — runs module once with all items
- name: Install packages (list)
ansible.builtin.dnf:
name:
- httpd
- php
- firewalld
state: present
Modules like dnf, apt, and pip accept lists natively. Using a loop
makes N separate calls to the package manager instead of one.
13. changed_when: false for Read-Only Commands¶
# This always reports "changed" even though it changes nothing
- name: Check disk space
ansible.builtin.command: df -h
register: disk_info
# Fix — mark it as never changing
- name: Check disk space
ansible.builtin.command: df -h
register: disk_info
changed_when: false
If your playbook shows changes on every run, it's not idempotent. The exam expects idempotent playbooks (second run = zero changes).
14. include_role vs import_role¶
import_role |
include_role |
|
|---|---|---|
| When parsed | At playbook parse time (static) | At task execution time (dynamic) |
| Supports conditionals | On each task inside the role | On the include itself |
| Tags | Inherited by all tasks | Only on the include task |
| Loops | Cannot be used in loops | Can be used in loops |
| Variable scope | Play scope | Limited scope |
Rule for exam: Use roles: section (simplest) unless you need
conditional role inclusion, in which case use include_role.