Red Hat Certified Ansible (EX294-style) Study Guide
The Red Hat Certified Ansible exam (EX294-style) validates your ability to automate Linux system administration at scale using Ansible: writing playbooks and roles, managing inventories, templating configuration with Jinja2, handling variables and secrets, and ensuring tasks run idempotently. It is a hands-on, performance-based exam aimed at sysadmins, DevOps engineers, and automation practitioners who manage fleets of systems. Expect to build and run real playbooks against managed nodes rather than answer purely theoretical questions.
Domain 1: Ansible Fundamentals
- Ansible is agentless: it manages nodes over SSH (Linux) or WinRM (Windows) with no persistent agent installed on the targets, only Python on the managed node for most modules.
- Idempotency means re-running the same playbook converges to the desired state and reports 'ok' (no changes) when the system is already correct, instead of repeating changes.
- Check mode (ansible-playbook site.yml --check or -C) performs a dry run that reports what would change without actually modifying any host.
- Ad-hoc commands run a single module against hosts without a playbook: ansible <pattern> -m <module> -a '<args>', e.g. ansible all -m ping or ansible all -m ansible.builtin.setup.
- ansible-playbook site.yml runs a full playbook; --syntax-check parses YAML and play structure without connecting; --list-hosts shows which hosts a play would target.
- The forks setting (ansible.cfg [defaults] forks=, or -f on the CLI, default 5) controls how many hosts Ansible configures in parallel.
- SSH pipelining (pipelining=True in ansible.cfg) and ControlPersist reuse persistent SSH connections across tasks, reducing connection overhead and dramatically speeding up large runs.
- serial at the play level (e.g. serial: 2 or serial: 25%) controls how many hosts are updated per batch, enabling rolling updates that limit blast radius.
- Use ansible-doc <module> (e.g. ansible-doc ansible.builtin.copy) to read module documentation and examples directly from the command line.
- ansible-navigator and Execution Environments (container images built with ansible-builder) package Ansible plus its Python and collection dependencies for portable, reproducible runs.
- AWX / Automation Controller front the agentless engine with RBAC, scheduling, surveys, credential storage, and a web UI; execution nodes distribute job load across the fleet.
- Privilege escalation uses become (become: true, become_user, become_method: sudo by default); -b on the CLI is shorthand for --become.
- The command module does not run through a shell, so pipes, redirects, and globbing are not interpreted; use the shell module when you need shell features.
- If gather_facts is disabled and you reference a fact, the variable is undefined and the task fails because facts were never collected for that host.
Domain 2: Inventory
- Inventory defines the managed hosts and groups Ansible operates on, plus optional host and group variables, in INI or YAML format.
- Static inventory lists hosts explicitly; dynamic inventory uses plugins (e.g. amazon.aws.aws_ec2, azure_rm) to auto-discover hosts from a cloud or CMDB so the inventory stays accurate as instances change.
- Per-host connection variables are set inline, e.g. web1 ansible_host=10.0.0.5 ansible_port=2222 ansible_user=deploy.
- Groups let you target logically related hosts; a group is declared with [webservers] in INI, and hosts can belong to multiple groups.
- Special built-in groups: 'all' matches every host and 'ungrouped' matches hosts not in any user-defined group.
- Group variables live in group_vars/<group>.yml and host variables in host_vars/<host>.yml, kept alongside the inventory or playbook.
- Use --limit (or host patterns) to restrict a run to specific hosts or groups; patterns support set logic like 'web:!staging' (web group except staging) and 'web:&prod' (intersection).
- ansible-inventory --list -i inventory.ini renders the parsed inventory as JSON, useful for verifying dynamic inventory and variable resolution; --graph shows the group hierarchy.
- Enable inventory caching (cache: true with a cache plugin and timeout) so dynamic inventory plugins do not re-query the cloud API on every run.
- keyed_groups and the constructed plugin auto-create groups from instance metadata such as cloud tags, so hosts are grouped without manual edits.
- Multiple inventory sources can be combined by pointing -i at a directory; Ansible merges all sources found there.
- Variable precedence within inventory: host_vars override group_vars, and a child group's vars override its parent group's vars.
- You can target a subset of a play at runtime with --start-at-task="Configure app" to begin partway through a playbook.
- ansible_connection=local runs tasks on the control node itself rather than over SSH, useful for controller-side or localhost tasks.
Domain 3: Playbooks and Tasks
- A play maps a group of hosts (hosts:) to an ordered set of tasks and roles; a playbook is one or more plays in a YAML list.
- A task invokes a single module (e.g. ansible.builtin.dnf, copy, service) with arguments to perform one idempotent action.
- Handlers are special tasks listed under handlers: that run only when triggered by notify, and only once, at the end of the play (or earlier with meta: flush_handlers).
- Conditionals use when: with a Jinja2 expression (no surrounding {{ }} needed), e.g. when: ansible_os_family == 'RedHat'.
- Loops use loop: (preferred) or the older with_items:; loop_control with label and index_var customizes loop output and indexing.
- Error handling: ignore_errors: true continues past failures, and block/rescue/always provides try-catch-finally semantics around grouped tasks.
- command and shell are not inherently idempotent; add creates:/removes: paths or changed_when:/failed_when: to control their reported state.
- ansible.builtin.dnf (or yum) manages packages; pass a list to name: to install several in one transaction rather than looping per package.
- ansible.builtin.service / systemd manages services with state: started and enabled: true for boot persistence.
- ansible.builtin.lineinfile edits a single line, blockinfile manages a marked block, and ansible.builtin.unarchive extracts archives with src and dest.
- Tags let you run or skip subsets: tag tasks/roles and use --tags webserver or --skip-tags; ansible-playbook site.yml --tags webserver.
- run_once: true executes a task a single time for the whole batch (often combined with delegate_to: to run it on one specific host).
- Long-running tasks use async: <seconds> with poll: 0 for fire-and-forget, checking later with the async_status module.
- Notify a restart handler from the config task so the service restarts only when the configuration actually changed, preserving idempotency.
Domain 4: Variables and Templates
- The ansible.builtin.template module renders a Jinja2 .j2 template using variables and facts to a file on the managed host, with mode/owner/group control.
- Facts are system data gathered by the setup module (OS, IP, memory, etc.); reference them like ansible_facts['os_family'] or ansible_os_family.
- Disable fact gathering with gather_facts: false when not needed, and limit collection with gather_subset (e.g. gather_subset: min or '!all') to speed runs.
- Fact caching (jsonfile or redis cache plugin with a timeout) persists facts between runs so they are not re-gathered every execution.
- Variable precedence (low to high, abbreviated): role defaults < inventory/group_vars < host_vars < play vars < set_fact/registered < extra-vars (-e); -e always wins.
- Put easily overridable tunables in a role's defaults/main.yml (lowest precedence) and internal constants in vars/main.yml (higher precedence).
- set_fact creates variables at runtime; cacheable: true persists them to the fact cache, otherwise they are scoped to the current run.
- register captures a task's result into a variable; inspect .rc, .stdout, .changed, and .failed, and use it in later when: conditions.
- Jinja2 filters transform data, e.g. {{ value | default('x') }}, | int, | to_json, | regex_replace, and {{ list | join(',') }}.
- Ansible Vault encrypts secrets: ansible-vault encrypt secrets.yml, ansible-vault edit, and ansible-vault encrypt_string to embed a single encrypted value inline among readable vars.
- Supply the vault password at run time with --ask-vault-pass or --vault-password-file; the playbook fails to decrypt without it.
- Load OS-specific variable files with include_vars based on facts (e.g. "{{ ansible_os_family }}.yml") to keep one parameterized template.
- Keep complex logic out of templates: compute and shape data in the play with set_fact and filters, leaving templates focused on rendering.
- ansible.builtin.copy places static files (with optional content:), while template processes Jinja2; use validate: to check rendered config before replacing the live file.
Domain 5: Roles and Reuse
- A role is a reusable, structured bundle of tasks, handlers, variables, templates, and files for one unit of functionality, following a fixed directory layout.
- Standard role layout: tasks/, handlers/, templates/, files/, vars/, defaults/, meta/, and library/, each with a main.yml entry point where applicable.
- defaults/main.yml holds the lowest-precedence default variables meant to be easily overridden; vars/main.yml holds higher-precedence values not meant for casual override.
- Apply roles via the roles: keyword (runs before the play's tasks:), or with import_role / include_role inside tasks.
- import_role is static (parsed at parse time, so its tags and handlers are known up front) while include_role is dynamic (evaluated at runtime, supporting loops and when: on the include itself).
- pre_tasks run before roles, then roles run, then tasks:, then post_tasks; handlers notified anywhere run at the end of the play unless flushed early.
- meta/main.yml lists role dependencies, which run before the current role's tasks, plus Galaxy metadata like supported platforms.
- By default a role runs once per play even if listed twice; set allow_duplicates: true or invoke it with different parameters to run it again.
- Collections package roles, modules, and plugins under a namespace (namespace.collection.resource), e.g. community.general or ansible.posix.
- ansible-galaxy role init webserver scaffolds a new role; ansible-galaxy collection list shows installed collections.
- Pin versions in requirements.yml and install them with ansible-galaxy install -r requirements.yml (roles) and ansible-galaxy collection install -r requirements.yml in CI, rather than pulling latest.
- Collections install into the first writable path in the collections search path (e.g. ~/.ansible/collections) unless -p specifies a target.
- Roles do not automatically see each other's vars/; use set_fact (facts persist on the host for the play) or define shared values at play/group level to pass data between roles.
- Design single-responsibility roles parameterized by variables: this improves reuse, testability, selective application via tags/limits, and reduces the blast radius of changes.
Red Hat Certified Ansible (EX294-style) exam tips
- This is a hands-on exam: practice writing and running real playbooks against managed nodes, not just memorizing facts. Always re-run your playbook to confirm it is idempotent (second run reports zero changes).
- Lean on ansible-doc <module> during practice to recall exact module parameters and examples; knowing how to find syntax fast beats memorizing every option.
- Validate before you commit: use --syntax-check to catch YAML errors and --check (dry run) to preview changes, and watch indentation carefully since YAML is whitespace-sensitive.
- Use fully qualified collection names (e.g. ansible.builtin.copy) to avoid ambiguity, and confirm required collections are installed with ansible-galaxy collection list.
- Master variable precedence and Vault: know that -e extra-vars wins over everything, put overridable values in defaults/, and be ready to create and decrypt vaulted files with the correct vault password.
Study guide FAQ
What does it mean that Ansible is idempotent, and why does the exam care?
Idempotency means running a playbook repeatedly produces the same end state: tasks make changes only when needed and report 'ok' otherwise. Exam tasks are graded on the resulting system state, so non-idempotent constructs like raw command/shell without creates: or changed_when: can cause repeated changes or failures. Always design tasks so a second run reports no changes.
When should I use import_role versus include_role (and import_tasks versus include_tasks)?
Use the import_* (static) forms when you want everything parsed up front so tags and handlers are visible and there are no runtime loops over the inclusion. Use the include_* (dynamic) forms when you need to apply when: conditionals or loops to the inclusion itself or decide at runtime which content to run. Static is processed at parse time; dynamic is evaluated when the play reaches that point.
How do I manage secrets like passwords and API keys in playbooks?
Use Ansible Vault. Encrypt whole files with ansible-vault encrypt secrets.yml (edit later with ansible-vault edit), or encrypt only sensitive values inline with ansible-vault encrypt_string so the rest of the vars file stays readable. Provide the password at run time with --ask-vault-pass or --vault-password-file; never store secrets in plaintext in the repository.
Why did my task fail with an undefined variable when referencing a fact like ansible_default_ipv4?
Facts are only available if they were gathered. If a play sets gather_facts: false, or you limited collection with gather_subset and the needed fact was excluded, that fact variable is undefined and the task fails. Enable gather_facts, add an explicit setup task or include the right gather_subset, or enable fact caching so the values are available.