Back to skills
SKILL.md
Ansible Role Design
BSecurityProduction-grade Ansible role structure patterns for directory layout, variable organization (defaults vs vars), handlers, and task organization, derived from analysis of 7 geerlingguy production roles.
- 2 stars
- 0 votes
- 0 copies
- 3 views
- Added September 11, 2026
Security analysis
88/100- Accesses sensitive system or user directories
Pro scans all 6 files and shows the line behind each finding
npx -y skills add jcasnellie69/homelab-config --skill ansible-role-design --agent claude-codeAre you the author of Ansible Role Design?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/jcasnellie69-ansible-role-design)---
name: ansible-role-design
description: >
Production-grade Ansible role structure patterns for directory layout, variable
organization (defaults vs vars), handlers, and task organization, derived from
analysis of 7 geerlingguy production roles.
when_to_use: >
Use when creating Ansible roles, designing role directory structure, organizing role
variables in defaults vs vars, writing role handlers, or structuring role tasks.
---
# Ansible Role Design
Production-grade role structure patterns derived from analysis of 7 geerlingguy roles.
## Standard Directory Structure
Every Ansible role follows this organizational pattern:
```text
role-name/
├── defaults/
│ └── main.yml # User-configurable defaults (lowest precedence)
├── vars/
│ ├── Debian.yml # OS-specific internal values
│ └── RedHat.yml
├── tasks/
│ ├── main.yml # Task router
│ ├── install.yml # Feature-specific tasks
│ └── configure.yml
├── handlers/
│ └── main.yml # Event-triggered tasks
├── templates/
│ └── config.conf.j2 # Jinja2 templates
├── files/
│ └── static-file.txt # Static files
├── meta/
│ └── main.yml # Role metadata, dependencies
└── README.md # Documentation
```
### Directory Purposes
| Directory | Purpose | Precedence |
|-----------|---------|------------|
| `defaults/` | User-overridable values | Lowest |
| `vars/` | Internal/OS-specific values | High |
| `tasks/` | Ansible tasks | N/A |
| `handlers/` | Service restarts, reloads | N/A |
| `templates/` | Jinja2 config files | N/A |
| `files/` | Static files to copy | N/A |
| `meta/` | Galaxy info, dependencies | N/A |
### When to Omit Directories
Only create directories that are actually needed:
- **Omit `templates/`** if using only `lineinfile` or `copy`
- **Omit `handlers/`** if role doesn't manage services
- **Omit `vars/`** if no OS-specific differences
- **Omit `files/`** if no static files to copy
## Task Organization
### Main Task File as Router
Use `tasks/main.yml` as a routing file that includes feature-specific files:
```yaml
# tasks/main.yml
---
- name: Include OS-specific variables
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
- name: Install packages
ansible.builtin.include_tasks: install.yml
- name: Configure service
ansible.builtin.include_tasks: configure.yml
- name: Setup users
ansible.builtin.include_tasks: users.yml
when: role_users | length > 0
```
### When to Split Tasks
| Scenario | Approach |
|----------|----------|
| < 30 lines | Keep in main.yml |
| 30-100 lines | Consider splitting |
| > 100 lines | Definitely split |
| Optional features | Separate file with `when:` |
| OS-specific logic | Separate files per OS |
### Task File Naming
Use descriptive, feature-based names:
```text
tasks/
├── main.yml # Router only
├── install.yml # Package installation
├── configure.yml # Configuration tasks
├── users.yml # User management
├── install-Debian.yml # Debian-specific install
└── install-RedHat.yml # RedHat-specific install
```
## Variable Organization
### defaults/ vs vars/
| Location | Purpose | User Override? |
|----------|---------|----------------|
| `defaults/main.yml` | User configuration | Yes (easily) |
| `vars/main.yml` | Internal constants | Possible but discouraged |
| `vars/Debian.yml` | OS-specific values | No (internal) |
### defaults/main.yml Example
```yaml
# defaults/main.yml
---
# User-configurable options
docker_edition: "ce"
docker_service_state: started
docker_service_enabled: true
docker_users: []
# Feature toggles
docker_install_compose: true
docker_compose_version: "2.24.0"
```
### vars/Debian.yml Example
```yaml
# vars/Debian.yml
---
# OS-specific internal values (not for user override)
docker_package_name: docker-ce
docker_service_name: docker
docker_config_path: /etc/docker/daemon.json
```
### Loading OS-Specific Variables
Simple pattern:
```yaml
- name: Include OS-specific variables
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
```
Advanced pattern with fallback:
```yaml
- name: Load OS-specific vars
ansible.builtin.include_vars: "{{ lookup('first_found', params) }}"
vars:
params:
files:
- "{{ ansible_distribution }}.yml"
- "{{ ansible_os_family }}.yml"
- main.yml
paths:
- vars
```
## Variable Naming Convention
Prefix variables with role name:
```yaml
# Pattern: {role_name}_{feature}_{attribute}
# Examples
docker_edition: "ce"
docker_service_state: started
docker_compose_version: "2.24.0"
docker_users: []
# Grouped by feature
security_ssh_port: 22
security_ssh_password_auth: "no"
security_fail2ban_enabled: true
```
### Benefits
- Prevents conflicts with other roles
- Clear ownership of variables
- Easy to grep across codebase
- Self-documenting
## Handler Patterns
### Simple Handler Definitions
```yaml
# handlers/main.yml
---
- name: restart docker
ansible.builtin.systemd:
name: docker
state: restarted
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
```
### Handler Naming
Use lowercase with action + service pattern:
```yaml
- name: restart ssh # Not "Restart SSH Service"
- name: reload nginx # Not "Reload Nginx Config"
- name: reload systemd # For daemon-reload
```
### Throttled Handlers
For cluster operations, restart one node at a time:
```yaml
- name: restart pve-cluster
ansible.builtin.systemd:
name: pve-cluster
state: restarted
throttle: 1
```
## Template Organization
### When to Use Templates
Use `templates/` when:
- Configuration has conditional content
- Need variable substitution
- Complex multi-line configuration
- Users may need to extend/override
Use `lineinfile` when:
- Simple single-line changes
- Modifying existing system files
### Template Variables
Expose template paths as variables for user override:
```yaml
# defaults/main.yml
nginx_conf_template: nginx.conf.j2
nginx_vhost_template: vhost.j2
```
```yaml
# tasks/configure.yml
- name: Deploy nginx config
ansible.builtin.template:
src: "{{ nginx_conf_template }}"
dest: /etc/nginx/nginx.conf
notify: reload nginx
```
## Meta Configuration
### meta/main.yml Structure
```yaml
# meta/main.yml
---
galaxy_info:
author: your_name
description: Role description
license: MIT
min_ansible_version: "2.12"
platforms:
- name: Debian
versions:
- bullseye
- bookworm
- name: Ubuntu
versions:
- focal
- jammy
dependencies:
- role: common
- role: geerlingguy.docker
when: install_docker | default(false)
```
## Role Complexity Scaling
Based on geerlingguy role analysis:
| Role Complexity | Directories | Task Files | Examples |
|-----------------|-------------|------------|----------|
| Minimal | 3-4 | 1 (main.yml) | pip, git |
| Standard | 5-6 | 2-4 | security, docker |
| Complex | 7+ | 5-8 | postgresql, nginx |
### Minimal Role
```text
pip/
├── defaults/main.yml
├── tasks/main.yml
├── meta/main.yml
└── README.md
```
### Standard Role
```text
docker/
├── defaults/main.yml
├── vars/{Debian,RedHat}.yml
├── tasks/{main,install,configure}.yml
├── handlers/main.yml
├── meta/main.yml
└── README.md
```
### Complex Role
```text
postgresql/
├── defaults/main.yml
├── vars/{Debian,RedHat,Archlinux}.yml
├── tasks/{main,install,configure,users,databases}.yml
├── handlers/main.yml
├── templates/{postgresql.conf,pg_hba.conf}.j2
├── meta/main.yml
└── README.md
```
## Task Naming Convention
Start task names with action verbs:
```yaml
# GOOD
- name: Ensure Docker is installed
- name: Configure SSH security settings
- name: Add user to docker group
# BAD
- name: Docker installation
- name: SSH settings
- name: User docker group
```
## File Validation
Validate critical configuration files:
```yaml
- name: Update SSH configuration
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "^PermitRootLogin"
line: "PermitRootLogin no"
validate: 'sshd -T -f %s'
notify: restart ssh
- name: Update sudoers
ansible.builtin.lineinfile:
path: /etc/sudoers
line: "{{ user }} ALL=(ALL) NOPASSWD: ALL"
validate: 'visudo -cf %s'
```
## Documentation
Every role needs a README.md with:
1. **Description** - What the role does
2. **Requirements** - Prerequisites
3. **Role Variables** - All variables with defaults
4. **Dependencies** - Other roles needed
5. **Example Playbook** - How to use it
## Additional Resources
For detailed role design patterns and techniques, consult:
- **`references/role-structure-standards.md`** - Production role structure patterns from geerlingguy analysis
- **`references/handler-best-practices.md`** - Handler design, notification patterns, flush strategies
- **`references/meta-dependencies.md`** - Role dependencies, Galaxy metadata, platform support
- **`references/variable-management-patterns.md`** - Variable naming, scoping, precedence patterns
- **`references/documentation-templates.md`** - README templates and documentation standards
## Related Skills
- **ansible-playbook-design** - When to use roles vs playbooks
- **ansible-fundamentals** - Module selection and naming
- **ansible-testing** - Role testing with molecule
Files in this skill
- SKILL.md
- references/documentation-templates.md
- references/handler-best-practices.md
- references/meta-dependencies.md
- references/role-structure-standards.md
- references/variable-management-patterns.md
Attribution
Comments
Loading comments…