Skip to content

Portal | Level: L2: Operations | Topics: RHCE (EX294) Exam, Ansible, Git, Linux Fundamentals | Domain: DevOps & Tooling

RHCE (EX294) Exam Preparation — Primer

Why This Matters

The RHCE (EX294) is a hands-on, performance-based exam that validates your ability to automate Linux system administration using Red Hat Ansible Automation Platform. It is the industry-standard certification for Ansible automation on RHEL. Everything is done on a live system — no multiple choice. You either automate it correctly or you don't.

Prerequisite: RHCSA (EX200) or equivalent Linux administration skills.


Exam Objectives Overview

The EX294 covers these domains:

  1. Understand core components of Ansible — inventories, modules, variables, facts, plays, playbooks, configuration files, roles, collections
  2. Install and configure an Ansible control node — install packages, create config, manage settings
  3. Configure Ansible managed nodes — SSH keys, privilege escalation, validate connectivity
  4. Script administration tasks — shell scripts calling Ansible, ad hoc commands
  5. Create and use static inventories — host groups, nested groups, inventory variables
  6. Create Ansible plays and playbooks — tasks, handlers, conditionals, loops, tags, error handling, templates
  7. Automate standard RHCSA tasks — users, groups, packages, services, firewall, storage, network, cron, SELinux, files
  8. Manage content — deploy files, templates, archives
  9. Create and use templates — Jinja2 templates for configuration files
  10. Work with Ansible variables and facts — variable precedence, registered variables, facts, magic variables
  11. Create and work with roles — role structure, defaults, handlers, tasks, templates, files, vars
  12. Download roles from Ansible Galaxy and Automation Hub — requirements.yml, collections
  13. Manage parallelism — forks, serial, async, throttle
  14. Use Ansible Vault — encrypt files, strings, use at runtime
  15. Use provided documentation — ansible-doc, module documentation
  16. Work with Ansible collections — install, use, FQCN

1. Core Components of Ansible

Architecture

Control Node (RHEL with ansible-core installed)
     |
     | SSH (key-based auth)
     v
Managed Nodes (RHEL systems)
  • Control node: Where Ansible runs. Needs Python 3 + ansible-core.
  • Managed nodes: Target systems. Need Python 3 + SSH access.
  • Inventory: Defines which hosts to manage and how to group them.
  • Modules: Units of work (e.g., yum, copy, service, user).
  • Plugins: Extend Ansible (connection, callback, lookup, filter).
  • Playbooks: YAML files containing ordered lists of plays.
  • Roles: Reusable bundles of tasks, handlers, files, templates, variables.
  • Collections: Distribution format for roles, modules, and plugins (namespace.collection).

Key Files

File Purpose
/etc/ansible/ansible.cfg System-wide config (lowest precedence)
./ansible.cfg Project-level config (highest file precedence)
~/.ansible.cfg User-level config
ANSIBLE_CONFIG env var Overrides all file-based config
inventory or /etc/ansible/hosts Default inventory location

Config precedence (highest to lowest): 1. ANSIBLE_CONFIG environment variable 2. ./ansible.cfg (current directory) 3. ~/.ansible.cfg (home directory) 4. /etc/ansible/ansible.cfg (system)

Minimal ansible.cfg

[defaults]
inventory = ./inventory
remote_user = ansible
ask_pass = false

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false

2. Install and Configure the Control Node

Installation

# RHEL 9 — enable the Ansible repo
sudo dnf install ansible-core

# Verify
ansible --version
ansible-config dump --changed

# Install collections
ansible-galaxy collection install ansible.posix
ansible-galaxy collection install community.general

ansible.cfg Deep Dive

[defaults]
inventory = ./inventory
remote_user = devops
roles_path = ./roles:/usr/share/ansible/roles
collections_path = ./collections
forks = 5
log_path = ./ansible.log
host_key_checking = false

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false

Key settings for the exam: - forks — controls parallelism (default 5) - host_key_checking — set to false in lab environments - roles_path — where Ansible searches for roles - remote_user — SSH user for managed nodes - log_path — enables logging (not on by default)


3. Configure Managed Nodes

SSH Key Setup

