Module 6: Roles and Collections
Learning Objectives
By the end of this module you will be able to:
- Describe the role directory structure and naming conventions
- Explain when to use
defaults/main.ymlvsvars/main.yml - Scaffold roles and collections using
ansible-creator - Manage development environments with
ade - Create argument validation with
meta/argument_specs.yml - Use Fully Qualified Collection Names (FQCNs)
The Story So Far
Lionel and Jordan have been writing playbooks, managing inventory across environments, using variables and facts, and deploying configuration files with templates and handlers. The automation works well, but it lives in a growing pile of playbooks inside one directory, and other teams at Parasol Tech are starting to ask for access.
"The database team wants our nginx setup," Lionel says. "And the monitoring team keeps copying our template tasks into their own playbooks. Every copy drifts a little."
Jordan nods. "We need to package this. One source of truth for the web server configuration that any team can consume without copying files around."
This week, Parasol Tech's leadership sponsors a Community of Practice (CoP), a cross-team group dedicated to automation standards. The CoP's first decision: all reusable automation must be packaged as roles inside collections. No more copy-pasted playbooks.
What Are Roles?
A role is a self-contained unit of automation with a standardized directory structure. Instead of putting everything in a single playbook, you split the automation into well-defined directories (tasks, variables, templates, handlers, metadata), each in its own file. Ansible knows how to assemble these pieces automatically.
Think of a role as a function in programming. It takes inputs (variables), does work (tasks), and can be called from any playbook. The directory structure is the interface contract: anyone reading the role knows exactly where to find each piece.
Role Directory Structure
Every role follows a standard layout. Here is the structure of the webserver role we will build in this module:
roles/webserver/
defaults/
main.yml # User-facing variables with default values
vars/
main.yml # Internal constants (not for users)
tasks/
main.yml # The main task list
handlers/
main.yml # Handler definitions
templates/
webserver.conf.j2 # Jinja2 templates
index.html.j2
files/ # Static files (none in this role)
meta/
main.yml # Role metadata and dependencies
argument_specs.yml # Input validation
README.md # Documentation
Not every directory is required. Ansible only uses the directories that exist. But the naming is strict: tasks/main.yml, not tasks/install.yml, because Ansible looks for main.yml by convention.
Each directory has a specific purpose:
| Directory | Purpose |
|---|---|
defaults/ |
User-facing variables with default values. Lowest precedence. |
vars/ |
Internal variables and constants. High precedence, hard to override. |
tasks/ |
The task list that the role executes. |
handlers/ |
Handlers that tasks can notify. |
templates/ |
Jinja2 templates deployed by ansible.builtin.template. |
files/ |
Static files deployed by ansible.builtin.copy. |
meta/ |
Role metadata, dependencies, and argument validation. |
Splitting Tasks into Components
When a role grows large, you split tasks/main.yml into component files and include them:
# tasks/main.yml
- name: Install packages
ansible.builtin.include_tasks:
file: "{{ role_path }}/tasks/install.yml"
- name: Configure the service
ansible.builtin.include_tasks:
file: "{{ role_path }}/tasks/configure.yml"
- name: Manage the service lifecycle
ansible.builtin.include_tasks:
file: "{{ role_path }}/tasks/service.yml"
Notice the {{ role_path }} prefix. This is critical because it ensures the path resolves to the correct role, even when one role includes another. Never use relative paths like tasks/install.yml without it.
Always use {{ role_path }} for file references
Relative paths in include_tasks, include_vars, and template resolve against the including role, not necessarily your role. Use {{ role_path }}/tasks/, {{ role_path }}/vars/, and {{ role_path }}/templates/ to be explicit.
Naming Conventions
Naming is where most role problems start. When multiple roles run in the same play, their variables share a single namespace. If two roles both define a variable called packages, one will overwrite the other.
The rule is simple: prefix everything with the role name.
Variable Prefixing
# defaults/main.yml — CORRECT
webserver_port: 80
webserver_document_root: /var/www/html
webserver_server_name: localhost
# defaults/main.yml — WRONG (will collide with other roles)
port: 80
document_root: /var/www/html
server_name: localhost
This applies to:
- All variables in
defaults/main.yml - All variables in
vars/main.yml - All registered variables (
register: webserver_config_result) - All custom facts (
ansible.builtin.set_fact: webserver_detected_version: ...) - All tags (
tags: webserver_install)
Internal Variable Prefix
Variables that are internal to the role, not intended for users to override, get a double underscore prefix:
# vars/main.yml — internal constants
__webserver_packages_default:
- httpd
__webserver_service_name: httpd
__webserver_config_dir: /etc/httpd/conf
The double underscore signals "this is an implementation detail, do not set it in your inventory." Users configure the role through defaults/main.yml, not through these internal variables.
Handler Naming
Handlers also need the role prefix to avoid collisions. Use a naming convention that includes the role name:
# handlers/main.yml
- name: Validate webserver configuration
ansible.builtin.command:
cmd: "httpd -t"
changed_when: false
listen: "webserver_validate_config"
- name: Reload webserver
ansible.builtin.service:
name: "{{ __webserver_service_name }}"
state: reloaded
listen: "webserver_reload"
The listen directive lets tasks notify handlers by topic rather than by exact name. This is especially useful in roles because the handler name can be descriptive while the listen value follows a strict rolename_action pattern.
Role Names
Role names themselves must use underscores, never dashes:
defaults vs vars
This is one of the most important distinctions in role design, and getting it wrong causes real problems.
defaults/main.yml -- The User Interface
Variables in defaults/main.yml have the lowest precedence in Ansible's variable hierarchy. This means they can be overridden by almost anything: inventory variables, group vars, host vars, play vars, extra vars. That is exactly what you want for user-facing configuration.
Think of defaults/main.yml as the "API" of your role. It documents every knob the user can turn:
# defaults/main.yml
webserver_port: 80
webserver_document_root: /var/www/html
webserver_server_name: localhost
webserver_service_enabled: true
webserver_max_connections: 256
# webserver_admin_email:
# webserver_packages:
Notice the commented-out variables at the bottom. These are inputs that have no safe default value (like an admin email), so the role does not set one. But by listing them here, users know these options exist. The comments serve as documentation.
vars/main.yml -- Internal Constants
Variables in vars/main.yml have high precedence: they override inventory variables, group vars, and most other sources. Only extra vars (-e) can override them.
This makes vars/main.yml the wrong place for user-facing defaults. If you put webserver_port: 80 in vars/main.yml, users cannot override it from their inventory. They would need -e webserver_port=8080 on every run, which defeats the purpose.
Use vars/main.yml for values that should not change:
# vars/main.yml — internal constants
__webserver_packages_default:
- httpd
__webserver_service_name: httpd
__webserver_config_dir: /etc/httpd/conf
__webserver_config_file: httpd.conf
These are implementation details: the service name, the config directory path, the default package list. Users should not need to set these, and if they do override them by accident, bad things happen.
Never put user-facing defaults in vars/main.yml
The high precedence of vars/ makes variables nearly impossible to override from inventory. Always use defaults/main.yml for anything users should be able to customize.
Quick Reference
defaults/main.yml |
vars/main.yml |
|
|---|---|---|
| Precedence | Lowest (easily overridden) | High (hard to override) |
| Purpose | User-facing configuration | Internal constants |
| Naming | rolename_variable |
__rolename_variable |
| Can users override? | Yes, from inventory/group_vars | Only with -e extra vars |
| Contains | Sensible defaults, documented options | Service names, paths, magic values |
What Are Collections?
A collection is a distribution package for Ansible content. It bundles roles, plugins, modules, and documentation into a single artifact with a namespace, a version, and declared dependencies.
Before collections, sharing Ansible content meant distributing standalone roles through Ansible Galaxy. This worked, but it had problems: no namespacing (two people could create a role named nginx), no dependency management between roles, and no way to bundle roles with custom modules or plugins.
Collections solve all of these. A collection has a namespace and a name (like parasoltech.infrastructure) that guarantees uniqueness. It includes a galaxy.yml manifest that declares dependencies and versioning. And it can contain any combination of roles, modules, plugins, and documentation.
Collection Structure
parasoltech/infrastructure/
galaxy.yml # Collection manifest (name, version, deps)
README.md # Collection documentation
LICENSE # License file
meta/
runtime.yml # Minimum Ansible version requirement
plugins/ # Custom modules, filters, etc.
roles/
webserver/ # Roles live here
defaults/main.yml
tasks/main.yml
...
tests/ # Collection-level tests
docs/ # Additional documentation
The key file is galaxy.yml: it is the identity card of the collection.
Scaffolding with ansible-creator
You do not need to create all these directories and files by hand. The ansible-creator CLI tool generates the entire scaffolding for you.
Creating a Collection
ansible-creator init collection parasoltech.infrastructure \
~/ansible/collections/parasoltech/infrastructure
This creates the full directory structure with template files for galaxy.yml, README.md, LICENSE, meta/runtime.yml, and placeholder directories for plugins, roles, and tests.
The general syntax is:
VS Code integration
If you use the Ansible VS Code extension, you can also scaffold collections through a graphical wizard. Click the Ansible icon in the sidebar, then select Collection project. The wizard calls ansible-creator behind the scenes and produces the same result.
What about ansible-galaxy init?
You may also see ansible-galaxy collection init and ansible-galaxy role init used to scaffold collections and roles. These commands work, but ansible-creator is the newer, recommended tool because it generates a more complete project scaffold. Beyond the basic directory structure, ansible-creator includes devcontainer configurations, CI workflows, test infrastructure, and additional boilerplate that ansible-galaxy init does not provide. For new projects, prefer ansible-creator.
Creating a Role Inside a Collection
To add a role to an existing collection:
cd ~/ansible/collections/parasoltech/infrastructure
ansible-creator init role webserver --path roles/webserver
This creates the role directory structure inside the collection's roles/ directory, including defaults/main.yml, tasks/main.yml, handlers/main.yml, meta/main.yml, and template placeholders.
What ansible-creator Produces
After scaffolding, the collection looks like this:
parasoltech/infrastructure/
galaxy.yml
README.md
LICENSE
meta/
runtime.yml
plugins/
roles/
webserver/
defaults/
main.yml
handlers/
main.yml
meta/
main.yml
tasks/
main.yml
templates/
vars/
main.yml
README.md
tests/
docs/
All files come with sensible defaults that you customize for your use case. The galaxy.yml needs your namespace and description; the role's defaults/main.yml needs your variables; the tasks/main.yml needs your automation logic.
Configuring galaxy.yml
The galaxy.yml file is the manifest for your collection. Here is the one for parasoltech.infrastructure:
---
namespace: parasoltech
name: infrastructure
version: 1.0.0
readme: README.md
authors:
- Parasol Tech Platform Team <platform@parasol.example>
description: Infrastructure automation collection for Parasol Tech
license_file: LICENSE
tags:
- infrastructure
- linux
dependencies:
"ansible.posix": ">=1.0.0"
build_ignore:
- .gitignore
- .venv
- collections
- .tox
- .ade
Each field has a specific purpose:
| Field | Purpose |
|---|---|
namespace |
The organization or team name. Immutable after publishing. |
name |
The collection name. Together with namespace, forms the FQCN. |
version |
Semantic version (see Semantic Versioning below). |
readme |
Path to the README file. |
authors |
List of authors with optional email. |
description |
Short description for Galaxy/Hub search results. |
license_file |
Path to the license file. |
tags |
Discovery tags for Galaxy/Hub. Available tags include application, cloud, database, infrastructure, linux, monitoring, networking, security, tools, windows, and others. |
dependencies |
Other collections this one requires, with version constraints. |
build_ignore |
Files and directories to exclude when building the collection artifact. |
Dependencies
The dependencies field declares which other collections yours needs. Version constraints use pip-style syntax:
dependencies:
"ansible.posix": ">=1.0.0" # 1.0.0 or higher
"ansible.utils": "*" # any version
"community.general": ">=5.0,<7" # 5.x or 6.x, not 7.x
When someone installs your collection, ansible-galaxy automatically installs these dependencies too.
Build Ignore
The build_ignore field keeps development artifacts out of the published package. When ade manages your collection, it creates .venv, collections, and .ade directories inside the collection root. These are useful during development but should never be included in the distributed tarball:
Managing Dependencies with ade
The Ansible Development Environment tool (ade) manages your collection's development workspace. It handles:
- Creating isolated Python virtual environments
- Installing your collection in editable mode (changes take effect immediately)
- Resolving and installing collection dependencies declared in
galaxy.yml - Installing Python dependencies from
requirements.txtandtest-requirements.txt - Tracking system-level package requirements
Installing Your Collection for Development
Navigate to your collection root and run:
The -e . flag means editable install: ade creates a symlink from the virtual environment into your working directory. When you edit files in the collection, the changes are immediately visible to Ansible without reinstalling.
Typical output looks like this:
$ ade install -e .
Note: Created virtual environment: .venv
Note: Installed collections include: ansible.posix and parasoltech.infrastructure
Note: All python requirements are installed.
Note: All required system packages are installed.
Editable vs regular install
Without -e, ade install . copies the collection into the virtual environment. Changes to your source files are not reflected until you reinstall. Always use -e during development.
Viewing the Dependency Tree
To see what ade has installed and the full dependency graph:
This shows your collection, its dependencies, and their dependencies. It is useful for understanding what gets pulled in and for troubleshooting version conflicts.
Handling System Dependencies
Some collections require system-level packages (C libraries, Python bindings compiled from C, etc.). When ade detects missing system packages, it tells you what to install:
$ ade install -e .
Warning: Required system packages are missing. Please use the system
package manager to install them.
- python3-cffi
- python3-cryptography
Install the listed packages with your system package manager (dnf install, apt install, etc.), then re-run ade install -e ..
Immutable environments
In container-based environments like devcontainers or Red Hat Dev Spaces, you cannot install system packages at runtime with dnf install. If ade reports missing system packages, the recommended approach is to add them to the container image itself:
- Devcontainer: Add a
postCreateCommandor customDockerfilein.devcontainer/to install the packages during container build. - Dev Spaces: Add the packages to the
devfile.yamlcontainer component image. - Custom EE: Include them in your
execution-environment.ymlsystem dependencies.
The community-ansible-dev-tools base image already includes the most common system dependencies.
Argument Validation
Every role should validate its inputs. If a user passes webserver_port: "eighty" instead of an integer, the role should fail immediately with a clear message, not halfway through when a template renders Listen eighty and the web server refuses to start.
Ansible provides argument validation through meta/argument_specs.yml. This file declares the type, default value, and constraints for every role input.
Writing argument_specs.yml
Here is the argument specification for the webserver role:
---
argument_specs:
main:
short_description: Install and configure a web server
description:
- Install web server packages, deploy configuration from
a template, deploy a default index page, and manage the
service lifecycle.
options:
webserver_port:
type: int
default: 80
description: The HTTP port the web server listens on.
webserver_document_root:
type: str
default: /var/www/html
description: >-
The document root directory where web content is served from.
webserver_server_name:
type: str
default: localhost
description: >-
The server name used in the virtual host configuration.
webserver_service_enabled:
type: bool
default: true
description: >-
Whether to start and enable the web server service.
webserver_max_connections:
type: int
default: 256
description: >-
The maximum number of simultaneous client connections.
webserver_admin_email:
type: str
required: false
description: >-
The admin email shown in server error pages.
If not set, the server default is used.
webserver_packages:
type: list
elements: str
required: false
description: >-
List of packages to install. If not provided, the role
uses platform-specific defaults.
The main key matches the entrypoint, tasks/main.yml. If your role has multiple entrypoints (e.g., tasks/install.yml and tasks/configure.yml called separately), each gets its own entry under argument_specs.
What Validation Catches
When Ansible loads a role with argument specs, it checks:
- Type: Is
webserver_portactually an integer? Iswebserver_service_enableda boolean? - Required: Is
webserver_portprovided? (If no default exists andrequired: true) - Choices: Is the value one of an allowed set? (Use
choices: [a, b, c]) - Elements: For list types, what type should each element be?
If validation fails, Ansible stops before running any tasks and reports the error. This is fail-fast behavior: catching errors at the top instead of midway through the role.
The Connection to defaults/main.yml
Notice that the defaults in argument_specs.yml match defaults/main.yml. They should always agree. The argument_specs.yml is the formal contract; defaults/main.yml is where the values are actually set. If they diverge, the behavior becomes confusing.
Keep defaults and argument specs in sync
When you add a new variable to defaults/main.yml, add the matching entry to meta/argument_specs.yml. When you change a default, update both files.
Fully Qualified Collection Names (FQCNs)
A Fully Qualified Collection Name identifies any piece of content within a collection. The format is:
For modules:
# FQCN — always correct, never ambiguous
- name: Install packages
ansible.builtin.package:
name: httpd
state: present
# Short name — works only if ansible.builtin is in the search path
- name: Install packages
package:
name: httpd
state: present
For roles:
# Using a collection role with FQCN
- name: Deploy web servers
hosts: webservers
roles:
- role: parasoltech.infrastructure.webserver
Why FQCNs Matter
Short names like copy, template, or package work because Ansible searches a default set of collections (starting with ansible.builtin). But when you add community or custom collections, short names become ambiguous. If both ansible.builtin and community.general provide a module with the same name, which one runs?
FQCNs eliminate this ambiguity. ansible.builtin.copy always means the copy module from ansible.builtin. community.general.filesystem always means the filesystem module from community.general. There is never any doubt.
Throughout this course we have used FQCNs from the start: ansible.builtin.template, ansible.builtin.service, ansible.builtin.debug. This is intentional. It is a habit worth building early, even when short names would work.
Semantic Versioning
Collections use semantic versioning (SemVer) to communicate the impact of changes. The version number has three parts:
| Part | When to increment | Example |
|---|---|---|
| MAJOR | Breaking changes (removed variables, changed behavior) | 1.0.0 -> 2.0.0 |
| MINOR | New features (new roles, new variables, new modules) | 1.0.0 -> 1.1.0 |
| PATCH | Bug fixes (no new features, no breaking changes) | 1.0.0 -> 1.0.1 |
For the parasoltech.infrastructure collection:
- Adding a new
databaserole? Bump MINOR:1.0.0->1.1.0 - Fixing a template bug in the
webserverrole? Bump PATCH:1.0.0->1.0.1 - Renaming
webserver_porttowebserver_listen_port? That is a breaking change. Bump MAJOR:1.0.0->2.0.0
Semantic versioning lets consumers specify dependency constraints with confidence. If your collection is at 1.3.2, a consumer declaring "parasoltech.infrastructure": ">=1.0.0,<2.0.0" knows they will get bug fixes and new features but never breaking changes.
Ansible Galaxy and Automation Hub
Ansible Galaxy (galaxy.ansible.com) is the public community registry for Ansible collections. Anyone can browse, download, and publish collections.
Automation Hub is the enterprise equivalent: a curated, supported registry included with the Ansible Automation Platform. Organizations use private Automation Hub instances to distribute internal collections (like parasoltech.infrastructure).
Automation Hub: two versions
Red Hat offers two versions of Automation Hub:
- Ansible Automation Hub (console.redhat.com): A hosted, hybrid-cloud service providing Red Hat certified and validated collections. Available to all AAP subscribers.
- Private Automation Hub: A self-hosted instance you run inside your organization to distribute internal collections, curate approved content, and host container images for Execution Environments.
Most organizations use both: the hosted hub for upstream certified content and a private instance for internal automation.
Installing Collections from Galaxy
# Install a specific collection
ansible-galaxy collection install community.general
# Install a specific version
ansible-galaxy collection install community.general:9.0.0
# Install from a requirements file
ansible-galaxy collection install -r requirements.yml
A requirements.yml file lists multiple collections with version constraints:
---
collections:
- name: ansible.posix
version: ">=1.0.0"
- name: community.general
version: ">=9.0.0"
Building a Collection for Distribution
To build your collection into an installable tarball:
This produces a file like parasoltech-infrastructure-1.0.0.tar.gz that can be installed with ansible-galaxy collection install or uploaded to Galaxy or Automation Hub.
Publishing to Galaxy
# Publish to Galaxy (requires an API key from galaxy.ansible.com)
ansible-galaxy collection publish parasoltech-infrastructure-1.0.0.tar.gz
For internal distribution at Parasol Tech, the CoP publishes to a private Automation Hub instead. The workflow is similar: build the tarball, then push it to the Hub.
Building the webserver Role
Now let us build the parasoltech.infrastructure.webserver role step by step. This role installs a web server, deploys a configuration file from a template, creates a default index page, and manages the service lifecycle.
defaults/main.yml
The user-facing variables define what consumers of the role can customize:
---
# The HTTP port the web server listens on
webserver_port: 80
# The document root where web content is served from
webserver_document_root: /var/www/html
# The server name used in the virtual host configuration
webserver_server_name: localhost
# Whether to start and enable the web server service
webserver_service_enabled: true
# The maximum number of simultaneous client connections
webserver_max_connections: 256
# The admin email shown in server error pages
# webserver_admin_email:
# Packages to install (overridable per platform via vars/)
# webserver_packages:
Every variable has the webserver_ prefix. The two commented-out variables (webserver_admin_email, webserver_packages) have no safe default, so they are listed but not set. Users know these options exist by reading this file.
vars/main.yml
Internal constants that should not be overridden:
---
__webserver_packages_default:
- httpd
__webserver_service_name: httpd
__webserver_config_dir: /etc/httpd/conf
__webserver_config_file: httpd.conf
The double underscore prefix marks these as internal. The service name, config directory, and default packages are implementation details that users should not need to change.
tasks/main.yml
The task list ties everything together. Notice how it uses patterns from every previous module: package management (Module 2), templates with backup (Module 5), handlers (Module 5), and variable-driven logic (Module 4):
---
- name: Install web server packages
ansible.builtin.package:
name: "{{ webserver_packages | default(__webserver_packages_default) }}"
state: present
- name: Ensure document root exists
ansible.builtin.file:
path: "{{ webserver_document_root }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Deploy web server configuration
ansible.builtin.template:
src: "{{ role_path }}/templates/webserver.conf.j2"
dest: "{{ __webserver_config_dir }}/{{ __webserver_config_file }}"
owner: root
group: root
mode: "0644"
backup: true
notify:
- webserver_validate_config
- webserver_reload
- name: Deploy default index page
ansible.builtin.template:
src: "{{ role_path }}/templates/index.html.j2"
dest: "{{ webserver_document_root }}/index.html"
owner: root
group: root
mode: "0644"
backup: true
- name: Ensure web server service is in the desired state
ansible.builtin.service:
name: "{{ __webserver_service_name }}"
state: "{{ webserver_service_enabled | ternary('started', 'stopped') }}"
enabled: "{{ webserver_service_enabled }}"
Key patterns to notice:
{{ role_path }}/templates/for explicit template pathsbackup: trueon every template/copy task- FQCNs throughout (
ansible.builtin.package, notpackage) - Handler notification uses the
listentopics (webserver_validate_config,webserver_reload) | default(__webserver_packages_default)lets users override packages while providing a built-in fallback
handlers/main.yml
---
- name: Validate webserver configuration
ansible.builtin.command:
cmd: "httpd -t"
changed_when: false
listen: "webserver_validate_config"
- name: Reload webserver
ansible.builtin.service:
name: "{{ __webserver_service_name }}"
state: reloaded
listen: "webserver_reload"
- name: Restart webserver
ansible.builtin.service:
name: "{{ __webserver_service_name }}"
state: restarted
listen: "webserver_restart"
Notice changed_when: false on the validation command. It is a read-only check, so it should never report a change.
Templates
The web server configuration template (webserver.conf.j2) uses patterns from Module 5:
{{ ansible_managed | comment }}
#
# Web server configuration for {{ webserver_server_name }}
Listen {{ webserver_port }}
ServerName {{ webserver_server_name }}
DocumentRoot "{{ webserver_document_root }}"
MaxRequestWorkers {{ webserver_max_connections }}
{% if webserver_admin_email is defined %}
ServerAdmin {{ webserver_admin_email }}
{% endif %}
<Directory "{{ webserver_document_root }}">
AllowOverride None
Require all granted
</Directory>
ErrorLog "logs/error_log"
CustomLog "logs/access_log" combined
And a simple index.html.j2:
{{ ansible_managed | comment('<!--', '-->') }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ webserver_server_name }}</title>
</head>
<body>
<h1>Welcome to {{ webserver_server_name }}</h1>
<p>This page is managed by Ansible.</p>
<p>Served by the <code>parasoltech.infrastructure.webserver</code> role.</p>
</body>
</html>
Notice how {{ ansible_managed | comment('<!--', '-->') }} uses custom comment delimiters for HTML. The | comment filter accepts arguments to change the comment syntax from the default #.
Using the Role in a Playbook
A playbook that uses this role is short because the complexity is inside the role:
---
- name: Deploy web servers
hosts: webservers
roles:
- role: parasoltech.infrastructure.webserver
vars:
webserver_port: 8080
webserver_server_name: web.parasol.example
webserver_document_root: /var/www/parasol
webserver_admin_email: admin@parasol.example
The playbook is a list of roles, not a list of tasks. All the logic -- installing packages, deploying templates, managing services -- lives inside the role. The playbook just says what to apply and where.
Exercises
Exercise 1: Explore the Collection Structure
Navigate to the companion collection and examine the structure:
Open the key files and verify:
galaxy.ymlhas the correct namespace, name, and versionroles/webserver/defaults/main.ymlhas all variables prefixed withwebserver_roles/webserver/vars/main.ymlhas internal variables prefixed with__webserver_roles/webserver/meta/argument_specs.ymlmatches the defaults
Exercise 2: Scaffold a New Collection with ansible-creator
Create a second collection using ansible-creator:
ansible-creator init collection parasoltech.monitoring \
~/ansible/collections/parasoltech/monitoring
Explore the generated files. Compare the structure to the parasoltech.infrastructure collection. Notice how ansible-creator generates the same layout every time. Consistent scaffolding means consistent collections.
Exercise 3: Use ade for Dependency Management
Install the parasoltech.infrastructure collection in editable mode:
Check the dependency tree:
You should see ansible.posix listed as a dependency (declared in galaxy.yml).
Exercise 4: Add Argument Validation
Add a new variable to the webserver role:
- Add
webserver_log_leveltodefaults/main.ymlwith a default ofwarn - Add the matching entry to
meta/argument_specs.ymlwithtype: strandchoices: [debug, info, notice, warn, error, crit] - Use the new variable in the
webserver.conf.j2template
Test that validation works by passing an invalid value:
Ansible should reject the value before running any tasks.
Exercise 5: Build the Collection
Build the collection into a distributable tarball:
Examine the resulting .tar.gz file. Notice that the directories listed in build_ignore (.venv, collections, .tox, .ade) are not included in the archive.
Summary
In this module you:
- Learned the role directory structure and how Ansible assembles tasks, defaults, vars, handlers, templates, and metadata into a reusable unit
- Understood the critical difference between
defaults/main.yml(user-facing, low precedence) andvars/main.yml(internal, high precedence) - Applied naming conventions: prefix all role variables with the role name, prefix internal variables with double underscores, never use dashes in role names
- Created argument validation with
meta/argument_specs.ymlto fail fast on bad input - Scaffolded a collection and role with
ansible-creatorand managed the development environment withade - Configured
galaxy.ymlwith metadata, dependencies, version, and build ignore rules - Used Fully Qualified Collection Names to reference content unambiguously
- Understood semantic versioning and how it communicates the impact of changes
The CoP at Parasol Tech now has a standard: all reusable automation goes into the parasoltech.infrastructure collection with properly named, validated, documented roles. The database team installs the collection and uses the webserver role without copying a single file. When the platform team fixes a bug, they bump the patch version and every consumer gets the fix on their next install.