Back to articles
Ansible & RHCEFlagship #0724 Jan 2027

Every RHCSA task, translated to Ansible

Every RHCSA task mapped to its manual command, its Ansible module and the collection that module lives in - plus the collections trap that catches people out in the exam.

#ansible#rhcsa#automation#devops

The single most useful reference for anyone going from RHCSA to RHCE. It is what turns two unrelated certifications into one continuous skill.

The method change. The usual advice is watch → understand → do it yourself → break it → troubleshoot → rebuild. Replace it with:

Learn it manually → automate it with Ansible → break it → troubleshoot it → rebuild it automatically.

Do this on the same day for the same topic. On the day you create users by hand, you also write the user module task. The bridge gets built daily rather than saved up for one panicked week.

Part of [[rhcsa-to-rhce-a-60-day-lab-curriculum|RHCSA to RHCE - A 60-Day Lab Curriculum]].

The collections trap

Only some of these modules ship in ansible.builtin. Several of the ones you need most for RHCSA-style work — firewalld, SELinux, mount, ACL, LVM — live in ansible.posix or community.general.

ansible-galaxy collection list                    # what you actually have
ansible-doc -l ansible.posix                      # what is inside one
ansible-galaxy collection install ansible.posix community.general

Install both on the control node early, and confirm again before any mock exam. If ansible-doc firewalld returns nothing during the exam, you needed the collection and did not check.

The map

RHCSA taskManual commandAnsible moduleCollection
Create useruseradduserbuiltin
Create groupgroupaddgroupbuiltin
Set passwordpasswduser + password: (hashed)builtin
Password ageingchageuser (password_expire_*)builtin
Deploy SSH keyssh-copy-idauthorized_keyansible.posix
sudo rulesvisudocopy/template + validatebuiltin
Install packagednf installdnfbuiltin
Package groupdnf group installdnf (name: "@group")builtin
Add repositoryedit .repoyum_repositorybuiltin
Start/enable servicesystemctlservice / systemd_servicebuiltin
Custom unit filewrite .servicetemplate + systemd_service (daemon_reload)builtin
Firewall port/servicefirewall-cmdfirewalldansible.posix
SELinux modesetenforceselinuxansible.posix
SELinux booleansetsebool -Psebooleanansible.posix
SELinux contextsemanage fcontextsefcontext + command: restoreconcommunity.general
SELinux portsemanage portseportcommunity.general
Copy filecpcopybuiltin
Templated configedit by handtemplate (Jinja2)builtin
Line in a filesed -ilineinfile / blockinfilebuiltin
Permissions/ownershipchmod, chownfilebuiltin
ACLssetfaclaclansible.posix
Create directorymkdir -pfile (state: directory)builtin
Archivetararchive / unarchivebuiltin
Cron jobcrontab -ecronbuiltin
systemd timerwrite .timertemplate + systemd_servicebuiltin
Mount + fstabmount, edit fstabmountansible.posix
Partitionparted, fdiskpartedcommunity.general
Filesystemmkfs.xfsfilesystemcommunity.general
LVM volume groupvgcreatelvgcommunity.general
LVM logical volumelvcreate, lvextendlvolcommunity.general
Swapmkswap, swaponfilesystem + mountmixed
Hostnamehostnamectlhostnamebuiltin
Network confignmclinmclicommunity.general
Kernel parametersysctlsysctlansible.posix
Reboot and waitrebootrebootbuiltin
Gather system infouname, dfsetup (facts)builtin
No module exists—command (never shell unless you need a shell)builtin

The pattern to repeat for every topic

Manual, in the morning:

groupadd -g 5000 developers
useradd -g developers -c "Amit" amit
usermod -aG wheel amit
chage -M 30 -W 7 sara

Automated, the same evening — same outcome:

- name: Developer accounts exist
  hosts: managed
  become: true

  vars:
    developers:
      - { name: amit, uid: 3001, comment: "Amit" }
      - { name: sara, uid: 3002, comment: "Sara" }

  tasks:
    - name: Developers group exists
      ansible.builtin.group:
        name: developers
        gid: 5000
        state: present

    - name: Developer accounts exist
      ansible.builtin.user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        group: developers
        comment: "{{ item.comment }}"
        state: present
      loop: "{{ developers }}"

Then break it — delete a user, change a UID by hand, remove the group — and rebuild it by re-running the playbook. That final step is what the exam actually measures.

When there is no module

Use command, and make it honest about change:

- name: Relabel the web content
  ansible.builtin.command: restorecon -Rv /web
  register: relabel
  changed_when: relabel.stdout | length > 0

Without changed_when, that task reports changed on every single run and your playbook is no longer idempotent. shell is only for when you genuinely need pipes, redirection or globbing — otherwise command is safer, because it does not invoke a shell at all.

Finding the module in the exam

You will not remember every module. You do not need to. You need this reflex:

ansible-doc -l | grep -i <thing>          # find it
ansible-doc <module>                      # read the options
ansible-doc -s <module>                   # copy the skeleton

Practise it until it is faster than recalling from memory. It is allowed, it is reliable, and it is the difference between a pass and a blank screen.

The three module choices that most often cost marks

state: present versus state: latest on dnf. They are not the same answer, and a requirement that says "install" is not a requirement that says "upgrade".

state: mounted versus state: present on mount. mounted writes fstab and mounts now; present writes fstab only. A requirement asking for a persistent mount that is also active right now needs the first.

permanent: true without immediate: true on firewalld. You have then configured exactly one of the two states the exam checks. Set both.

→ [[ansible-fundamentals-labs|Ansible Fundamentals Labs]] · [[automating-the-rhcsa-set-with-ansible|Automating the RHCSA Set with Ansible]] · [[red-hat-exam-day-protocol|Red Hat Exam Day Protocol]]