Ansible Guide
Isometric navy platform with pink cubes on stems linked by thin red lines to a larger central cube, evoking Ansible roles bundled into a collection
automation

Ansible Roles vs Collections Explained: Which to Use

A role packages tasks for one job; a collection ships roles, modules and plugins as one versioned unit. How they differ and which to write first.

By Ansible Guide Editorial · · 7 min read

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” and vars/ 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: true in meta/main.yml if you really mean it.
  • Argument specs. A meta/argument_specs.yml file 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.

RoleCollection
What it isReusable unit of configurationDistribution package for Ansible content
ContainsTasks, handlers, vars, defaults, templates, files, metaRoles, modules, plugins, playbooks, docs, tests
Custom modules and pluginsStandalone roles only (library/, module_utils/, plugin dirs)Yes, under plugins/; roles inside cannot embed them
IdentityRole name, e.g. nas_clientnamespace.collection, e.g. community.docker
VersioningVersion string or Git ref at install timeSemantic version required in galaxy.yml
Declares dependenciesOther roles in meta/main.ymlOther collections in galaxy.yml, with ranges
Default install pathFirst writable roles_path entry, ~/.ansible/roles~/.ansible/collections/ansible_collections
Scaffold commandansible-galaxy role initansible-galaxy collection init
Referenced asnas_clientmy_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.

Sources

  1. Roles - Ansible Community Documentation
  2. Using Ansible collections
  3. Using collections in a playbook
  4. Installing collections
  5. Collection structure
  6. Collection Galaxy metadata structure
  7. Galaxy User Guide
  8. Installing Ansible
  9. ansible.posix.mount module
#ansible #ansible-roles#ansible-collections#ansible-galaxy#homelab

Related