Ansible Playbook Structure — Project Layout Best Practices

Introduction

A well-organized Ansible project is the difference between maintainable automation and spaghetti YAML. This guide covers the recommended directory structure from single playbooks to enterprise-scale projects with multiple environments, roles, and collections.

Minimal Project

project/
├── ansible.cfg
├── inventory.yml
└── site.yml
# ansible.cfg
[defaults]
inventory = inventory.yml
roles_path = roles
retry_files_enabled = false
stdout_callback = yaml

[privilege_escalation]
become = true
become_method = sudo
# inventory.yml
all:
  children:
    webservers:
      hosts:
        web01:
          ansible_host: 192.168.1.10
        web02:
          ansible_host: 192.168.1.11
    databases:
      hosts:
        db01:
          ansible_host: 192.168.1.20

Standard Project Layout

project/
├── ansible.cfg
├── requirements.yml          # Galaxy roles/collections
├── site.yml                  # Master playbook
├── webservers.yml            # Per-group playbook
├── databases.yml
│
├── inventory/
│   ├── production/
│   │   ├── hosts.yml
│   │   ├── group_vars/
│   │   │   ├── all.yml
│   │   │   ├── webservers.yml
│   │   │   └── databases.yml
│   │   └── host_vars/
│   │       └── db01.yml
│   └── staging/
│       ├── hosts.yml
│       └── group_vars/
│           └── all.yml
│
├── roles/
│   ├── common/
│   │   ├── defaults/main.yml
│   │   ├── handlers/main.yml
│   │   ├── tasks/main.yml
│   │   ├── templates/
│   │   └── files/
│   ├── nginx/
│   └── postgresql/
│
├── files/                    # Global files
├── templates/                # Global templates
└── filter_plugins/           # Custom filters

Inventory Organization

Single Environment

# inventory.yml
all:
  vars:
    ansible_user: deploy
    ansible_python_interpreter: /usr/bin/python3
  children:
    webservers:
      hosts:
        web[01:10]:
    databases:
      hosts:
        db[01:03]:
    loadbalancers:
      hosts:
        lb01:
        lb02:

Multi-Environment

# Use different inventories per environment
ansible-playbook site.yml -i inventory/staging/
ansible-playbook site.yml -i inventory/production/
# inventory/production/group_vars/all.yml
env: production
domain: app.example.com
nginx_worker_connections: 4096
db_pool_size: 50

# inventory/staging/group_vars/all.yml
env: staging
domain: dev.example.com
nginx_worker_connections: 1024
db_pool_size: 10

Group Variables

# inventory/production/group_vars/webservers.yml
nginx_version: "1.26"
nginx_worker_processes: auto
nginx_sites:
  - name: app
    server_name: "{{ domain }}"
    root: /var/www/app
    ssl: true

# inventory/production/group_vars/databases.yml
postgresql_version: "16"
postgresql_max_connections: 200
postgresql_shared_buffers: "4GB"

Encrypted Variables

# Create encrypted file
ansible-vault create inventory/production/group_vars/databases/vault.yml
# inventory/production/group_vars/databases/vault.yml
vault_db_password: "s3cret!"
vault_replication_password: "r3pl!ca"

# inventory/production/group_vars/databases/main.yml (unencrypted)
db_password: "{{ vault_db_password }}"
replication_password: "{{ vault_replication_password }}"

Playbook Organization

Master Playbook (site.yml)

---
# site.yml — master playbook
- name: Apply common configuration
  hosts: all
  roles:
    - common

- name: Configure web servers
  hosts: webservers
  roles:
    - nginx
    - app

- name: Configure databases
  hosts: databases
  roles:
    - postgresql
    - backup

Per-Group Playbooks

# webservers.yml — run only web server tasks
---
- name: Configure web servers
  hosts: webservers
  roles:
    - common
    - nginx
    - app

Task-Specific Playbooks

# deploy.yml — application deployment only
---
- name: Deploy application
  hosts: webservers
  serial: "25%"
  tasks:
    - name: Pull latest code
      ansible.builtin.git:
        repo: https://github.com/org/app.git
        dest: /opt/app
        version: "{{ app_version | default('main') }}"

    - name: Restart application
      ansible.builtin.systemd:
        name: app
        state: restarted

Role Structure