# On control node — generate key if needed
ssh-keygen -t ed25519 -C "ansible"

# Distribute to managed nodes
ssh-copy-id devops@managed1.example.com
ssh-copy-id devops@managed2.example.com

# Verify
ansible all -m ping

Privilege Escalation

On each managed node, the Ansible user needs passwordless sudo:

# On managed node (or automate with Ansible)
echo "devops ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/devops
chmod 0440 /etc/sudoers.d/devops
visudo -cf /etc/sudoers.d/devops   # syntax check

Connectivity Validation

# Test all hosts
ansible all -m ping

# Test specific group
ansible webservers -m ping

# Test with verbose output
ansible all -m ping -v

4. Static Inventories

INI Format

[webservers]
web1.example.com
web2.example.com

[dbservers]
db1.example.com
db2.example.com

[datacenter:children]
webservers
dbservers

[webservers:vars]
http_port=80

YAML Format

all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
      vars:
        http_port: 80
    dbservers:
      hosts:
        db1.example.com:
        db2.example.com:
    datacenter:
      children:
        webservers:
        dbservers:

Host and Group Variables

inventory/
  hosts          # or hosts.yml
  host_vars/
    web1.example.com.yml
  group_vars/
    all.yml
    webservers.yml
    dbservers.yml

Variable precedence (for inventory): - host_vars/ overrides group_vars/ - Child group vars override parent group vars - all group has lowest group precedence

Useful Inventory Commands

# List all hosts
ansible-inventory --list

# Show host variables
ansible-inventory --host web1.example.com

# Graph view
ansible-inventory --graph

5. Ad Hoc Commands

# Syntax: ansible <pattern> -m <module> -a "<arguments>"

# Ping all hosts
ansible all -m ping

# Run a command
ansible webservers -m command -a "uptime"

# Use shell module (supports pipes, redirection)
ansible all -m shell -a "df -h | grep /dev/sda"

# Copy a file
ansible all -m copy -a "src=/tmp/file dest=/tmp/file mode=0644"

# Install a package
ansible webservers -m dnf -a "name=httpd state=present" --become

# Start and enable a service
ansible webservers -m service -a "name=httpd state=started enabled=yes" --become

# Manage users
ansible all -m user -a "name=testuser state=present" --become

# Gather facts
ansible web1.example.com -m setup

# Filter facts
ansible web1.example.com -m setup -a "filter=ansible_distribution*"

6. Playbook Fundamentals

Basic Playbook Structure

---
- name: Configure webservers
  hosts: webservers
  become: true
  vars:
    http_port: 80
    doc_root: /var/www/html

  tasks:
    - name: Install httpd
      ansible.builtin.dnf:
        name: httpd
        state: present

    - name: Start and enable httpd
      ansible.builtin.service:
        name: httpd
        state: started
        enabled: true

    - name: Deploy index page
      ansible.builtin.template:
        src: templates/index.html.j2
        dest: "{{ doc_root }}/index.html"
        owner: apache
        group: apache
        mode: '0644'
      notify: Restart httpd

  handlers:
    - name: Restart httpd
      ansible.builtin.service:
        name: httpd
        state: restarted

Multiple Plays

---
- name: Configure webservers
  hosts: webservers
  become: true
  tasks:
    - name: Install httpd
      ansible.builtin.dnf:
        name: httpd
        state: present

- name: Configure dbservers
  hosts: dbservers
  become: true
  tasks:
    - name: Install mariadb
      ansible.builtin.dnf:
        name: mariadb-server
        state: present

Running Playbooks

# Run a playbook
ansible-playbook site.yml

# Check mode (dry run)
ansible-playbook site.yml --check

# Diff mode (show changes)
ansible-playbook site.yml --diff

# Check + diff together
ansible-playbook site.yml --check --diff

# Limit to specific hosts
ansible-playbook site.yml --limit web1.example.com

# Start at a specific task
ansible-playbook site.yml --start-at-task "Install httpd"

# Step through tasks
ansible-playbook site.yml --step

# Syntax check
ansible-playbook site.yml --syntax-check

# List tasks
ansible-playbook site.yml --list-tasks

# List tags
ansible-playbook site.yml --list-tags

7. Modules for System Administration

