RHEL in Practice #5: Automating RHEL with Ansible — Bridging to the RHCE Track
In #1〜#4 of the RHEL in Practice track we stood up an nginx web server, PostgreSQL, Podman containers, and monitoring all by hand. Handling a single machine that way is something you have now mastered. But the story changes the moment you bring up the same configuration on a second server, then a third. Retyping the same dnf commands, the same systemctl, the same firewalld rules, and the same SELinux booleans by hand, you will inevitably drop a step or two. In this post we move that hand work into Ansible and organize the big picture of reproducing the same result from a single set of code.
The goal of this post is not to dig deep into Ansible syntax. It is to show “why we move to automation” and how the hand work you learned in real RHEL operations turns into a single line of a playbook. The proper Ansible syntax and RHCE exam scope are covered separately in the RHCE series.
The Limits of Doing It by Hand #
Recall the procedure for standing up nginx in #1. You installed the package, enabled the service, opened http and https in firewalld, and — if you used a non-standard port — registered the SELinux port label. For a single machine that takes five minutes. But this work has three weaknesses.
First, it is hard to reproduce. Drop the firewalld --reload on the second server, or forget the -P on setsebool, and that one server behaves subtly differently. Second, it leaves no record. Six months later, to recall “which boolean did I turn on for this server,” you have to dig through command history. Third, there is no way to verify it. To confirm whether the current state matches the intended state, you end up checking each item by hand.
Ansible solves all three at once. Instead of a procedure, you write down the desired state as code, and Ansible compares it against the current state and brings only the missing parts into line. The code itself is the record, and running the same code any number of times yields the same result.
Installing Ansible and a Minimal Configuration #
On RHEL, Ansible is installed only on the control node (the side issuing commands). The target servers only need Python and SSH. Not having to install a separate agent is one of Ansible’s big advantages.
# install ansible-core on the control node
sudo dnf install -y ansible-core
# check the version
ansible --versionansible-core is a lightweight package holding only the engine and the built-in modules. There is also the ansible package bundling many more collections, but for RHEL work the cleaner approach is to add the collections you need on top of ansible-core. Next, place an inventory and an ansible.cfg in your working directory.
# inventory: the list of servers to manage
[web]
web1.example.com
web2.example.com
[db]
db1.example.com# ansible.cfg: project defaults
[defaults]
inventory = ./inventory
remote_user = ansible
host_key_checking = False
[privilege_escalation]
become = True
become_method = sudoThe inventory is a file that groups which servers you manage, and ansible.cfg gathers the defaults so you do not have to attach options every time. become = True means escalating privileges with sudo on the target, which is essential for work like package installation or service control on RHEL. You confirm connectivity in a single line.
# ping every host (check SSH and Python connectivity)
ansible all -m pingThe ping module does not use ICMP — it connects over SSH and confirms that Python works. When you get pong back, you are ready to run a playbook.
Idempotency: The Core That Guarantees the Same Result #
The single most important concept for understanding Ansible is idempotency. It is the property that running the same playbook any number of times leaves the same result as running it once. Type dnf install twice by hand and the second time reports “already installed,” but a script that blindly lists commands easily produces odd side effects on the second run.
Ansible modules deal with state, not commands. Writing state: present means “this package must exist,” not “install it now.” If it already exists, nothing happens and it passes as ok; only when it is absent does it install and report changed. So on the first run several items show changed, but run it again right away and they all become ok. This “everything ok on the second run” is the signal that idempotency is being maintained.
Thanks to this property, a playbook becomes not a “one-time install script” but a “definition that brings the server into the intended state whenever you run it.” Even if a server has drifted for some reason, running the same playbook again returns it to where it belongs.
Moving Hand Work into a Playbook #
Now let’s move the nginx work from #1 into a single playbook. We put the four things you did by hand — package install, service enable, firewalld opening, and SELinux boolean — into one file.
# web.yml: one nginx cycle
- name: Configure the web server
hosts: web
become: true
tasks:
- name: Install nginx
ansible.builtin.dnf:
name: nginx
state: present
- name: Enable + start the nginx service
ansible.builtin.systemd:
name: nginx
enabled: true
state: started
- name: Allow http and https in firewalld
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: enabled
loop:
- http
- https
- name: SELinux boolean for reverse proxy
ansible.posix.seboolean:
name: httpd_can_network_connect
state: true
persistent: trueLay them side by side with the commands you typed by hand and the correspondence is direct. dnf install -y nginx becomes the dnf module’s state: present; systemctl enable --now nginx becomes the systemd module’s enabled: true + state: started; firewall-cmd --add-service ... --permanent + --reload becomes the firewalld module’s permanent: true + immediate: true; and setsebool -P becomes the seboolean module’s persistent: true. The work you did by hand in #1 has become code as-is.
Running it is one line.
# preview only what will change, before actually applying
ansible-playbook web.yml --check
# apply for real
ansible-playbook web.yml--check is a mode that does not actually change anything and only shows what would change. Making a habit of running this mode once to confirm the blast radius before applying to a production server reduces incidents. The inventory’s [web] group contains web1 and web2, so this single playbook configures both servers identically. Even if the count grows to ten servers, it is the same single-line command. All three weaknesses of hand work disappear here.
Abstracting with rhel-system-roles #
The playbook above handled firewalld and SELinux directly with modules. Red Hat officially provides rhel-system-roles, which abstract one level above that. They bundle RHEL operational areas like firewall, SELinux, time synchronization, and storage into pre-verified roles, so that instead of calling modules you just write down “the desired result” as variables.
# install the system roles collection
sudo dnf install -y rhel-system-rolesFor example, for time synchronization, you hand the timesync role nothing but the NTP servers and it sorts out the chrony configuration and the service for you.
# timesync.yml: time synchronization role
- name: Configure time synchronization
hosts: all
become: true
roles:
- role: rhel-system-roles.timesync
vars:
timesync_ntp_servers:
- hostname: 0.pool.ntp.org
- hostname: 1.pool.ntp.orgIn the same way, the selinux role takes booleans, ports, and fcontexts as variables to replace the semanage and setsebool work you did by hand in #1, and the firewall role takes service and port openings as variables. The abstraction level is higher than using modules directly, so the realistic operating split is to use system-roles for work close to a standard RHEL configuration and modules for work that needs fine-grained control. Because Red Hat maintains system-roles in step with the RHEL version, there is also less breakage on an OS upgrade than when you wire up modules yourself.
Operational Points #
Here are the things to keep in mind in practice when moving to automation.
- Understand the hand work first, then automate. You need the experience of getting stuck by hand in #1〜#4 for a playbook to read clearly. Skip the hand work and just copy a playbook, and you will not be able to step in when it breaks.
- Run
--checkfirst. Making a habit of confirming the planned changes before applying straight to a production server prevents big incidents. - Confirm idempotency. Run the playbook twice and watch whether the second run finishes all
ok. Ifchangedkeeps showing on the second run too, that task was not written to be idempotent and needs fixing. - Standard with system-roles, fine-grained with modules. Use verified roles for work close to a standard RHEL configuration, and control only special requirements directly with modules.
- Keep playbooks under version control. Put the inventory and playbooks in Git and the code becomes the record of your server configuration.
Wrap-up #
What this post nailed down:
- Why automation. The weaknesses of hand work are hard reproduction, no record, and no verification. Ansible writes state as code and solves all three at once.
- Minimal configuration. Install only
ansible-coreon the control node, group the targets with an inventory, and put the defaults in ansible.cfg. There is no agent on the target. - Idempotency. Modules deal with state, not commands, so any number of runs yields the same result. You confirm it by whether the second run is all
ok. - Moving hand work over. The four nginx steps from #1 move as-is into a single playbook of the
dnf,systemd,firewalld, andsebooleanmodules. - Abstraction. With rhel-system-roles you handle selinux, firewall, and timesync with variables alone.
Next: Wrapping Up the Track #
We have come from running one cycle by hand to tying it together as code. The last stop of the track is bringing the pieces covered so far into a single picture.
In #6 Wrapping Up the Track: Reference Architecture, we gather web, DB, containers, monitoring, and automation into one reference architecture and organize how the whole practice track runs as a single system. And if you need deeper automation, the RHCE series covers Ansible syntax and the exam scope in earnest.