RHCE common mistakes
RHCE · Understand the traps and how to avoid them.
Runs `ansible-playbook site.yml` from a directory other than the project directory, so the project's `ansible.cfg` is never read and the run silently uses `/etc/ansible/ansible.cfg` and `/etc/ansible/hosts`. The playbook then connects as the wrong remote user, uses the wrong inventory, or asks for a sudo password that was already configured.
Ansible searches for ansible.cfg in this order: ANSIBLE_CONFIG, ./.ansible.cfg, ~/.ansible.cfg, /etc/ansible/ansible.cfg. Precedence depends on the current working directory, not on where the playbook file lives.
What to do instead
Change into the project directory before running, or export ANSIBLE_CONFIG=/path/to/ansible.cfg. Confirm the file actually in use with ansible --version (the 'config file' line) and ansible-config dump --only-changed.
Writes a task that calls a module which is not in ansible-core (for example `firewalld`, `seboolean`, `selinux`, `sefcontext`, `mount` alternatives) and hits 'couldn't resolve module/action: ...', then spends the remaining time debugging the task arguments instead of the missing collection.
Current Ansible ships as ansible-core, which contains only the ansible.builtin modules; many RHEL administration modules live in redistributable collections such as ansible.posix and community.general.
What to do instead
Check before writing: ansible-doc -l <keyword> or ansible-doc ansible.posix.firewalld. Install what is missing (ansible-galaxy collection install ansible.posix) or use an available equivalent (ansible.builtin.service, ansible.builtin.lineinfile, ansible.builtin.command + semanage). Prefer fully qualified collection names so resolution is unambiguous.
Runs ad hoc commands such as `ansible all -m ansible.builtin.dnf -a 'name=httpd state=present'` without `-b`, gets a permission-denied error, and concludes the inventory or SSH configuration is broken.
Ad hoc commands run as the remote user (often an unprivileged automation account) and escalate only when become is requested. ping succeeding proves connectivity, not privilege.
What to do instead
Add -b (and --become-user root if the default is not root), or set become = true under [privilege_escalation] in ansible.cfg. Verify escalation explicitly with ansible all -b -m ansible.builtin.command -a 'id' before writing playbooks.
Puts short hostnames in the inventory that do not resolve from the control node, so every task returns UNREACHABLE; the candidate then 'fixes' it by setting `ansible_connection=local` for all hosts, which makes the playbook run against the control node while still reporting success.
The inventory hostname is used both as the SSH target and as the fact/variable key; if name resolution or SSH key trust is not in place, the connection fails before any module runs, and local masks the error instead of fixing it.
What to do instead
Provide a resolvable address with ansible_host= in the inventory or host_vars, or add /etc/hosts entries. Distribute keys with ssh-copy-id (or ansible.builtin.authorized_key) so host-key prompts do not block the first connection. Verify with ansible-inventory --list and ansible all -m ansible.builtin.ping.
Writes conditional tasks as `when: "{{ ansible_facts['distribution'] }} == 'RedHat'"` and then chases a deprecation warning, a truthiness surprise, or a task that runs on the wrong hosts.
The when keyword is already evaluated as a Jinja2 expression, so wrapping the variable in {{ }} creates a double-templated/nested expression rather than the intended comparison.
What to do instead
Drop the braces: when: ansible_facts['distribution'] == 'RedHat'. Use | bool for string booleans, is defined for optional variables, and a YAML list (implicit AND) or or for compound conditions instead of nesting braces.
A task notifies `restart httpd` while the handler is named `Restart httpd` (or the handler sits in a role but is notified by an unqualified name), so the service is never restarted - yet the play reports success and no error is raised.
Handler notification matches on the exact, case-sensitive name string, or on a shared listen topic. Handlers also only run if the notifying task actually reported changed, and they run at the end of the play unless flushed.
What to do instead
Use character-identical strings for notify: and name:, or give related handlers a common listen: topic. Confirm the notifying task reported changed and that 'RUNNING HANDLER' appears in the output; use meta: flush_handlers when a later task depends on the restart.
Hand-builds a role directory with the entry point named `task/main.yml` or `main.yaml` in the wrong place, or references it as `roles: [ ./roles/myrole ]`; the play either cannot find the role or runs it with nothing executed.
Roles are resolved by bare name against roles_path (by default a roles/ directory next to the playbook, plus configured paths), and the required entry point is <role>/tasks/main.yml (or main.yaml). A role with no task file runs as an empty task list without failing.
What to do instead
Scaffold with ansible-galaxy init roles/myrole, keep roles/ adjacent to the playbook (or set roles_path in ansible.cfg), reference it as roles: [ myrole ], and verify with ansible-playbook --syntax-check plus a run that shows the role's tasks executing.
Stores variables in `group_vars/webserver.yml` while the inventory group is named `web_servers`, or expects play-level `vars:` to override a value passed with `-e`, so hosts get undefined-variable errors or stale values.
group_vars and host_vars files are matched to group and host names exactly, with no pluralisation or normalisation, and variable precedence places extra vars above play vars and inventory group/host vars.
What to do instead
Name the file exactly as the group or host. Inspect what a host actually receives with ansible-inventory --host <host> --vars or ansible <host> -m ansible.builtin.debug -a 'var=myvar', and remember -e wins over everything templated in the play.
Appends configuration with `ansible.builtin.shell: echo 'PermitRootLogin no' >> /etc/ssh/sshd_config`, or runs `command: dnf install -y httpd`; the second run duplicates the line, reports `changed` again, or fails outright.
shell and command judge success by exit status only; Ansible has no model of the desired end state, so it cannot tell an already-applied change from a new one and is never idempotent.
What to do instead
Use the state-aware module: ansible.builtin.lineinfile/blockinfile/template for configuration, ansible.builtin.dnf for packages, ansible.builtin.service for services. If a command is unavoidable, guard it with creates:/removes: and changed_when:/failed_when:. Always run the playbook a second time and confirm changed=0.
Deploys a web page or config file with `copy`/`template` and no `mode` (or `mode: 644`, unquoted), leaves it labelled with the wrong SELinux type so httpd returns 403, starts the service with `state: started` only, and never opens the firewall port.
The default remote file mode and ownership come from the transferring user's umask, unquoted octal modes are read as decimal by YAML, SELinux enforcing mode blocks content whose type is not readable by the service, and state: started does not enable a unit at boot.
What to do instead
Set owner, group, and a quoted mode: '0644' (or '0600' for secrets); set the SELinux context with setype: httpd_sys_content_t (or ansible.posix.seboolean for booleans); use state: started together with enabled: true; open the port with ansible.posix.firewalld. Verify from the control node with curl and systemctl is-enabled.