Package Management

- name: Install packages
  ansible.builtin.dnf:
    name:
      - httpd
      - firewalld
      - php
    state: present

- name: Remove a package
  ansible.builtin.dnf:
    name: telnet
    state: absent

- name: Install a specific version
  ansible.builtin.dnf:
    name: httpd-2.4.51
    state: present

- name: Update all packages
  ansible.builtin.dnf:
    name: "*"
    state: latest

- name: Install a package group
  ansible.builtin.dnf:
    name: "@Development Tools"
    state: present

- name: Enable a DNF module stream
  ansible.builtin.dnf:
    name: "@postgresql:15/server"
    state: present

Service Management

- name: Start and enable httpd
  ansible.builtin.service:
    name: httpd
    state: started
    enabled: true

- name: Restart a service
  ansible.builtin.service:
    name: httpd
    state: restarted

- name: Reload a service
  ansible.builtin.service:
    name: httpd
    state: reloaded

- name: Stop and disable a service
  ansible.builtin.service:
    name: cups
    state: stopped
    enabled: false

User and Group Management

- name: Create a group
  ansible.builtin.group:
    name: developers
    gid: 2000
    state: present

- name: Create a user
  ansible.builtin.user:
    name: jdoe
    uid: 2001
    group: developers
    groups: wheel
    append: true
    shell: /bin/bash
    home: /home/jdoe
    password: "{{ 'P@ssw0rd' | password_hash('sha512') }}"
    state: present

- name: Remove a user
  ansible.builtin.user:
    name: olduser
    state: absent
    remove: true

- name: Add SSH key for user
  ansible.posix.authorized_key:
    user: jdoe
    key: "{{ lookup('file', 'files/jdoe_id_ed25519.pub') }}"
    state: present

File Management

- name: Copy a file
  ansible.builtin.copy:
    src: files/httpd.conf
    dest: /etc/httpd/conf/httpd.conf
    owner: root
    group: root
    mode: '0644'
    backup: true

- name: Create a directory
  ansible.builtin.file:
    path: /opt/myapp
    state: directory
    owner: appuser
    group: appuser
    mode: '0755'

- name: Create a symlink
  ansible.builtin.file:
    src: /opt/myapp/current
    dest: /opt/myapp/latest
    state: link

- name: Download a file
  ansible.builtin.get_url:
    url: https://example.com/file.tar.gz
    dest: /tmp/file.tar.gz
    checksum: sha256:abcdef1234567890

- name: Extract an archive
  ansible.builtin.unarchive:
    src: /tmp/file.tar.gz
    dest: /opt/myapp/
    remote_src: true

- name: Edit a line in a file
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^PermitRootLogin'
    line: 'PermitRootLogin no'
    backup: true

- name: Add a block of text
  ansible.builtin.blockinfile:
    path: /etc/hosts
    block: |
      192.168.1.10 web1.example.com
      192.168.1.11 web2.example.com
    marker: "# {mark} ANSIBLE MANAGED BLOCK"

Firewall Management

- name: Enable firewalld
  ansible.builtin.service:
    name: firewalld
    state: started
    enabled: true

- name: Allow HTTP
  ansible.posix.firewalld:
    service: http
    permanent: true
    immediate: true
    state: enabled

- name: Allow HTTPS
  ansible.posix.firewalld:
    service: https
    permanent: true
    immediate: true
    state: enabled

- name: Allow custom port
  ansible.posix.firewalld:
    port: 8080/tcp
    permanent: true
    immediate: true
    state: enabled

- name: Allow port range
  ansible.posix.firewalld:
    port: 5000-5100/tcp
    permanent: true
    immediate: true
    state: enabled

Storage Management

- name: Create a partition
  community.general.parted:
    device: /dev/sdb
    number: 1
    state: present
    part_end: 1GiB

- name: Create a filesystem
  community.general.filesystem:
    fstype: xfs
    dev: /dev/sdb1

- name: Mount a filesystem
  ansible.posix.mount:
    path: /mnt/data
    src: /dev/sdb1
    fstype: xfs
    state: mounted

- name: Create an LVM volume group
  community.general.lvg:
    vg: datavg
    pvs: /dev/sdc

