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
Is CertGrid a hands-on Linux lab simulator?
No. The real Red Hat Certified Ansible (EX294-style) exam is a hands-on, performance-based lab exam. CertGrid provides MCQ-style readiness practice to help you check concepts, commands, troubleshooting choices, and weak domains before doing hands-on labs - it is not a live lab simulator.
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.