Standard Ansible Role Git Repository Layout

Bottom line

The standard Ansible role repository layout is defined by a seven-directory convention (tasks, handlers, templates, files, vars, defaults, meta) auto-generated by ansible-galaxy role init, which also produces README.md and a tests/ directory. The repository root must be named after the role for Galaxy publishing. A role is a self-contained directory tree.

In production projects, the role lives inside a roles/ directory of a larger playbook project with inventory files, group_vars/, host_vars/, tier-specific playbooks, and a site.yml master playbook. The landscape is shifting: standalone role repositories are being superseded by collections, where roles live under roles/ within the collection structure alongside plugins, modules, and a galaxy.yml manifest. Confidence in these conventions is high - they come directly from official Ansible documentation and are corroborated across multiple independent technical sources.

Key findings

  • Finding: ansible-galaxy role init <name> is the canonical way to scaffold a role. It creates 8 subdirectories and 8 files by default, though the official docs count only seven "main standard" directories (excluding tests/). Every role repository root should match the role name for Galaxy compatibility. (Source: official Ansible docs, Galaxy Developer Guide)

  • Finding: The seven standard directories each have a specific, non-negotiable purpose. tasks/ for execution logic, handlers/ for event-driven actions, defaults/ for overridable low-precedence variables, vars/ for high-precedence internal constants, templates/ for Jinja2 files, files/ for static assets, and meta/ for Galaxy metadata and role dependencies. A role needs only one of these to be valid. (Source: official Ansible Roles docs; Adfinis Ansible Guide)

  • Finding: Role repositories are not the whole project. The full recommended project layout places roles inside a roles/ directory alongside inventory/, group_vars/, host_vars/, per-tier playbooks (webservers.yml, dbservers.yml), and a site.yml master playbook. This is documented in the official Ansible best practices and reinforced across tutorials. (Source: official Ansible Best Practices; ComputingForGeeks 2026 tutorial)

  • Finding: Collections are replacing standalone role repositories as the preferred distribution format. In a collection, roles live under roles/role_name/ and follow the same directory structure, but with two key restrictions. Roles can't contain embedded plugins (they must go in the collection's plugins/ directory), and role names are restricted to lowercase alphanumeric characters plus underscores. The Adfinis guide explicitly recommends collections over single-role repos. (Source: Ansible Collection Structure docs; Adfinis Ansible Guide)

  • Finding: The practical pain points with the role layout include: scattered logic across many directories making it harder to trace execution flow; cross-platform boilerplate when writing OS-aware roles; and community debate about whether roles should be "large and server-role-focused" or "small and function-generic." (Source: Roelof Jan Elsinga blog; Ansible Forum discussion; Ansible GitHub issue #52759)

Background

Ansible roles were introduced as a structured alternative to monolithic playbooks. The directory convention was designed so that Ansible can auto-discover tasks, handlers, variables, templates. Files without explicit path configuration - it simply looks for main.yml files in each standard subdirectory.

The ansible-galaxy CLI tool, bundled with Ansible, provides role init to scaffold this structure. Roles are shared via Ansible Galaxy, a public registry that imposes naming and metadata conventions. Since Ansible 2.9/2.10, the collection format was introduced as a higher-level packaging mechanism that can bundle multiple roles, modules. Plugins together, and this has become the recommended distribution method.

Current state

As of 2026 (latest stable: ansible-core 2.19/Ansible 12):

  • The ansible-galaxy role init command still produces the same skeleton: defaults/, files/, handlers/, meta/, tasks/, templates/, tests/, vars/, plus README.md.
  • Official docs reference seven main standard directories (excluding tests/ and README.md), though tests/ is universally generated and used.
  • Standalone roles still work and are still published to Galaxy, but collections are the forward-looking standard. The Adfinis guide notes: "We generally recommend collections over single-role repositories. While there is no official 'don't use single-role repos' announcement yet, and they still work, collections are the way forward."
  • Role argument validation via meta/argument_specs.yml (since Ansible 2.11) adds a formal interface layer.
  • DEFAULT_PRIVATE_ROLE_VARS (since ansible-core 2.15) changed how vars: in the roles: section of playbooks scope, making role variables no longer leak into play scope by default.
  • Ansible-core 2.19 introduced significant templating changes that may require updating existing roles.

Technical details: directory-by-directory reference

Directory Required? Purpose Ansible behavior
tasks/ No (but practically always) Core execution logic. main.yml is the entry point. Auto-loaded at role invocation. Supports include_tasks/import_tasks for splitting.
handlers/ No Event-driven tasks triggered by notify. Run once at end of play. main.yml auto-loaded. Deduplicates: if 5 tasks notify the same handler, it runs once.
defaults/ No Overridable variables with lowest precedence. The role's public API. main.yml auto-loaded. Intended for values consumers should override.
vars/ No High-precedence variables. Internal constants. main.yml auto-loaded. Can use OS-specific files loaded via include_vars or first_found.
templates/ No Jinja2 template files (.j2 extension convention). template module auto-searches here. Recommended: mirror target filesystem path inside.
files/ No Static files deployed via copy/script modules. copy and script modules auto-search here. Recommended: mirror target filesystem path.
meta/ No (recommended) Galaxy metadata (author, license, platforms, tags) and dependencies. main.yml auto-loaded. Required for Galaxy publishing of standalone roles.
tests/ No Test playbook (test.yml) and inventory. Generated by ansible-galaxy init. Not counted among "seven main standard" directories.
README.md No (recommended) Role documentation. Generated by ansible-galaxy init. Galaxy renders it on the role page.
library/ No Custom Ansible modules (Python). Auto-added to module search path for this role and subsequent roles. Standalone roles only.
module_utils/ No Shared Python code for custom modules. Importable by modules in library/. Standalone roles only.
filter_plugins/ No Custom Jinja2 filters. Available to this role and subsequent roles. Standalone roles only.
lookup_plugins/ No Custom lookup plugins. Available to this role and subsequent roles. Standalone roles only.
meta/argument_specs.yml No Role argument validation specification (Ansible 2.11+). Validates role parameters before execution. Recommended over inline specs in meta/main.yml.

The full project layout (beyond the role)

A production Ansible project wraps roles in a larger structure:

project/
├── ansible.cfg              # Project-specific config (roles_path, collections_paths)
├── inventory/
│   ├── production           # Production hosts inventory
│   └── staging              # Staging hosts inventory
├── group_vars/
│   ├── all.yml              # Variables for all hosts
│   ├── webservers.yml       # Variables for webservers group
│   └── dbservers.yml        # Variables for database group
├── host_vars/
│   └── hostname1.yml        # Per-host variable overrides
├── library/                 # Project-level custom modules (optional)
├── filter_plugins/          # Project-level custom filters (optional)
├── requirements.yml         # Pinned role/collection dependencies
├── site.yml                 # Master playbook (includes all tier playbooks)
├── webservers.yml           # Playbook targeting webservers group
├── dbservers.yml            # Playbook targeting dbservers group
└── roles/
    ├── common/              # The "common" role (standard layout inside)
    ├── webtier/             # The "webtier" role
    └── monitoring/          # The "monitoring" role

Key conventions from the official best practices:

  • site.yml is the master playbook that includes per-tier playbooks.
  • Tier playbooks (webservers.yml, dbservers.yml) map host groups to their roles.
  • group_vars/ holds variables by function (webservers, dbservers) and geography (atlanta, boston).
  • Tags enable selective execution: ansible-playbook site.yml --tags ntp.
  • requirements.yml pins role and collection versions for reproducibility.

Jeff Geerling (author of many popular Ansible roles) recommends project-local role and collection paths (set via ansible.cfg) so each project tracks its own dependencies independently.

Collections vs standalone roles

When a role lives inside a collection, the structure changes at the collection level:

collection/
├── galaxy.yml               # Collection manifest (required)
├── README.md
├── docs/                    # Collection-level documentation
├── meta/
│   └── runtime.yml          # Collection metadata, plugin routing, deprecations
├── plugins/                 # All plugins (modules, lookup, filter, etc.)
│   ├── modules/
│   ├── lookup/
│   └── filter/
├── roles/                   # Collection roles
│   ├── role1/               # Same internal layout as standalone role
│   │   ├── tasks/
│   │   ├── defaults/
│   │   ├── vars/
│   │   ├── templates/
│   │   ├── files/
│   │   ├── handlers/
│   │   └── meta/
│   └── role2/
├── playbooks/               # Collection playbooks (FQCN-referencable)
└── tests/                   # ansible-test integration tests

Critical differences for collection-hosted roles:

  1. No embedded plugins - roles can't have library/, filter_plugins/, etc. All plugins live in the collection's plugins/ directory.
  2. Role names restricted - lowercase alphanumeric + underscore, must start with alpha character.
  3. role_name in meta/main.yml is ignored - Galaxy uses the directory name.
  4. meta/main.yml is optional (but recommended) for collection roles, whereas it's required for standalone roles submitted to Galaxy.

Variable precedence in roles

Understanding where to place variables is critical. Within roles, the hierarchy (lowest to highest):

  1. defaults/main.yml - lowest, designed to be overridden
  2. Inventory group_vars/
  3. Inventory host_vars/
  4. Playbook vars: / vars_files
  5. vars/main.yml - high, for internal constants
  6. Task vars:
  7. Extra vars (-e) - highest, overrides everything

Best practice: put most values in defaults/ so consumers can customize without forking. Reserve vars/ for OS-specific paths, service names, and internal constants. Always namespace variables with the role name prefix (e.G., nginx_port, not port).

Limitations and critiques

  • Scattered visibility: the directory layout means you can't read a playbook and understand what it executes; you must navigate through multiple main.yml files across subdirectories. (Source: Roelof Jan Elsinga)
  • Cross-platform boilerplate: writing roles that work across RHEL and Debian requires when: ansible_os_family conditionals and OS-specific variable files, which creates maintenance overhead. This was raised as a pain point on the Ansible forum and GitHub issues. (Source: Ansible Forum, GitHub issue #52759)
  • Role granularity debate: the community lacks consensus on whether roles should represent "server roles" (e.G., webserver role that does everything) or "functions" (e.G., separate nginx, firewall, monitoring roles). (Source: Ansible Forum, Jul 2024)
  • Standalone vs collection confusion: the migration from standalone roles to collections creates uncertainty about which format to use for new roles. (Source: Adfinis Ansible Guide)
  • tests/ directory ambiguity: generated by ansible-galaxy init but not counted among "seven main standard directories." In practice, most roles use Molecule (molecule/ directory) instead of the built-in tests/, creating a mismatch between the scaffold and actual testing practice. (Source: CMU SEI; molecule documentation)

Open questions

  • Will Ansible eventually deprecate standalone roles in favor of collections-only distribution? The Adfinis guide notes there is no official announcement, but the direction is clear.
  • How will the extensions/ directory in collections evolve? It's described as a placeholder for future features.
  • Will the Molecule project eventually replace or merge with the tests/ directory convention generated by ansible-galaxy init?

Practical takeaways

  • Start every new role with ansible-galaxy role init <name> - it produces the correct scaffold and saves time.
  • For new projects, prefer collections over standalone role repos. Bundle related roles, modules, and plugins in one collection repository.
  • Use defaults/main.yml for everything users might override. Reserve vars/main.yml for OS-specific constants loaded via include_vars or first_found.
  • Split tasks into separate files (e.G., install.yml, config.yml) and use main.yml only as a router with import_tasks and tag assignments.
  • Match target filesystem paths inside files/ and templates/ directories for clarity.
  • Pin role/collection versions in requirements.yml and use project-local paths via ansible.cfg for reproducible deployments.
  • Use Molecule for testing instead of the basic tests/ directory generated by ansible-galaxy init.
  • Namespace all role variables with the role name prefix to prevent collisions in multi-role projects.

Sources used