- name: Create an LVM logical volume
  community.general.lvol:
    vg: datavg
    lv: datalv
    size: 5g

Cron Jobs

- name: Create a cron job
  ansible.builtin.cron:
    name: "Backup database"
    minute: "0"
    hour: "2"
    job: "/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1"
    user: root

- name: Create a cron job with special time
  ansible.builtin.cron:
    name: "Daily cleanup"
    special_time: daily
    job: "/usr/local/bin/cleanup.sh"

SELinux Management

- name: Set SELinux to enforcing
  ansible.posix.selinux:
    policy: targeted
    state: enforcing

- name: Set SELinux boolean
  ansible.posix.seboolean:
    name: httpd_can_network_connect
    state: true
    persistent: true

- name: Set SELinux file context
  community.general.sefcontext:
    target: '/srv/myapp(/.*)?'
    setype: httpd_sys_content_t
    state: present

- name: Apply SELinux file context
  ansible.builtin.command: restorecon -Rv /srv/myapp
  changed_when: true

- name: Set SELinux port label
  community.general.seport:
    ports: 8443
    proto: tcp
    setype: http_port_t
    state: present

Network Configuration

- name: Configure a static IP with nmcli
  community.general.nmcli:
    conn_name: eth0
    ifname: eth0
    type: ethernet
    ip4: 192.168.1.100/24
    gw4: 192.168.1.1
    dns4:
      - 8.8.8.8
      - 8.8.4.4
    state: present

- name: Configure hostname
  ansible.builtin.hostname:
    name: web1.example.com

- name: Manage /etc/hosts
  ansible.builtin.lineinfile:
    path: /etc/hosts
    line: "192.168.1.100 web1.example.com web1"
    state: present

8. Variables, Facts, and Precedence

Variable Definition Locations

# In playbook vars section
vars:
  http_port: 80

# In external file
vars_files:
  - vars/common.yml
  - "vars/{{ ansible_os_family }}.yml"

# From command line
# ansible-playbook site.yml -e "http_port=8080"

# In inventory (host_vars/, group_vars/)
# In roles (defaults/main.yml, vars/main.yml)
# Registered from task output

Variable Precedence (lowest to highest)

  1. Role defaults (roles/x/defaults/main.yml)
  2. Inventory file or script group vars
  3. group_vars/all
  4. group_vars/<group>
  5. Inventory file or script host vars
  6. host_vars/<host>
  7. Play vars_files
  8. Play vars
  9. Play vars_prompt
  10. Task vars
  11. include_vars
  12. set_fact / registered vars
  13. Role vars (roles/x/vars/main.yml)
  14. Block vars
  15. Task vars (only for the task)
  16. Extra vars (-e) — always win

Facts

# Access facts
- name: Show OS distribution
  ansible.builtin.debug:
    msg: "OS is {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"

# Alternative dot notation
- name: Show IP
  ansible.builtin.debug:
    msg: "IP is {{ ansible_default_ipv4.address }}"

# Custom facts
# Place .fact files in /etc/ansible/facts.d/ on managed nodes
# Accessible via ansible_local.<factname>

# Disable fact gathering
- hosts: all
  gather_facts: false

# Cache facts
# ansible.cfg:
# [defaults]
# fact_caching = jsonfile
# fact_caching_connection = /tmp/ansible_facts
# fact_caching_timeout = 3600

Registered Variables

- name: Check if file exists
  ansible.builtin.stat:
    path: /etc/myapp.conf
  register: myapp_conf

- name: Create config if missing
  ansible.builtin.template:
    src: myapp.conf.j2
    dest: /etc/myapp.conf
  when: not myapp_conf.stat.exists

- name: Capture command output
  ansible.builtin.command: cat /etc/hostname
  register: hostname_output
  changed_when: false

- name: Display hostname
  ansible.builtin.debug:
    var: hostname_output.stdout

Magic Variables

Variable Description
hostvars All variables for all hosts
groups All groups and their hosts
group_names Groups the current host belongs to
inventory_hostname Name of the current host from inventory
ansible_play_hosts Active hosts in the current play
ansible_check_mode True if running in check mode

9. Conditionals, Loops, and Error Handling