roles/nginx/
├── defaults/
│   └── main.yml          # Default variables (lowest priority)
├── vars/
│   └── main.yml          # Role variables (higher priority)
├── tasks/
│   ├── main.yml          # Entry point
│   ├── install.yml
│   ├── configure.yml
│   └── service.yml
├── handlers/
│   └── main.yml          # Service restart handlers
├── templates/
│   ├── nginx.conf.j2
│   └── site.conf.j2
├── files/
│   └── ssl-params.conf
├── meta/
│   └── main.yml          # Dependencies and metadata
└── README.md
# roles/nginx/tasks/main.yml
---
- name: Install nginx
  ansible.builtin.import_tasks: install.yml
  tags: [nginx, install]

- name: Configure nginx
  ansible.builtin.import_tasks: configure.yml
  tags: [nginx, configure]

- name: Manage nginx service
  ansible.builtin.import_tasks: service.yml
  tags: [nginx, service]
# roles/nginx/meta/main.yml
---
dependencies:
  - role: common
  - role: ssl
    when: nginx_ssl_enabled | default(false)

galaxy_info:
  author: Your Name
  description: Nginx web server
  license: MIT
  min_ansible_version: "2.15"
  platforms:
    - name: Ubuntu
      versions: [22.04, 24.04]
    - name: EL
      versions: [8, 9]

Collections Requirements

# requirements.yml
---
roles:
  - name: geerlingguy.docker
    version: "7.4.0"

collections:
  - name: community.general
    version: ">=9.0.0"
  - name: ansible.posix
    version: ">=1.5.0"
  - name: community.postgresql
# Install dependencies
ansible-galaxy install -r requirements.yml
ansible-galaxy collection install -r requirements.yml

Enterprise Layout

ansible-infra/
├── ansible.cfg
├── Makefile                  # Common commands
├── requirements.yml
│
├── playbooks/
│   ├── site.yml
│   ├── deploy.yml
│   ├── rollback.yml
│   ├── security-audit.yml
│   └── disaster-recovery.yml
│
├── inventory/
│   ├── production/
│   ├── staging/
│   ├── development/
│   └── dynamic/
│       └── aws_ec2.yml       # Dynamic inventory plugin
│
├── roles/
│   ├── internal/             # Custom roles
│   │   ├── common/
│   │   ├── app/
│   │   └── monitoring/
│   └── external/             # Galaxy roles (gitignored)
│
├── collections/              # Local collections
│
├── plugins/
│   ├── filter/
│   ├── callback/
│   └── inventory/
│
├── files/                    # Global shared files
├── templates/                # Global shared templates
│
├── tests/
│   ├── molecule/             # Role tests
│   └── integration/          # Playbook tests
│
├── docs/
│   └── runbooks/
│
├── .github/
│   └── workflows/
│       ├── lint.yml
│       └── test.yml
│
├── .ansible-lint             # Linting config
├── .yamllint                 # YAML linting
└── .gitignore

Makefile for Common Commands

.PHONY: lint test deploy

lint:
	ansible-lint playbooks/
	yamllint .

test:
	ansible-playbook playbooks/site.yml --check --diff -i inventory/staging/

deploy-staging:
	ansible-playbook playbooks/deploy.yml -i inventory/staging/

deploy-production:
	ansible-playbook playbooks/deploy.yml -i inventory/production/ --ask-vault-pass

requirements:
	ansible-galaxy install -r requirements.yml --force
	ansible-galaxy collection install -r requirements.yml --force

.gitignore

# Ansible
*.retry
roles/external/
collections/ansible_collections/

# Vault
*.vault
vault_password

# Secrets
*.pem
*.key

# OS
.DS_Store
*.swp

Troubleshooting

IssueSolution
Role not foundCheck roles_path in ansible.cfg
Variable not definedCheck precedence: defaults < group_vars < host_vars < playbook
Wrong inventoryVerify with ansible-inventory -i inventory/prod/ --list
Vault passwordUse --vault-password-file or ANSIBLE_VAULT_PASSWORD_FILE

Best Practices

  1. One role per concern — nginx, postgresql, monitoring, not server-setup
  2. Environment parity — same roles, different variables per environment
  3. Encrypt secrets — ansible-vault for passwords, keys, tokens
  4. Pin versions — lock Galaxy roles and collections in requirements.yml
  5. Use tags — enable selective execution of tasks
  6. Lint everything — ansible-lint and yamllint in CI/CD
  7. Document — README per role, runbooks for operations

Conclusion

Good project structure scales from a single playbook to hundreds of roles across multiple environments. Start simple, add structure as complexity grows, and keep your automation maintainable with clear separation between roles, inventory, and playbooks.