RHCE (EX294) — Street Ops¶
Real-world patterns and exam-ready workflows for every RHCE objective.
Control Node Setup (Speed Run)¶
# 1. Install
sudo dnf install ansible-core -y
# 2. Minimal ansible.cfg
cat > ansible.cfg <<'EOF'
[defaults]
inventory = ./inventory
remote_user = devops
roles_path = ./roles
collections_path = ./collections
host_key_checking = false
[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false
EOF
# 3. Static inventory
cat > inventory <<'EOF'
[webservers]
node1.example.com
node2.example.com
[dbservers]
node3.example.com
[all:children]
webservers
dbservers
EOF
# 4. Distribute SSH keys
for h in node1 node2 node3; do
ssh-copy-id devops@${h}.example.com
done
# 5. Verify
ansible all -m ping
Playbook Patterns You Will Use Repeatedly¶
The "Full RHCSA Task" Pattern¶
This is the pattern for automating any standard sysadmin task:
---
- name: Full system configuration
hosts: all
become: true
vars_files:
- vars/common.yml
- vars/secrets.yml
tasks:
# Users & Groups
- name: Create groups
ansible.builtin.group:
name: "{{ item }}"
state: present
loop: "{{ system_groups }}"
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
group: "{{ item.group }}"
groups: "{{ item.extra_groups | default(omit) }}"
password: "{{ item.password | password_hash('sha512') }}"
state: present
loop: "{{ users }}"
loop_control:
label: "{{ item.name }}"
# Packages
- name: Install required packages
ansible.builtin.dnf:
name: "{{ base_packages }}"
state: present
# Services
- name: Configure services
ansible.builtin.service:
name: "{{ item.name }}"
state: "{{ item.state }}"
enabled: "{{ item.enabled }}"
loop: "{{ services }}"
loop_control:
label: "{{ item.name }}"
# Firewall
- name: Configure firewall rules
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: enabled
loop: "{{ firewall_services }}"
# Cron
- name: Configure cron jobs
ansible.builtin.cron:
name: "{{ item.name }}"
minute: "{{ item.minute | default('*') }}"
hour: "{{ item.hour | default('*') }}"
job: "{{ item.job }}"
user: "{{ item.user | default('root') }}"
loop: "{{ cron_jobs }}"
loop_control:
label: "{{ item.name }}"
# SELinux
- name: Set SELinux booleans
ansible.posix.seboolean:
name: "{{ item.name }}"
state: "{{ item.state }}"
persistent: true
loop: "{{ selinux_booleans }}"
loop_control:
label: "{{ item.name }}"
The "Config Deploy + Restart" Pattern¶
tasks:
- name: Deploy config from template
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: '0644'
validate: "{{ item.validate | default(omit) }}"
loop: "{{ config_files }}"
loop_control:
label: "{{ item.dest }}"
notify: "Restart {{ service_name }}"
handlers:
- name: "Restart {{ service_name }}"
ansible.builtin.service:
name: "{{ service_name }}"
state: restarted
The "Vault + Variables" Pattern¶
# Create encrypted secrets
ansible-vault create vars/secrets.yml
# Put: db_password, api_keys, etc.
# Run with vault
ansible-playbook site.yml --vault-password-file .vault_pass
# vars/common.yml
base_packages:
- httpd
- firewalld
- php
http_port: 80
# In playbook
vars_files:
- vars/common.yml
- vars/secrets.yml # encrypted
Role Creation Workflow¶
# 1. Initialize role
mkdir -p roles
cd roles
ansible-galaxy role init apache
# 2. Edit the key files
# roles/apache/defaults/main.yml — default variables
# roles/apache/tasks/main.yml — task list
# roles/apache/handlers/main.yml — handlers
# roles/apache/templates/ — Jinja2 templates
# roles/apache/files/ — static files
# roles/apache/meta/main.yml — dependencies
# 3. Use in playbook
cat > site.yml <<'EOF'
---
- name: Deploy webservers
hosts: webservers
become: true
roles:
- apache
EOF
roles/apache/tasks/main.yml¶
---
- name: Install Apache
ansible.builtin.dnf:
name: "{{ apache_packages }}"
state: present
- name: Deploy Apache config
ansible.builtin.template:
src: httpd.conf.j2
dest: /etc/httpd/conf/httpd.conf
owner: root
group: root
mode: '0644'
notify: Restart Apache
- name: Deploy vhost config
ansible.builtin.template:
src: vhost.conf.j2
dest: /etc/httpd/conf.d/vhost.conf
notify: Restart Apache
- name: Ensure document root exists
ansible.builtin.file:
path: "{{ apache_doc_root }}"
state: directory
owner: apache
group: apache
mode: '0755'
- name: Start and enable Apache
ansible.builtin.service:
name: httpd
state: started
enabled: true
- name: Open firewall for HTTP
ansible.posix.firewalld:
service: http
permanent: true
immediate: true
state: enabled
Galaxy Requirements Pattern¶
# requirements.yml
---
roles:
- name: geerlingguy.apache
- name: geerlingguy.mysql
version: "3.5.0"
collections:
- name: ansible.posix
- name: community.general
- name: community.crypto
# Install everything
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install -r requirements.yml
Storage Automation Patterns¶
LVM + Filesystem + Mount¶
- name: Create VG
community.general.lvg:
vg: datavg
pvs: /dev/sdb
- name: Create LV
community.general.lvol:
vg: datavg
lv: datalv
size: 5g
- name: Create filesystem
community.general.filesystem:
fstype: xfs
dev: /dev/datavg/datalv
- name: Mount filesystem
ansible.posix.mount:
path: /mnt/data
src: /dev/datavg/datalv
fstype: xfs
state: mounted
Template Patterns¶
Multi-Server Config (nginx upstream)¶
{# templates/nginx-upstream.conf.j2 #}
upstream backend {
{% for host in groups['appservers'] %}
server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port }};
{% endfor %}
}
server {
listen {{ http_port }};
server_name {{ ansible_fqdn }};
location / {
proxy_pass http://backend;
}
}
Conditional Config Block¶
{# templates/sshd_config.j2 #}
Port {{ ssh_port | default(22) }}
PermitRootLogin {{ 'yes' if allow_root_ssh | default(false) else 'no' }}
PasswordAuthentication {{ 'yes' if allow_password_auth | default(false) else 'no' }}
{% if ssh_allowed_users is defined %}
AllowUsers {{ ssh_allowed_users | join(' ') }}
{% endif %}
{% for key, value in sshd_options.items() %}
{{ key }} {{ value }}
{% endfor %}
Debugging Checklist¶
Remember: On the RHCE exam,
ansible-docis your lifeline. You have no internet access, butansible-doc <module>shows every parameter with examples. Runansible-doc -l | grep <keyword>to find module names you cannot remember. This is faster than guessing parameter names.
When something doesn't work on the exam:
- Syntax check first:
ansible-playbook site.yml --syntax-check - Increase verbosity:
ansible-playbook site.yml -vvv - Check mode:
ansible-playbook site.yml --check --diff - Module docs:
ansible-doc ansible.builtin.copy - Verify connectivity:
ansible <host> -m ping - Check become:
ansible <host> -m command -a "whoami" --become - Check variables: Add
debugtask to print variables - Vault issues: Ensure correct
--vault-password-fileor--ask-vault-pass
Exam Time Management¶
| Task Type | Estimated Time |
|---|---|
| Control node setup | 10 min |
| Static inventory + variables | 10 min |
| Basic playbook (packages, services) | 15 min |
| Role creation | 20 min |
| Template-based config | 15 min |
| Vault tasks | 10 min |
| SELinux / firewall automation | 15 min |
| User/group management | 10 min |
| Storage automation (LVM, mounts) | 15 min |
| Review and re-run | 20 min |
Total exam time: ~4 hours. Budget for re-running everything at the end.