Conditionals (when)

- name: Install on RHEL only
  ansible.builtin.dnf:
    name: httpd
    state: present
  when: ansible_facts['os_family'] == "RedHat"

- name: Multiple conditions (AND)
  ansible.builtin.dnf:
    name: httpd
    state: present
  when:
    - ansible_facts['os_family'] == "RedHat"
    - ansible_facts['distribution_major_version'] | int >= 8

- name: OR condition
  ansible.builtin.debug:
    msg: "This is a web server"
  when: "'webservers' in group_names or 'proxy' in group_names"

- name: Check variable is defined
  ansible.builtin.debug:
    msg: "Port is {{ http_port }}"
  when: http_port is defined

Loops

# Simple list
- name: Install packages
  ansible.builtin.dnf:
    name: "{{ item }}"
    state: present
  loop:
    - httpd
    - firewalld
    - php

# Better — pass list to module directly
- name: Install packages (preferred)
  ansible.builtin.dnf:
    name:
      - httpd
      - firewalld
      - php
    state: present

# Dict loop
- name: Create users
  ansible.builtin.user:
    name: "{{ item.name }}"
    group: "{{ item.group }}"
    state: present
  loop:
    - { name: alice, group: developers }
    - { name: bob, group: admins }

# Loop with index
- name: Show loop index
  ansible.builtin.debug:
    msg: "{{ index }}: {{ item }}"
  loop:
    - alpha
    - bravo
  loop_control:
    index_var: index

# Loop with label (cleaner output)
- name: Create users with label
  ansible.builtin.user:
    name: "{{ item.name }}"
  loop: "{{ users }}"
  loop_control:
    label: "{{ item.name }}"

Handlers

tasks:
  - name: Update httpd config
    ansible.builtin.template:
      src: httpd.conf.j2
      dest: /etc/httpd/conf/httpd.conf
    notify:
      - Restart httpd
      - Verify httpd

handlers:
  - name: Restart httpd
    ansible.builtin.service:
      name: httpd
      state: restarted

  - name: Verify httpd
    ansible.builtin.uri:
      url: http://localhost/
      status_code: 200

# Force handlers to run mid-play
  - name: Flush handlers now
    ansible.builtin.meta: flush_handlers

Tags

tasks:
  - name: Install packages
    ansible.builtin.dnf:
      name: httpd
      state: present
    tags:
      - install
      - packages

  - name: Configure httpd
    ansible.builtin.template:
      src: httpd.conf.j2
      dest: /etc/httpd/conf/httpd.conf
    tags:
      - configure

# Run only tagged tasks
# ansible-playbook site.yml --tags "install"

# Skip tagged tasks
# ansible-playbook site.yml --skip-tags "configure"

# Special tags: always, never
  - name: Always run this
    ansible.builtin.debug:
      msg: "Always runs"
    tags: always

Error Handling

# Ignore errors
- name: Try to stop a service that may not exist
  ansible.builtin.service:
    name: nonexistent
    state: stopped
  ignore_errors: true

# Block / rescue / always
- name: Handle errors gracefully
  block:
    - name: Attempt risky operation
      ansible.builtin.command: /opt/risky-script.sh

    - name: This only runs if above succeeds
      ansible.builtin.debug:
        msg: "Script succeeded"
  rescue:
    - name: This runs if block fails
      ansible.builtin.debug:
        msg: "Script failed, running recovery"
  always:
    - name: This always runs
      ansible.builtin.debug:
        msg: "Cleanup complete"

# Custom changed_when / failed_when
- name: Run a script
  ansible.builtin.command: /opt/check.sh
  register: check_result
  changed_when: "'CHANGED' in check_result.stdout"
  failed_when: "'CRITICAL' in check_result.stdout"

10. Jinja2 Templates

Template Basics

