Roles and Collections
As playbooks grow, copying the same tasks, handlers, and templates between plays becomes unmanageable. Ansible solves this with two layers of reuse: roles, which package related automation into a self-contained, shareable unit, and collections, which bundle roles, modules, and plugins for distribution through Ansible Galaxy or a private Automation Hub.
This page assumes familiarity with playbooks.md and variables-and-facts.md. If you are new to Ansible, start at index.md and installation.md.
Why Roles
A role groups everything a single unit of automation needs — tasks, handlers, templates, static files, default and overriding variables, and metadata — into a predictable directory layout. Instead of pasting the same twenty tasks into every play that configures a web server, you write them once as a webserver role and call it by name.
Roles give you:
- DRY playbooks. Plays shrink to a list of roles and the variables that customize them.
- Encapsulation. A role's internals (which template it renders, which handler restarts a service) stay hidden behind its name.
- Shareability. A well-structured role can be published to Galaxy or vendored into other projects unchanged.
- Testability. Roles have a conventional
tests/directory and pair naturally with Molecule.
Tip
Aim for one responsibility per role. A role named nginx should install and configure Nginx — not also provision the database. Compose behavior by listing several small roles in a play rather than building one monolithic role.
Role Directory Structure
Every role follows the same standard tree. Ansible auto-loads the main.yml file in each directory, so you rarely reference these paths explicitly.
roles/
webserver/
tasks/main.yml # Primary list of tasks the role runs
handlers/main.yml # Handlers, notified by tasks (e.g. restart nginx)
templates/ # Jinja2 templates rendered with ansible.builtin.template
files/ # Static files copied with ansible.builtin.copy
vars/main.yml # Role variables (high precedence)
defaults/main.yml # Default variables (low precedence, meant to be overridden)
meta/main.yml # Role metadata: dependencies and galaxy_info
library/ # Custom modules local to this role
module_utils/ # Shared Python helpers for the role's modules
lookup_plugins/ # Custom lookup plugins (also filter_plugins/, etc.)
tests/ # Test inventory and a sample playbook
| Directory | Auto-loaded file | Purpose |
|---|---|---|
tasks/ |
main.yml |
The role's entry point — the tasks executed when the role runs. |
handlers/ |
main.yml |
Handlers triggered by notify; run once at the end of the play. |
templates/ |
(none) | Jinja2 templates; ansible.builtin.template searches here first. |
files/ |
(none) | Static assets; ansible.builtin.copy searches here first. |
vars/ |
main.yml |
Variables tied to the role's logic; high precedence. |
defaults/ |
main.yml |
Sensible defaults callers are expected to override; lowest precedence. |
meta/ |
main.yml |
Declares dependencies and galaxy_info (author, license, platforms). |
library/ |
(none) | Custom modules usable only by this role. |
module_utils/ |
(none) | Reusable Python code imported by the role's modules. |
lookup_plugins/ |
(none) | Role-scoped plugins (lookup, filter, test, callback, etc.). |
tests/ |
test.yml |
A minimal inventory and playbook for exercising the role. |
Any directory you do not need can be omitted. A trivial role might contain only tasks/main.yml.
Note
Within tasks/main.yml you can split logic across additional files (for example tasks/install.yml) and pull them in with ansible.builtin.import_tasks or ansible.builtin.include_tasks. Only main.yml is auto-loaded; everything else is included explicitly.
Creating a Role
Scaffold the standard tree with ansible-galaxy:
# Create a role skeleton in the current directory
ansible-galaxy role init webserver
# Older shorthand (still valid)
ansible-galaxy init webserver
Roles are discovered from a roles_path. By default Ansible looks in a roles/ directory next to the playbook, then in ~/.ansible/roles, /usr/share/ansible/roles, and /etc/ansible/roles. Set a custom path in ansible.cfg:
[defaults]
roles_path = ./roles:./external_roles
For most projects, keeping roles in a roles/ directory beside your playbooks is the simplest layout and needs no configuration.
Using Roles in a Play
There are three ways to invoke a role. The classic roles: keyword adds roles to a play, and they run before any tasks in that play's tasks: section:
- name: Configure web tier
hosts: web
become: true
roles:
- common
- role: webserver
vars:
http_port: 8080
Inside the tasks: section you can instead call a role with a module, which gives you finer control:
- name: Configure web tier
hosts: web
become: true
tasks:
- name: Apply baseline hardening (static)
ansible.builtin.import_role:
name: common
- name: Deploy each site (dynamic)
ansible.builtin.include_role:
name: webserver
vars:
http_port: "{{ item.port }}"
loop: "{{ sites }}"
The difference between the two matters:
ansible.builtin.import_roleis static. The role's tasks are parsed and inserted when the playbook is first read. Tags applied to the import propagate to every task in the role, but you cannot use it inside aloop.ansible.builtin.include_roleis dynamic. The role is resolved at runtime, so it works withloop,when, and other runtime constructs. Because it is evaluated late, a tag on the include does not automatically reach the role's inner tasks.
Important
Use import_role when you want the role's tasks visible to --list-tasks and controllable by tags. Use include_role when you need to run a role conditionally or repeatedly in a loop. Mixing them freely is fine — choose per call site.
Variables passed with vars: (as shown above) are scoped to that role invocation, which is the cleanest way to parameterize a role.
Role Variables and Precedence
Roles expose two variable files with very different precedence:
defaults/main.ymlholds the lowest-precedence variables in Ansible. They exist to be overridden by inventory, play vars,-eextra vars, or avars:block on the role call. Put every knob a user might tune here.vars/main.ymlholds high-precedence variables tied to the role's internal logic — values you do not expect callers to change, such as a package name that differs per OS family.
# defaults/main.yml — caller-overridable
http_port: 80
worker_processes: auto
# vars/main.yml — internal, high precedence
nginx_package: nginx
nginx_service: nginx
Because vars/main.yml outranks most other sources, avoid placing tunable settings there — a caller's inventory value will be silently ignored. For the complete precedence order, see variables-and-facts.md.
Role Dependencies
A role can require other roles to run first. Declare them in meta/main.yml:
galaxy_info:
author: Joseph Streeter
description: Installs and configures Nginx
license: MIT
min_ansible_version: "2.16"
platforms:
- name: Ubuntu
versions:
- jammy
- noble
dependencies:
- role: common
- role: firewall
vars:
firewall_allowed_ports:
- 80
- 443
Listed dependencies are executed before the role that declares them, in order. Keep dependency chains shallow; deep chains make execution order hard to reason about. galaxy_info is also what Ansible Galaxy reads when the role is published.
Collections
A collection is a distributable bundle that can contain roles, modules, plugins, and even entire playbooks, packaged under a namespace. Content inside a collection is addressed by its Fully Qualified Collection Name (FQCN) in the form namespace.collection.content:
ansible.builtin.copy # copy module in the builtin collection
community.general.timezone # timezone module in community.general
ansible.posix.firewalld # firewalld module in ansible.posix
amazon.aws.ec2_instance # ec2_instance module in amazon.aws
ansible.builtin ships inside ansible-core itself — modules like copy, template, service, and apt are always available. Everything else lives in a separate collection you install on demand:
| Collection | Contents |
|---|---|
ansible.builtin |
Core modules bundled with ansible-core; no install needed. |
community.general |
Broad grab-bag of community modules and plugins. |
ansible.posix |
POSIX-oriented modules (firewalld, mount, sysctl, authorized_key). |
amazon.aws |
Official AWS modules maintained by the cloud provider. |
Note
The ansible package (as opposed to the minimal ansible-core) is itself a curated bundle of many popular collections. Installing ansible-core alone gives you only ansible.builtin, so you add the collections you need explicitly. See installation.md.
Installing and Managing Collections and Roles
Install a single collection or role directly:
# Install a collection from Ansible Galaxy
ansible-galaxy collection install community.general
# Install a standalone role
ansible-galaxy role install geerlingguy.docker
By default collections install to ~/.ansible/collections. For reproducible, project-local installs, point Ansible at a directory inside the repo:
export ANSIBLE_COLLECTIONS_PATH=./collections
ansible-galaxy collection install community.general -p ./collections
The durable, team-friendly approach is a collections/requirements.yml that pins both collections and roles to specific versions. Commit this file so every checkout resolves identical content:
# collections/requirements.yml
---
collections:
- name: community.general
version: "9.5.0"
- name: ansible.posix
version: "1.5.4"
- name: amazon.aws
version: ">=8.0.0,<9.0.0"
roles:
- name: geerlingguy.docker
version: "7.4.1"
- src: https://github.com/example/ansible-role-myapp.git
scm: git
version: "v2.1.0"
name: myapp
Install everything the file declares:
# Installs BOTH the collections and roles sections
ansible-galaxy install -r collections/requirements.yml
# Installs only the collections section
ansible-galaxy collection install -r collections/requirements.yml
Important
Always pin versions and commit requirements.yml. Unpinned dependencies mean a community.general release can silently change your automation's behavior between runs. Pinning makes builds reproducible across developers and CI.
Using Collection Content
The recommended way to reference collection content is by its full FQCN at each usage site. It is unambiguous and survives refactoring:
- name: Ensure the timezone is set
community.general.timezone:
name: America/Chicago
- name: Open the HTTPS service in the firewall
ansible.posix.firewalld:
service: https
permanent: true
state: enabled
immediate: true
Ansible also supports a play-level collections: keyword that lets you write short module names by declaring a search list. Treat this as legacy shorthand — it obscures where a module comes from and can collide when two collections define the same name:
# Legacy shorthand — prefer explicit FQCN instead
- name: Configure host
hosts: all
collections:
- community.general
tasks:
- name: Set timezone using short name
timezone:
name: America/Chicago
Tip
Standardize on FQCN everywhere. It is required by ansible-lint's default rules, makes examples copy-pasteable without hidden context, and prevents ambiguous module resolution. See best-practices.md.
Publishing and Sharing
You can share your own content the same way you consume others'. Build a collection tarball from a directory containing a galaxy.yml manifest:
# From the collection's root (contains galaxy.yml)
ansible-galaxy collection build
# Publish the built artifact
ansible-galaxy collection publish ./my_namespace-my_collection-1.0.0.tar.gz
Public content is distributed through Ansible Galaxy, while organizations typically host vetted, internal content on a private Automation Hub and point ansible.cfg at it via a [galaxy] server list. For most teams, consuming pinned upstream collections and keeping your own roles in roles/ covers day-to-day needs without publishing anything.
Keep secrets out of shared roles and collections — parameterize sensitive values and supply them at runtime, as described in vault-and-secrets.md.