Skip to content

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:

echo 'thepassword' > .vault_pass
chmod 600 .vault_pass


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

# inventory
[webservers]
web1 http_port=80

[webservers:vars]
doc_root=/var/www/html
# playbook
vars:
  http_port: 8080

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.