Skip to content

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-doc is your lifeline. You have no internet access, but ansible-doc <module> shows every parameter with examples. Run ansible-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:

  1. Syntax check first: ansible-playbook site.yml --syntax-check
  2. Increase verbosity: ansible-playbook site.yml -vvv
  3. Check mode: ansible-playbook site.yml --check --diff
  4. Module docs: ansible-doc ansible.builtin.copy
  5. Verify connectivity: ansible <host> -m ping
  6. Check become: ansible <host> -m command -a "whoami" --become
  7. Check variables: Add debug task to print variables
  8. Vault issues: Ensure correct --vault-password-file or --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.