{# This is a comment #}

{# Variable substitution #}
ServerName {{ ansible_fqdn }}
Listen {{ http_port }}

{# Conditional #}
{% if enable_ssl %}
Listen 443
SSLEngine on
{% endif %}

{# Loop #}
{% for host in groups['webservers'] %}
server {{ host }} {{ hostvars[host]['ansible_default_ipv4']['address'] }}:80;
{% endfor %}

{# Filter #}
ServerAdmin {{ admin_email | default('admin@example.com') }}
DocumentRoot {{ doc_root | default('/var/www/html') }}

Common Filters

Filter Example Description
default {{ var \| default('fallback') }} Default value if undefined
lower / upper {{ name \| upper }} Case conversion
replace {{ path \| replace('/', '-') }} String replacement
join {{ list \| join(', ') }} Join list to string
int / float {{ port \| int }} Type casting
bool {{ val \| bool }} Cast to boolean
password_hash {{ pw \| password_hash('sha512') }} Password hashing
to_json / to_yaml {{ dict \| to_json }} Format conversion
regex_replace {{ s \| regex_replace('a', 'b') }} Regex replace
ipaddr {{ ip \| ansible.utils.ipaddr }} IP address validation

Template Task

- name: Deploy Nginx config
  ansible.builtin.template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    validate: nginx -t -c %s
  notify: Restart nginx

11. Roles

Role Structure

roles/
  webserver/
    defaults/       # Default variables (lowest precedence)
      main.yml
    vars/           # Role variables (high precedence)
      main.yml
    tasks/          # Main task list
      main.yml
    handlers/       # Handler definitions
      main.yml
    templates/      # Jinja2 templates
    files/          # Static files
    meta/           # Role metadata and dependencies
      main.yml

Creating a Role

# Scaffold a role
ansible-galaxy role init webserver

# Or create manually
mkdir -p roles/webserver/{tasks,handlers,templates,files,vars,defaults,meta}

Role tasks/main.yml

---
- name: Install httpd
  ansible.builtin.dnf:
    name: "{{ webserver_packages }}"
    state: present

- name: Deploy config
  ansible.builtin.template:
    src: httpd.conf.j2
    dest: /etc/httpd/conf/httpd.conf
  notify: Restart httpd

- name: Start httpd
  ansible.builtin.service:
    name: httpd
    state: started
    enabled: true

Role defaults/main.yml

---
webserver_packages:
  - httpd
  - mod_ssl
http_port: 80
doc_root: /var/www/html

Role meta/main.yml

---
dependencies:
  - role: common
  - role: firewall
    vars:
      firewall_services:
        - http
        - https

Using Roles in Playbooks

---
- name: Configure webservers
  hosts: webservers
  become: true

  roles:
    - common
    - webserver
    - role: firewall
      vars:
        firewall_services:
          - http

# Or with include_role (dynamic)
  tasks:
    - name: Include webserver role
      ansible.builtin.include_role:
        name: webserver
      vars:
        http_port: 8080

# Or with import_role (static)
    - name: Import webserver role
      ansible.builtin.import_role:
        name: webserver

12. Ansible Galaxy and Collections

Installing Roles from Galaxy

# Install a single role
ansible-galaxy role install geerlingguy.apache

# Install from requirements file
ansible-galaxy role install -r requirements.yml

requirements.yml

---
roles:
  - name: geerlingguy.apache
    version: "3.2.0"
  - name: geerlingguy.mysql

collections:
  - name: ansible.posix
    version: ">=1.5.0"
  - name: community.general
  - name: community.crypto

Installing Collections

# Install a collection
ansible-galaxy collection install ansible.posix

# Install from requirements.yml
ansible-galaxy collection install -r requirements.yml

# Install to a specific path
ansible-galaxy collection install ansible.posix -p ./collections

Using FQCN (Fully Qualified Collection Name)

# Always use FQCN in playbooks
- name: Set SELinux boolean
  ansible.posix.seboolean:
    name: httpd_can_network_connect
    state: true
    persistent: true

# Not just "seboolean" — use the full path

13. Parallelism

# In ansible.cfg
[defaults]
forks = 10   # How many hosts to manage in parallel

# Serial execution (rolling updates)
- hosts: webservers
  serial: 2          # 2 hosts at a time
  tasks: ...

# Percentage-based serial
- hosts: webservers
  serial: "25%"

# Stepped serial
- hosts: webservers
  serial:
    - 1      # First batch: 1 host (canary)
    - 5      # Second batch: 5 hosts
    - "100%" # Remaining hosts

# Async tasks (fire and forget)
- name: Long-running task
  ansible.builtin.command: /opt/long-job.sh
  async: 3600    # Max runtime in seconds
  poll: 0        # Don't wait (fire and forget)
  register: long_job

- name: Check on long job
  ansible.builtin.async_status:
    jid: "{{ long_job.ansible_job_id }}"
  register: job_result
  until: job_result.finished
  retries: 60
  delay: 10

# Throttle (limit concurrent tasks across hosts)
- name: Rate-limited API call
  ansible.builtin.uri:
    url: "https://api.example.com/deploy"
  throttle: 2    # Max 2 concurrent

14. Ansible Vault

Encrypting Files

# Create a new encrypted file
ansible-vault create secrets.yml

# Encrypt an existing file
ansible-vault encrypt vars/passwords.yml

# View encrypted file
ansible-vault view secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Decrypt a file
ansible-vault decrypt secrets.yml

# Change password
ansible-vault rekey secrets.yml

Encrypting Strings

# Encrypt a single variable value
ansible-vault encrypt_string 'SuperSecret123' --name 'db_password'

# Output (paste into vars file):
# db_password: !vault |
#   $ANSIBLE_VAULT;1.1;AES256
#   ...encrypted data...

Using Vault at Runtime

# Prompt for password
ansible-playbook site.yml --ask-vault-pass

# Use password file
ansible-playbook site.yml --vault-password-file ~/.vault_pass

# Multiple vault IDs
ansible-vault encrypt --vault-id dev@prompt secrets-dev.yml
ansible-vault encrypt --vault-id prod@~/.vault_prod secrets-prod.yml
ansible-playbook site.yml --vault-id dev@prompt --vault-id prod@~/.vault_prod

Vault in Practice

# vars/secrets.yml (encrypted)
---
db_password: SuperSecret123
api_key: abcdef1234567890

# playbook references it normally
- hosts: dbservers
  become: true
  vars_files:
    - vars/common.yml
    - vars/secrets.yml
  tasks:
    - name: Configure database
      ansible.builtin.template:
        src: db.conf.j2
        dest: /etc/myapp/db.conf

15. Using Documentation

ansible-doc

# List all modules
ansible-doc -l

# Search for modules
ansible-doc -l | grep firewall

# View module documentation
ansible-doc ansible.builtin.dnf
ansible-doc ansible.builtin.copy
ansible-doc ansible.posix.firewalld

# Show only examples
ansible-doc ansible.builtin.user -s

# List plugins by type
ansible-doc -t callback -l
ansible-doc -t lookup -l
ansible-doc -t filter -l

This is critical on the exam — you have access to ansible-doc and the local docs, but NOT the internet.


16. Ansible Collections

Understanding Collections

Collections bundle: - Modules - Roles - Plugins (lookup, filter, callback, connection)

Namespace format: namespace.collection (e.g., ansible.posix, community.general)

Key Collections for RHCE

Collection Contents
ansible.builtin Core modules (dnf, copy, service, template, file, etc.)
ansible.posix SELinux, firewalld, mount, authorized_key, cron, sysctl
community.general nmcli, parted, lvg, lvol, filesystem, sefcontext, timezone
community.crypto Certificate management

Installing and Using

# Install
ansible-galaxy collection install community.general

# List installed collections
ansible-galaxy collection list

# Use in playbooks with FQCN
- community.general.nmcli:
    conn_name: eth0
    type: ethernet
    ...

Exam Day Tips

  1. Read the objectives carefully. Each task maps to a specific skill.
  2. Use ansible-doc extensively. It's your only reference.
  3. Always use FQCN for module names (ansible.builtin.copy, not just copy).
  4. Test incrementally. Run --syntax-check then --check --diff before full runs.
  5. Manage your time. Don't get stuck — move on and come back.
  6. Idempotency matters. Your playbooks must be re-runnable without errors.
  7. Check your work. Run the playbook twice — second run should show no changes.
  8. Don't forget handlers. If config changes need a service restart, use handlers.
  9. Variable precedence. Extra vars (-e) always win. Role defaults are lowest.
  10. Vault password. They will tell you the vault password — note it immediately.

Wiki Navigation

Prerequisites