Ansible roles vs collections explained in one line: a role is a reusable bundle of tasks, handlers, templates and defaults for one job, and a collection is the versioned distribution package that can ship roles, modules, plugins and playbooks. They are not rivals. Write roles first; wrap them in a collection only when you need custom modules or versioned sharing.
For a homelab with an N100 mini-PC and a NAS, that split is most of the story. You will consume collections constantly, because modules like ansible.posix.mount and community.docker.docker_compose_v2 live in them. You will write roles for your own boxes. You may never need to publish a collection, and that is fine.
What is an Ansible role?
An Ansible role is a directory with a fixed layout that Ansible loads automatically: tasks, handlers, templates, files, variables and metadata for one unit of configuration, such as “Docker host” or “NFS client.” The official docs describe roles as a way to “automatically load related vars, files, tasks, handlers, and other Ansible artifacts based on a known file structure” (Ansible roles guide).
The layout looks like this:
roles/nas_client/
├── defaults/main.yml # low-precedence knobs callers override
├── vars/main.yml # high-precedence values, rarely what you want
├── tasks/main.yml # the actual work
├── handlers/main.yml # restarts and reloads
├── templates/ # Jinja2 files, usually *.j2
├── files/ # static files for copy or script
└── meta/
├── main.yml # dependencies, allow_duplicates
└── argument_specs.yml # input validation
A role needs at least one of those directories, not all of them. ansible-galaxy role init scaffolds the full set if you enjoy empty folders.
Three details cause most of the confusion in practice:
- defaults vs vars. The docs put
defaults/at “very low precedence” andvars/at high precedence. Tunables belong in defaults, or the caller’s override silently loses. The precedence rules for extra vars stack on top of this. - Deduplication. Ansible “only executes each role once in a play, even if you define it multiple times unless the parameters defined on the role are different.” Set
allow_duplicates: trueinmeta/main.ymlif you really mean it. - Argument specs. A
meta/argument_specs.ymlfile makes Ansible insert a validation task at the start of the role, which catches the misspelled variable before it reaches the NAS.
You call a role three ways: the play-level roles: keyword, import_role (static, like roles: but usable inside tasks:), or include_role (dynamic, evaluated when the task runs). Ansible finds an unqualified role name by searching collections, then a roles/ directory next to the playbook, then the configured roles_path, then the playbook’s own directory.
What is an Ansible collection?
An Ansible collection is a versioned package with a namespace and a name, such as community.docker, that can hold modules, plugins, roles and playbooks together. The docs define collections as “a distribution format for Ansible content that can include playbooks, roles, modules, and plugins” (Using Ansible collections). The unit is the package, not the job.
homelab/
├── galaxy.yml # namespace, name, version, readme, authors
├── meta/runtime.yml # requires_ansible, plugin routing
├── plugins/modules/ # custom modules, shared by every role
├── roles/
│ ├── docker_host/
│ └── nas_client/
└── playbooks/
galaxy.yml has five required keys (namespace, name, version, readme and authors), and the version “must be compatible with semantic versioning” (galaxy.yml reference). A dependencies key pins other collections by version range. meta/runtime.yml can declare supported ansible-core versions through requires_ansible (collection structure).
The part people miss: you already use collections. ansible.builtin.copy is a fully qualified collection name (FQCN): namespace ansible, collection builtin, module copy. The ansible-core package carries only built-in modules and plugins, while ansible “adds a community-curated selection of Ansible Collections” (installation guide). The ansible.posix.mount docs say that collection is not included in ansible-core, which explains the module-resolution error a lean control node throws when nobody installed it.
How roles and collections compare
This comparison draws on the official Ansible documentation only: the roles guide, the collections user and developer guides, and the Galaxy user guide. There is no benchmark here, because the differences are structural, not performance.
| Role | Collection | |
|---|---|---|
| What it is | Reusable unit of configuration | Distribution package for Ansible content |
| Contains | Tasks, handlers, vars, defaults, templates, files, meta | Roles, modules, plugins, playbooks, docs, tests |
| Custom modules and plugins | Standalone roles only (library/, module_utils/, plugin dirs) | Yes, under plugins/; roles inside cannot embed them |
| Identity | Role name, e.g. nas_client | namespace.collection, e.g. community.docker |
| Versioning | Version string or Git ref at install time | Semantic version required in galaxy.yml |
| Declares dependencies | Other roles in meta/main.yml | Other collections in galaxy.yml, with ranges |
| Default install path | First writable roles_path entry, ~/.ansible/roles | ~/.ansible/collections/ansible_collections |
| Scaffold command | ansible-galaxy role init | ansible-galaxy collection init |
| Referenced as | nas_client | my_namespace.homelab.nas_client |
Where each one wins
Roles win for your own hosts. A role in a roles/ folder next to the playbook needs no build step, no namespace and no galaxy.yml. Ansible finds it automatically. For a mini-PC running Docker, an NFS mount from the NAS and a backup job, three roles in one Git repo is the right amount of structure.
Collections win when you extend or ship. If you wrote a custom module or filter plugin, or several roles that must be versioned together, a collection is the supported container. The structure docs are blunt that “roles in a collection cannot contain plugins any more”; plugins live in the collection’s plugins/ tree, where every role in the collection can reach them. You also get semver, dependency ranges, and playbooks you can run by FQCN, as in ansible-playbook my_namespace.my_collection.playbook1 -i ./myinventory.
Collections cost something, though: a namespace, a build step with ansible-galaxy collection build, and the discipline of bumping versions. For a team, that buys real reproducibility. For one person’s homelab repo it is mostly ceremony; it is very easy to spend a weekend on a changelog and a CI matrix for a single mini-PC, and the mini-PC does not care.
How do roles and collections work together?
They nest. A collection’s roles/ directory holds ordinary roles, and you call them by FQCN, such as my_namespace.homelab.nas_client. According to the collections usage guide, “roles defined inside a collection always implicitly search their own collection first,” so they reach sibling modules and roles without full names. Two gotchas follow.
First, role names inside a collection are stricter: “only lowercase alphanumeric characters, plus _ and start with an alpha character.” A Galaxy-era role named nas-client has to be renamed.
Second, the playbook-level collections: keyword does not flow into roles. The docs state that “the roles do not inherit the collections set by the playbook.” A role that relies on short module names works in one playbook and breaks in the next. Writing FQCNs everywhere, including ansible.builtin.template, removes the whole class of bug.
A homelab layout that does not fight you
The layout that survives a rebuild keeps your roles in the repo and pins other people’s collections in requirements.yml:
homelab-ansible/
├── ansible.cfg
├── inventory/hosts.yml
├── requirements.yml
├── site.yml
└── roles/
├── docker_host/
└── nas_client/
# requirements.yml
---
collections:
- name: ansible.posix
version: ">=2.0.0,<3.0.0"
- name: community.docker
version: ">=3.6.0" # docker_compose_v2 arrived in 3.6.0
# roles/nas_client/tasks/main.yml
---
- name: Mount the NAS media share over NFS
ansible.posix.mount:
src: "{{ nas_client_export }}"
path: "{{ nas_client_mountpoint }}"
fstype: nfs
opts: "{{ nas_client_opts }}"
state: mounted
# site.yml
---
- name: Configure the mini-PC
hosts: docker_hosts
become: true
roles:
- nas_client
- docker_host
Run ansible-galaxy collection install -r requirements.yml on every fresh control node. The installing guide notes that plain ansible-galaxy install -r handles roles and collections together, but not with a custom install path. Add an upper bound to community.docker once you have run against the next major release; an unpinned range is how month-nine surprises happen.
Put the docker_hosts group and its variables in your inventory groups and group_vars, and if the share is SMB with credentials, keep the password in Ansible Vault rather than in role defaults. Then preview the first run before trusting a new role with the NAS.
Which one to pick
Pick roles for your own configuration and collections for everything you install from someone else. Start every project as a playbook plus a roles/ directory, pin third-party collections in requirements.yml, and use FQCNs in every task. Name roles with lowercase letters and underscores from day one so they move into a collection without renaming.
Promote them into a collection when one of three things happens: you write a custom module or plugin, a second repository needs the same roles, or someone else needs versioned releases. On a team, or publishing to Galaxy, start there, because semver and declared dependencies are exactly what a shared consumer needs. Until then, a collection is packaging for an audience of one.
FAQ
are ansible roles deprecated
No, Ansible roles are not deprecated. The current documentation still covers standalone roles, roles_path, and ansible-galaxy role install, and collections themselves contain roles. What changed is distribution: modules and plugins ship in collections, so a modern role calls collection content by fully qualified name instead of carrying its own copies.
what is fqcn in ansible
FQCN stands for fully qualified collection name: the namespace, collection and content name joined by dots, such as ansible.builtin.copy or community.docker.docker_compose_v2. It tells Ansible exactly which collection provides a module, plugin or role. Using FQCNs inside roles matters because roles do not inherit the playbook’s collections: search list.
should i install ansible or ansible-core
Install ansible-core plus a pinned requirements.yml if you want a lean, reproducible control node; install ansible if you want a large community-curated set of collections preinstalled. The core package carries only built-in modules and plugins, so anything outside ansible.builtin, such as ansible.posix.mount, needs a separate collection install.
how do i move a role into a collection
Copy the role into the collection’s roles/ directory, rename it to lowercase letters, digits and underscores starting with a letter, and move any embedded modules or plugins into the collection’s plugins/ tree. Then update callers to the FQCN, for example my_namespace.homelab.nas_client, and bump the version in galaxy.yml.
Related across the network
- How to Create a Shared Folder in OpenMediaVault 8 — openmediavaultlab.com
- Portainer Docker Homelab Setup: Install, Stacks and Agents — dockerhomelab.com