Courseiva
EX294Chapter 9 of 18Objective 4.1

Roles and Role-Based Automation

Roles and role-based automation. This concept solves the problem of spaghetti code in your automation — where everything is mixed together in one giant file, making it impossible to reuse, share, or debug. For the EX294 exam, understanding roles is critical because they are the standard way to organise Ansible automation in the real world, and the exam tests whether you can structure your code like a professional.

12 min read
Intermediate
Updated Jul 23, 2026
Editorial oversight: Johnson Ajibi· Security Systems Architect · MSc IT Security · IEEE Senior Member

A simple way to picture Roles and Role-Based Automation

The Head Chef Analogy

The Head Chef in a busy restaurant kitchen. They don't chop every vegetable or stir every sauce themselves. Instead, they have a system. They have a binder of standardised recipe cards. Each recipe card is a detailed, repeatable set of instructions for a specific dish, like 'Pan-Seared Salmon'. The recipe card doesn't care whether a junior chef or the sous-chef makes the salmon. It just says: '1. Season the fillet. 2. Heat oil in a pan. 3. Sear for 4 minutes per side.' This is a 'role'.

When the head chef needs to prepare a full menu for a banquet, they don't rewrite every recipe from scratch. They create a 'playbook' called 'Banquet Menu'. This playbook lists the recipe cards in order: 'Appetiser: Oysters Rockefeller (recipe card #3)', 'Main: Beef Wellington (recipe card #7)', 'Dessert: Creme Brulee (recipe card #12)'. If the head chef wants to change the main course for a different event, they don't tear up all the other recipe cards. They just swap out 'recipe card #7' for 'recipe card #15: Lamb Chops'. The individual recipe cards (roles) are reusable. The banquet menu (playbook) is a specific order of operations.

Before this system, the head chef had to tell every single chef exactly what to do for every single meal, every single time. It was exhausting, error-prone, and if the head chef was sick, nobody knew how to make the 'famous soup'. Now, with recipe cards (roles), anyone can follow the steps. The kitchen runs consistently, even with different staff. This is exactly what a DevOps engineer does with Ansible roles: package up a standard, repeatable 'recipe' for configuring a server, and then call those recipes in different orders and combinations to build different types of servers.

How It Actually Works

Let's strip this right down. Imagine you're writing instructions for a robot to set up a new computer in an office. Your first attempt might be one long list: 'Install Linux', 'Create user Alice', 'Install web server', 'Configure firewall', 'Install database'. This is called a 'flat playbook'. It works, but now imagine you have to do this for 100 different computers, some of which are web servers, some are databases, and some are just basic desktops. You'd end up with 100 slightly different giant lists. This is a maintenance nightmare. If you need to update the 'configure firewall' part, you have to find and edit it in all 100 lists.

Ansible roles are a way to break that giant list into smaller, reusable 'packages' or 'components'. A role is a self-contained unit of automation that does one specific thing. Think of it like a Lego brick. One brick is the 'Install and configure web server' brick. Another is the 'Install and configure database' brick. Another is the 'Create standard users' brick. Each brick has all the instructions it needs inside its own folder structure.

A role's folder structure is a standardised directory layout. It must have certain folders, but you only create the ones you need. The main ones are:

tasks: This is the main event. It contains a file called 'main.yml' which lists the actual Ansible tasks (the individual steps) the role performs.

handlers: This folder contains 'main.yml' with special tasks that are only run when notified by other tasks (e.g., 'restart apache' after a config file change).

defaults: This folder contains a 'main.yml' file that defines the lowest priority default variables for the role (e.g., 'webserver_port: 80').

vars: This folder contains a 'main.yml' file for higher priority variables. Use 'defaults' more often.

files: A place to store static files you want to copy to the target machine (like an image or a tarball).

templates: A place for Jinja2 template files (config files with variables that get filled in at runtime, like a config file with '{{ ansible_fqdn }}').

meta: Contains 'main.yml' for role dependencies (e.g., 'this web role depends on the common security role being applied first').

Now, how do you use these Lego bricks (roles)? In your main playbook, you don't write tasks directly. Instead, you use the 'roles:' keyword and list the roles you want to apply. For example:

- --- - name: Set up a web server hosts: webservers roles: - common - nginx - firewall

This playbook says: 'Go to every computer in the 'webservers' group. First, run the 'common' role (which might set up users and basic security). Then, run the 'nginx' role (which installs and configures the web server). Finally, run the 'firewall' role.'

Why does this matter? Reusability. You can apply the 'nginx' role to any computer that needs to be a web server. You can apply the 'firewall' role to every single computer. You can share the 'nginx' role with your team or download a pre-made one from Ansible Galaxy (a public library of roles). It's like having a toolbox full of specialised tools instead of one giant Swiss Army knife that tries to do everything. It's cleaner, more manageable, and it's how professional Ansible users organise their work. This is what you must do for EX294.

This diagram shows how a main playbook (site.yml) applies different roles to different groups of servers, and how a single role (nginx) contains its own internal structure of tasks, handlers, defaults, and templates.

Walk-Through

1

Identify the Component

Decide what single, specific function you want to package. For example, 'Install and configure Nginx web server'. This is the component your role will model.

2

Create the Role Skeleton

Run 'ansible-galaxy init nginx' in your ansible control node. This creates a folder called 'nginx/' with the standard subdirectories: tasks, handlers, defaults, vars, files, templates, meta.

3

Define the Tasks

Open 'roles/nginx/tasks/main.yml' and write the Ansible tasks that install the Nginx package, copy configuration files, and start the service. This is the core logic of the role.

4

Add Variables and Templates

If your Nginx configuration changes based on the environment (e.g., different server names), define a variable in 'defaults/main.yml' (e.g., nginx_server_name: localhost) and create a Jinja2 template file for the Nginx config in 'templates/nginx.conf.j2' that uses that variable.

5

Use the Role in a Playbook

Create a playbook file (e.g., 'site.yml') and add a 'roles:' section under a play. List 'nginx' as one of the roles. Run 'ansible-playbook site.yml' to apply your role to the target hosts.

6

Test and Refactor

Run your playbook against a test server. Fix any errors. If you need to use this role in another playbook that also needs a firewall, consider creating a separate 'firewall' role instead of adding firewall tasks to the Nginx role.

What This Looks Like on the Job

An IT professional at a mid-sized company called 'CloudCanvas' is tasked with automating the setup of their new microservices infrastructure. They need to set up 50 servers: 30 running a Python web app, 10 running a PostgreSQL database cluster, and 10 running Redis caches. Before using roles, the junior engineer wrote a single 800-line playbook that tried to install the web app, configure the database, and set up Redis differently based on 'when' conditions (e.g., 'when: inventory_hostname in groups['webservers']'). It was a nightmare. Every time a new server type was added, they had to add more 'when' conditions, making the playbook harder to read and test.

The senior engineer steps in. Heres how she fixes it using roles:

Step 1: She creates three roles using the 'ansible-galaxy init' command. - ansible-galaxy init roles/python-web-app (creates the standardised folder structure) - ansible-galaxy init roles/postgres-db - ansible-galaxy init roles/redis-cache

Step 2: She writes the specific tasks for each role in the 'tasks/main.yml' file. - For 'python-web-app': tasks include installing Python, cloning the Git repo, setting up a virtual environment, installing pip dependencies, and starting the Gunicorn service. - For 'postgres-db': tasks include installing PostgreSQL, creating database users, setting up replication, and opening the firewall port. - For 'redis-cache': tasks include installing Redis, configuring memory limits, and enabling persistence.

Step 3: She creates a main playbook called 'site.yml':

- --- - name: Deploy Python Web Application Servers hosts: webservers roles: - python-web-app - common-security

- name: Deploy Database Cluster hosts: databases roles: - postgres-db - common-security

- name: Deploy Redis Cache Nodes hosts: redis roles: - redis-cache - common-security

Step 4: She puts the 'site.yml' file in a version control repository (Git). She puts the 'roles/' directory next to it.

Step 5: When a new developer joins the team and needs to spin up a test environment on their laptop, they can clone the repo and run 'ansible-playbook site.yml'. The playbook applies the roles to the correct groups. If the database team needs to update the PostgreSQL version, they only edit the 'postgres-db' role. No other role is affected. If a new server type is needed (e.g., a monitoring server), the team creates a new role called 'monitoring-stack'. The entire automation library grows organically, cleanly, and is understandable by anyone on the team. This is the reality of infrastructure as code.

How EX294 Actually Tests This

The EX294 exam very specifically tests your ability to create, use, and understand the structure of Ansible roles. It is not a theoretical exam; it expects you to know the practical mechanics. Here is what you must master:

- Folder Structure: You must memorise the role directory layout. The exam will ask questions like 'Which directory within a role contains the variables with the lowest precedence?' (Answer: defaults/). Or 'Which file contains the main list of tasks for a role?' (Answer: tasks/main.yml). They love testing the difference between 'defaults/main.yml' and 'vars/main.yml' because beginners always confuse them. Remember: defaults are for variables the user of the role can easily override; vars are for internal variables that should not be changed. - Using Roles in Playbooks: You must know the two ways to use roles: 1. The 'roles:' directive at the play level (as shown above). 2. Using the 'include_role' module inside a tasks block for conditional imports (e.g., 'include_role: name=firewall when: ansible_facts['os_family'] == "Debian"'). - Common Traps:

Traps about Pre/Post Tasks: In Ansible, 'pre_tasks' run before any roles, and 'post_tasks' run after all roles. The exam might ask you to order a sequence of operations including these.

Dependency Hell: The 'meta/main.yml' file contains 'dependencies'. The exam will test if you understand that if role A depends on role B, role B runs BEFORE role A, even if A is listed first in the playbook.

Role Search Path: Ansible looks for roles in a specific path. The default is 'roles/' in the playbook directory, plus the path set by the 'roles_path' configuration in ansible.cfg. The exam might give you a scenario and ask where Ansible will look for a role.

Key Concepts to Memorise:

- Role structure: tasks, handlers, defaults, vars, files, templates, meta. - ansible-galaxy init command creates the skeleton. - The 'ansible-galaxy install' command downloads roles from Ansible Galaxy. - Variable precedence: You must know that 'vars/main.yml' takes higher precedence than 'defaults/main.yml'. - Question Types: Expect multiple-choice questions that ask you to identify the correct path for a file (e.g., 'Where do you put a custom Jinja2 template for an Nginx config in the nginx role?'). You will also get short-answer lab-style tasks where you must create a role from scratch in a test environment. Practise writing a role that installs and configures a single package (like 'ntp' or 'httpd'). The exam environment is CentOS/RHEL based.

Key Takeaways

An Ansible role is a standardised directory structure that bundles tasks, variables, files, templates, and handlers for a specific purpose, making automation reusable.

The 'roles' keyword in a playbook applies roles in the order they are listed before any tasks defined in the playbook.

Variables placed in 'defaults/main.yml' within a role have the lowest precedence and are designed to be overridden by the user of the role.

Variables placed in 'vars/main.yml' within a role have a higher precedence and are intended for internal role logic that should not be changed.

The 'ansible-galaxy init' command creates the skeleton directory structure for a new role, saving you from creating folders manually.

Role dependencies defined in 'meta/main.yml' cause the dependency role to run before the dependent role, regardless of the order they are listed in the playbook.

Handlers inside a role should be used for actions that need to run only when notified, like restarting a service after a configuration change.

You can use the 'include_role' module to conditionally apply a role mid-playbook, offering more flexibility than the static 'roles' clause.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

Roles

Standardised directory structure with dedicated subdirectories for tasks, handlers, defaults, vars, files, templates, and meta.

Bundle everything for a specific function (tasks, config files, templates) into one reusable unit.

Can be shared via Ansible Galaxy with metadata and versioning.

Include Tasks

Just a single file containing a list of tasks (e.g., 'tasks/install-nginx.yml').

No special directory structure; just tasks that are included into a playbook.

Cannot be easily packaged or shared without extra documentation; no built-in metadata.

Default Variables (defaults/main.yml)

Lowest variable precedence; designed to be overridden by playbook, inventory, or command line.

Intended for configuration options that users of the role are expected to customise.

Typically set to reasonable default values like port numbers or server names.

Vars Variables (vars/main.yml)

Higher precedence than 'defaults'; not easily overridden by playbook variables.

Intended for internal role logic or constants that should remain fixed.

Often used for things like package names or paths that must be exactly defined.

Static Import (import_role)

Parsed at the time the playbook is read; all tasks are added to the execution plan immediately.

Variable scope is limited to the play level; role variables are available as if they were part of the play.

Used when you need the role to always run during the playbook execution.

Dynamic Include (include_role)

Parsed when the playbook reaches that specific line during execution; tasks are added at that moment.

Variable scope is more flexible; you can pass variables directly in the 'include_role' call.

Used for conditional or looped role execution (e.g., only run the role for Debian hosts).

Watch Out for These

Mistake

Roles are just a way to organise playbook files into separate .yml files.

Correct

Roles are a standardised directory structure with specific subdirectories (tasks, handlers, defaults, etc.) that encapsulates all the needed resources for a purpose.

Beginners see that roles break up a large playbook and assume it's just a 'split' feature. They neglect the special folder system.

Mistake

Variables in 'defaults/main.yml' are 'default' and cannot be overridden.

Correct

Variables in 'defaults/main.yml' have the lowest precedence and are the easiest to override; they are designed to be overridden.

The word 'default' makes people think it's a fallback that is 'stuck', missing the entire point of using role defaults for flexibility.

Mistake

You must always create all the role subdirectories (files, templates, handlers) even if they are empty.

Correct

You only need to create the subdirectories you actually use. Ansible won't complain if 'handlers' or 'files' are missing.

Beginners over-engineer the structure because they think the exam requires 'completeness', adding unnecessary complexity to their code.

Mistake

Roles can only be stored locally in the 'roles/' directory next to the playbook.

Correct

Roles can be stored in multiple locations, including Ansible Galaxy, a private Git repository, a tarball, or any path defined by 'roles_path' in ansible.cfg.

Beginners work only with local 'roles/' folders in labs and don't realise how roles are distributed in a production team environment.

Mistake

The 'include_role' and 'import_role' modules are functionally identical and can be used interchangeably.

Correct

'include_role' is dynamic (processed when the play reaches that task), and 'import_role' is static (processed when the playbook is parsed). This affects variable scope and when the role tasks are added to the execution plan.

The names sound similar, and new learners often skip the distinction until they hit weird behaviour with variable scoping.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

What is the difference between 'defaults/main.yml' and 'vars/main.yml' in an Ansible role?

The difference is precedence. Variables in 'defaults/main.yml' have the lowest precedence and are intended to be easily overridden. Variables in 'vars/main.yml' have higher precedence and are used for internal role logic that should not normally be changed.

Can I use tasks defined inside a role in the middle of my playbook, not at the start?

Yes, you can use the 'include_role' module inside a tasks block. This allows you to run a role after some of your own tasks, or conditionally run a role based on a 'when' condition.

How do I download a role from Ansible Galaxy?

Use the command 'ansible-galaxy install <username.rolename>'. For example, 'ansible-galaxy install geerlingguy.nginx' installs a popular Nginx role into your configured roles path.

What happens if I have a playbook that uses a role, but the role directory doesn't exist?

Ansible will throw an error saying it cannot find the role. It will search in the default 'roles/' directory relative to the playbook, and any paths set in the 'roles_path' configuration.

Can a role have variables that are different for different environments (like development vs. production)?

Yes, you set default values in 'defaults/main.yml' (like 'nginx_port: 80'), and then in your playbook or inventory, you can override that variable (e.g., 'nginx_port: 8080') for specific hosts or groups.

What is the 'meta/main.yml' file in a role used for?

The 'meta/main.yml' file is used to define role dependencies. For example, if your web server role depends on a firewall role, you list 'dependencies: [{ role: firewall }]' in the meta file. Ansible will then apply the firewall role before the web server role.

Terms Worth Knowing

Keep going

You've finished Roles and Role-Based Automation. Continue through the EX294 study guide to build a complete picture of the exam.

Done with this chapter?