41 KiB
Role Structure Standards
Summary: Pattern Confidence
Analyzed 7 geerlingguy roles: security, users, docker, postgresql, nginx, pip, git
Universal Patterns (All 7 roles):
- Standard Ansible role directory structure (defaults/, tasks/, meta/, molecule/, .github/) (7/7 roles)
- tasks/main.yml as router with include_tasks/import_tasks (7/7 roles)
- Role-prefixed variable names preventing conflicts (7/7 roles use rolename_feature_attribute)
- Snake_case naming convention throughout (7/7 roles)
- defaults/ for user configuration, vars/ for OS-specific values (7/7 roles)
- Descriptive task names starting with action verbs (7/7 roles)
- Configuration file validation before applying (sshd -T, visudo -cf) (7/7 security-sensitive roles)
- Explicit file permissions on security-sensitive files (7/7 roles)
- Quality control files (.ansible-lint, .yamllint, .gitignore) (7/7 roles)
Contextual Patterns (Varies by complexity):
- Task file organization: simple roles use single main.yml, complex roles split into 8+ feature files
- vars/ directory presence: only when OS-specific data needed (4/7 roles have it)
- templates/ usage: complex config roles use templates/ heavily, simple roles use lineinfile/copy
- handlers/ presence: only service-managing roles need handlers (4/7 roles have them)
- Directory count scales with complexity: minimal roles (pip) have 3 dirs, complex roles (postgresql) have 7+ dirs
Evolving Patterns (Newer roles improved):
- Advanced include_vars with first_found lookup (docker role) provides fallback chain for better distribution support
- import_tasks vs include_tasks distinction: import for ordered execution, include for conditional
- Jinja2 block inheritance in templates (nginx role) for user extensibility without full template replacement
Sources:
- geerlingguy.security (analyzed 2025-10-23)
- geerlingguy.github-users (analyzed 2025-10-23)
- geerlingguy.docker (analyzed 2025-10-23)
- geerlingguy.postgresql (analyzed 2025-10-23)
- geerlingguy.nginx (analyzed 2025-10-23)
- geerlingguy.pip (analyzed 2025-10-23)
- geerlingguy.git (analyzed 2025-10-23)
Repositories:
- https://github.com/geerlingguy/ansible-role-security
- https://github.com/geerlingguy/ansible-role-github-users
- https://github.com/geerlingguy/ansible-role-docker
- https://github.com/geerlingguy/ansible-role-postgresql
- https://github.com/geerlingguy/ansible-role-nginx
- https://github.com/geerlingguy/ansible-role-pip
- https://github.com/geerlingguy/ansible-role-git
Pattern Confidence Levels (Historical)
Analyzed 2 geerlingguy roles: security, github-users
Universal Patterns (Both roles use identical approach):
- ✅ Standard directory structure - Both follow defaults/, tasks/, meta/, molecule/, .github/ structure
- ✅ Role-prefixed variable names - security_, github_users_ (prevents conflicts)
- ✅ Descriptive task names - Action verb + object pattern ("Ensure...", "Add...", "Update...")
- ✅ defaults/ for user configuration - All user-overridable values in defaults/main.yml
- ✅ Snake_case naming - Consistent variable naming convention
- ✅ Inline validation - validate parameter for critical config files
- ✅ File permissions - Explicit mode settings on all files
- ✅ Quality control files - .ansible-lint, .yamllint, .gitignore present
Contextual Patterns (Varies by role complexity):
- ⚠️ Task file organization - security splits tasks (ssh.yml, fail2ban.yml), github-users keeps single main.yml (role is simpler)
- ⚠️ vars/ directory - security has OS-specific vars files, github-users doesn't need them
- ⚠️ templates/ usage - security uses templates for fail2ban config, github-users has no templates
- ⚠️ handlers/ - security has 3 handlers (services to restart), github-users has none (no services managed)
- ⚠️ Conditional task execution - security uses OS-family conditionals, github-users is OS-agnostic
Key Finding: Simple roles (like github-users) can keep all tasks in main.yml. Complex roles (like security) should split into feature-based files when tasks exceed ~30-40 lines.
Overview
This document captures role structure and organization patterns from production-grade Ansible roles, demonstrating how to organize tasks, variables, handlers, and templates for maintainability and clarity.
Directory Organization
Pattern: Standard Ansible Role Structure
Description: Follow the standard Ansible role directory structure for consistency and Galaxy compatibility.
Directory Tree:
ansible-role-security/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── release.yml
│ └── stale.yml
├── defaults/
│ └── main.yml
├── handlers/
│ └── main.yml
├── meta/
│ └── main.yml
├── molecule/
│ └── default/
│ ├── converge.yml
│ └── molecule.yml
├── tasks/
│ ├── main.yml
│ ├── ssh.yml
│ ├── fail2ban.yml
│ ├── autoupdate-RedHat.yml
│ └── autoupdate-Debian.yml
├── templates/
│ └── jail.local.j2
├── vars/
│ ├── Debian.yml
│ └── RedHat.yml
├── .ansible-lint
├── .gitignore
├── .yamllint
├── LICENSE
└── README.md
Directory Purposes:
- defaults/ - User-overridable default values (lowest precedence)
- vars/ - OS-specific or internal variables (high precedence)
- tasks/ - Ansible tasks organized into logical files
- handlers/ - Event-triggered tasks (service restarts, reloads)
- templates/ - Jinja2 templates for configuration files
- meta/ - Role metadata (Galaxy info, dependencies)
- molecule/ - Testing scenarios and configurations
- .github/workflows/ - CI/CD automation
- files/ - Static files (not used in this role, but common)
When to Use:
- Always create this base structure for new roles
- Omit directories you don't need (files/, templates/ if unused)
- Add molecule/ for all production roles
- Include .github/workflows/ for open source or team roles
Anti-pattern:
- Don't create directories you won't use (empty dirs confuse users)
- Avoid non-standard directory names
- Don't mix role content with playbooks in same directory
Task Organization
Pattern: Main Task File as Router
Description: Use tasks/main.yml as a routing file that includes other task files based on conditions. This keeps the main file simple and delegates work to focused task files.
File Path: tasks/main.yml
Example Code:
---
- name: Include OS-specific variables.
include_vars: "{{ ansible_os_family }}.yml"
# Fail2Ban
- include_tasks: fail2ban.yml
when: security_fail2ban_enabled | bool
# SSH
- include_tasks: ssh.yml
# Autoupdate
- include_tasks: autoupdate-RedHat.yml
when:
- ansible_os_family == 'RedHat'
- security_autoupdate_enabled | bool
- include_tasks: autoupdate-Debian.yml
when:
- ansible_os_family == 'Debian'
- security_autoupdate_enabled | bool
Key Elements:
- include_vars at top - Load OS-specific variables first
- Logical grouping - Each include_tasks represents a feature
- Conditional includes - Only run tasks when needed
- Comments as section headers - Improve readability
- Boolean filter -
| boolensures proper boolean evaluation - Multi-line conditions - Use list format for multiple when clauses
Task File Organization Strategy:
- Feature-based: ssh.yml, fail2ban.yml (grouped by functionality)
- OS-specific: autoupdate-RedHat.yml, autoupdate-Debian.yml (split by platform)
When to Use:
- Split tasks into separate files when >30-40 lines
- Create OS-specific task files for platform differences
- Use conditional includes for optional features
- Keep main.yml under 50 lines as a routing file
Anti-pattern:
- Don't put all tasks in main.yml (hard to maintain)
- Avoid deep nesting of include_tasks (max 2 levels)
- Don't split too granularly (each file should have 10+ lines)
Pattern: Feature-Specific Task Files
Description: Create focused task files for specific features, with clear names that describe their purpose.
File Path: tasks/ssh.yml
Example Code:
---
- name: Ensure SSH daemon is running.
service:
name: "{{ security_sshd_name }}"
state: "{{ security_sshd_state }}"
- name: Update SSH configuration to be more secure.
lineinfile:
dest: "{{ security_ssh_config_path }}"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
validate: 'sshd -T -f %s'
mode: 0644
with_items:
- regexp: "^PasswordAuthentication"
line: "PasswordAuthentication {{ security_ssh_password_authentication }}"
- regexp: "^PermitRootLogin"
line: "PermitRootLogin {{ security_ssh_permit_root_login }}"
- regexp: "^Port"
line: "Port {{ security_ssh_port }}"
- regexp: "^UseDNS"
line: "UseDNS {{ security_ssh_usedns }}"
- regexp: "^PermitEmptyPasswords"
line: "PermitEmptyPasswords {{ security_ssh_permit_empty_password }}"
- regexp: "^ChallengeResponseAuthentication"
line: "ChallengeResponseAuthentication {{ security_ssh_challenge_response_auth }}"
- regexp: "^GSSAPIAuthentication"
line: "GSSAPIAuthentication {{ security_ssh_gss_api_authentication }}"
- regexp: "^X11Forwarding"
line: "X11Forwarding {{ security_ssh_x11_forwarding }}"
notify:
- reload systemd
- restart ssh
- name: Add configured users allowed to connect over ssh
lineinfile:
dest: "{{ security_ssh_config_path }}"
regexp: '^AllowUsers'
line: "AllowUsers {{ security_ssh_allowed_users | join(' ') }}"
state: present
create: true
validate: 'sshd -T -f %s'
mode: 0644
when: security_ssh_allowed_users | length > 0
notify: restart ssh
- name: Add configured user accounts to passwordless sudoers.
lineinfile:
dest: /etc/sudoers
regexp: '^{{ item }}'
line: '{{ item }} ALL=(ALL) NOPASSWD: ALL'
state: present
validate: 'visudo -cf %s'
mode: 0440
with_items: "{{ security_sudoers_passwordless }}"
when: security_sudoers_passwordless | length > 0
Key Patterns:
-
Validation parameters:
validate: 'sshd -T -f %s'- Test SSH config before applyingvalidate: 'visudo -cf %s'- Validate sudoers syntax- Prevents breaking critical system files
-
Idempotent configuration:
- lineinfile with regexp - Updates or adds lines
- state: present - Ensures line exists
- Anchored regexps (^) - Match start of line
-
Conditional execution:
when: security_ssh_allowed_users | length > 0- Skip if empty list- Prevents unnecessary file modifications
-
Handler notifications:
notify: restart ssh- Trigger service restart on changes- Multiple handlers can be notified
- Handlers run once at end, even if notified multiple times
-
File permissions:
mode: 0644for SSH config (readable by all)mode: 0440for sudoers (read-only, no world access)
When to Use:
- Always validate critical config files (SSH, sudoers, etc.)
- Use lineinfile for simple config changes
- Notify handlers instead of inline service restarts
- Set explicit file permissions on security-sensitive files
- Use conditional execution to skip unnecessary tasks
Anti-pattern:
- Don't modify critical files without validation
- Avoid command/shell when modules exist (lineinfile vs sed)
- Don't restart services directly in tasks (use handlers)
- Avoid hardcoded paths (use variables for OS differences)
Naming Conventions
Pattern: Descriptive Variable Names with Role Prefix
Description: Prefix all role variables with the role name to avoid conflicts with other roles or playbook variables.
File Path: defaults/main.yml
Example Code:
---
security_ssh_port: 22
security_ssh_password_authentication: "no"
security_ssh_permit_root_login: "no"
security_ssh_usedns: "no"
security_ssh_permit_empty_password: "no"
security_ssh_challenge_response_auth: "no"
security_ssh_gss_api_authentication: "no"
security_ssh_x11_forwarding: "no"
security_sshd_state: started
security_ssh_restart_handler_state: restarted
security_ssh_allowed_users: []
security_ssh_allowed_groups: []
security_sudoers_passwordless: []
security_sudoers_passworded: []
security_autoupdate_enabled: true
security_autoupdate_blacklist: []
security_autoupdate_additional_origins: []
security_autoupdate_reboot: "false"
security_autoupdate_reboot_time: "03:00"
security_autoupdate_mail_to: ""
security_autoupdate_mail_on_error: true
security_fail2ban_enabled: true
security_fail2ban_custom_configuration_template: "jail.local.j2"
Naming Pattern:
{role_name}_{feature}_{attribute}
Examples:
security_ssh_port- Role: security, Feature: ssh, Attribute: portsecurity_fail2ban_enabled- Role: security, Feature: fail2ban, Attribute: enabledsecurity_autoupdate_reboot_time- Role: security, Feature: autoupdate, Attribute: reboot_time
Key Elements:
- Role prefix - All variables start with "security_"
- Feature grouping - Related variables have common prefix (security_ssh_, security_fail2ban_)
- Descriptive names - Full words, not abbreviations
- Underscore separation - snake_case, not camelCase
- Boolean as strings - "yes"/"no" for SSH config (preserves YAML booleans elsewhere)
When to Use:
- Always prefix variables with role name
- Group related variables with feature prefix
- Use descriptive names (avoid abbreviations)
- Choose meaningful defaults
- Quote string values that look like booleans ("yes", "no", "true", "false")
Anti-pattern:
- Don't use generic variable names (port, enabled, config_path)
- Avoid abbreviations (ssh_cfg instead of ssh_config)
- Don't mix naming styles (snake_case vs camelCase)
- Avoid unquoted yes/no/true/false strings (YAML interprets as booleans)
Pattern: Task Naming Convention
Description: Write task names that are descriptive, actionable, and follow a consistent format.
Task Name Pattern:
[Action verb] [object] [additional context]
Examples from the role:
- "Ensure SSH daemon is running" - State verification
- "Update SSH configuration to be more secure" - Modification action
- "Add configured users allowed to connect over ssh" - Addition action
- "Install fail2ban" - Installation action
Guidelines:
- Start with action verb - Ensure, Update, Add, Install, Configure, Remove
- Be specific - "SSH daemon" not just "daemon"
- Add context - "to be more secure" explains why
- Use present tense - "Ensure" not "Ensuring"
- Capitalize first word - "Ensure SSH..." not "ensure ssh..."
When to Use:
- Every task should have a clear name
- Name describes the desired state, not the implementation
- Use consistent verbs across the role
Anti-pattern:
- Don't use vague names ("Configure SSH", "Setup system")
- Avoid implementation details ("Run lineinfile on sshd_config")
- Don't use all caps or weird capitalization
File Placement Decisions
Pattern: defaults/ vs vars/ Usage
Description: Use defaults/ for user-overridable values and vars/ for internal/OS-specific values.
File Paths:
defaults/main.yml- User-facing configurationvars/Debian.yml- Debian-specific internal valuesvars/RedHat.yml- RedHat-specific internal values
defaults/main.yml Example:
---
# User-configurable values (low precedence)
security_ssh_port: 22
security_ssh_password_authentication: "no"
security_fail2ban_enabled: true
security_autoupdate_enabled: true
vars/Debian.yml Example:
---
# Internal OS-specific values (high precedence)
security_ssh_config_path: /etc/ssh/sshd_config
security_sshd_name: ssh
vars/RedHat.yml Example (inferred structure):
---
# Internal OS-specific values (high precedence)
security_ssh_config_path: /etc/ssh/sshd_config
security_sshd_name: sshd
Decision Matrix:
| Variable Type | Location | Precedence | Use Case |
|---|---|---|---|
| User configuration | defaults/ | Low (easily overridden) | Settings users customize |
| OS-specific paths | vars/ | High (shouldn't override) | File paths, service names |
| Internal logic | vars/ | High | Values role needs to work |
| Feature toggles | defaults/ | Low | Enable/disable features |
When to Use:
- defaults/ - Any value users might want to change
- vars/ - OS-specific values, internal constants
- Load vars/ files conditionally by OS family
- Use include_vars to load appropriate vars file
Anti-pattern:
- Don't put user-facing config in vars/ (can't be overridden easily)
- Don't put OS-specific paths in defaults/ (users shouldn't change)
- Avoid duplicating values between defaults/ and vars/
Pattern: OS-Specific Variable Files
Description: Create separate variable files for each OS family to handle platform differences.
File Path: vars/Debian.yml, vars/RedHat.yml
Loading Pattern:
- name: Include OS-specific variables.
include_vars: "{{ ansible_os_family }}.yml"
Common OS-Specific Variables:
- Service names (ssh vs sshd)
- Configuration file paths
- Package names
- Default directories
When to Use:
- Different service names across OS families
- Different file paths or package names
- OS-specific configuration options
- Load at start of tasks/main.yml
Anti-pattern:
- Don't use when: conditionals for every OS difference
- Avoid complex variable resolution logic
- Don't hardcode OS-specific values in tasks
Handler Organization
Pattern: Simple Handler Definitions
Description: Define handlers with clear names and simple actions. Handlers should do one thing well.
File Path: handlers/main.yml
Example Code:
---
- name: reload systemd
ansible.builtin.systemd_service:
daemon_reload: true
- name: restart ssh
ansible.builtin.service:
name: "{{ security_sshd_name }}"
state: "{{ security_ssh_restart_handler_state }}"
- name: reload fail2ban
ansible.builtin.service:
name: fail2ban
state: reloaded
Key Elements:
- Descriptive names - Action + service (restart ssh, reload fail2ban)
- Single responsibility - Each handler does one thing
- Configurable state - Uses variable for restart/reload state
- Lowercase names - "reload systemd" not "Reload Systemd"
- Service vs systemd_service - Use appropriate module
Handler Naming Pattern:
[action] [service/component]
Examples:
- "restart ssh" - Restart SSH service
- "reload systemd" - Reload systemd daemon
- "reload fail2ban" - Reload fail2ban configuration
When to Use:
- Create one handler per service/action combination
- Use simple, action-oriented names
- Make handler behavior configurable via variables
- Use reload instead of restart when possible (less disruptive)
Anti-pattern:
- Don't combine multiple actions in one handler
- Avoid complex logic in handlers
- Don't use handlers for non-idempotent actions
Comparison to Virgo-Core Roles
system_user Role
Structure Analysis:
system_user/
├── defaults/
│ └── main.yml ✅
├── handlers/
│ └── main.yml (empty - appropriate, no services)
├── meta/
│ └── main.yml ✅
├── tasks/
│ └── main.yml ✅ (single file appropriate for scope)
├── templates/
│ └── sudoers.j2 ✅
└── README.md ✅
Matches:
- ✅ Proper defaults/ usage
- ✅ Appropriate task organization (role is simple enough for single file)
- ✅ Variable naming with role prefix (system_user_*)
- ✅ Clear task names
Gaps:
- ⚠️ No vars/ directory for OS-specific values (may not be needed)
- ❌ No molecule/ testing directory
- ❌ No .github/workflows/ for CI
Priority Actions:
- Nice-to-have: Add vars/ files if supporting multiple OS families (30 min)
- Critical: Add molecule/ directory (covered in testing-comprehensive.md)
proxmox_access Role
Structure Analysis:
proxmox_access/
├── defaults/
│ └── main.yml ✅
├── handlers/
│ └── main.yml ✅ (appropriate handlers defined)
├── meta/
│ └── main.yml ✅
├── tasks/
│ ├── main.yml ✅ (good routing pattern)
│ ├── roles.yml ✅
│ ├── groups.yml ✅
│ ├── users.yml ✅
│ ├── tokens.yml ✅
│ └── acls.yml ✅
├── templates/
│ └── terraform_env.sh.j2 ✅
└── README.md ✅
Matches:
- ✅ Excellent task organization (main.yml as router)
- ✅ Feature-based task files
- ✅ Proper variable naming (proxmox_access_*)
- ✅ Good handler usage
Gaps:
- ❌ No molecule/ testing directory
- ❌ No .github/workflows/ for CI
- ⚠️ No vars/ directory (but tasks include OS detection)
Priority Actions:
- Critical: Add molecule/ directory (covered in testing-comprehensive.md)
- Nice-to-have: Add vars/ files for Proxmox-specific paths (1 hour)
proxmox_network Role
Structure Analysis:
proxmox_network/
├── defaults/
│ └── main.yml ✅
├── handlers/
│ └── main.yml ✅ (network reload handler)
├── meta/
│ └── main.yml ✅
├── tasks/
│ ├── main.yml ✅
│ ├── bridges.yml ✅
│ ├── vlans.yml ✅
│ └── verify.yml ✅ (excellent - verification tasks)
└── README.md ✅
Matches:
- ✅ Good task organization
- ✅ Verification tasks (verify.yml) - advanced pattern
- ✅ Proper handlers for network changes
- ✅ Variable naming conventions
Gaps:
- ❌ No molecule/ testing directory
- ❌ No .github/workflows/ for CI
- ⚠️ No templates/ directory (uses lineinfile, which is fine)
Priority Actions:
- Critical: Add molecule/ directory with network verification (covered in testing-comprehensive.md)
Validation: geerlingguy.docker
Analysis Date: 2025-10-23 Repository: https://github.com/geerlingguy/ansible-role-docker
Directory Organization
- Pattern: Standard Ansible role structure - ✅ Confirmed
- Docker role has: defaults/, tasks/, handlers/, meta/, molecule/, .github/, vars/
- No templates/ directory (docker uses copy module with content parameter)
- Confirms that omitting unused directories is correct pattern
Task Organization
-
Pattern: tasks/main.yml as router - ✅ Confirmed
- main.yml loads OS-specific vars, then includes setup-{RedHat,Suse,Debian}.yml
- Same conditional include pattern as security role
- Observation: Uses more advanced include_vars with first_found lookup (evolution of simple include_vars pattern)
-
Pattern: Feature-based task files - ✅ Confirmed
- Tasks split by OS family: setup-RedHat.yml, setup-Suse.yml, setup-Debian.yml
- Additional feature files: docker-compose.yml, docker-users.yml
- Confirms pattern: Split by OS when logic differs, by feature when optional
Variable Naming
-
Pattern: Role-prefixed variables - ✅ Confirmed
- All variables prefixed with
docker_: docker_edition, docker_packages, docker_service_state, etc. - Confirms naming pattern is universal
- All variables prefixed with
-
Pattern: Feature grouping - ✅ Confirmed
- docker_service_* for service management
- docker_compose_* for compose options
- docker_apt_* for Debian-specific vars
- docker_yum_* for RedHat-specific vars
defaults/ vs vars/ Usage
- Pattern: defaults/ for user config, vars/ for OS-specific - ✅ Confirmed
- defaults/main.yml: All user-configurable options (packages, service state, repo URLs)
- vars/{RedHat,Debian,Suse}.yml: OS-specific package names and repo details
- Confirms this is standard practice across all roles
Task Naming Convention
- Pattern: Descriptive action verb + object - ✅ Confirmed
- "Load OS-specific vars."
- "Install Docker packages."
- "Configure Docker daemon options."
- "Ensure Docker is started and enabled at boot."
- Same pattern as security/users roles
Advanced Pattern: first_found Lookup
-
Pattern Evolution: Docker role uses advanced vars loading:
- name: Load OS-specific vars. include_vars: "{{ lookup('first_found', params) }}" vars: params: files: - '{{ansible_facts.distribution}}.yml' - '{{ansible_facts.os_family}}.yml' - main.yml paths: - 'vars'- vs security simple pattern:
include_vars: "{{ ansible_os_family }}.yml" - Insight: More complex roles use fallback chain for better distribution support
- Recommendation: Simple pattern for basic roles, first_found for complex multi-OS roles
- vs security simple pattern:
Key Validation Findings
What Docker Role Confirms:
- ✅ Standard directory structure is universal
- ✅ tasks/main.yml as router is standard
- ✅ Role-prefixed variable naming is universal
- ✅ defaults/ vs vars/ separation is universal
- ✅ Feature grouping in variable names is universal
- ✅ Descriptive task naming is universal
What Docker Role Evolves:
- 🔄 Advanced include_vars with first_found lookup (better than simple include_vars)
- 🔄 More OS-specific task files (RedHat, Suse, Debian vs just RedHat/Debian)
Pattern Confidence After Docker Validation:
- Directory structure: UNIVERSAL (3/3 roles follow)
- Task organization: UNIVERSAL (3/3 use main.yml as router)
- Variable naming: UNIVERSAL (3/3 use role prefix)
- defaults/ vs vars/: UNIVERSAL (3/3 follow pattern)
- OS-specific vars loading: EVOLVED (first_found is better than simple include)
Validation: geerlingguy.postgresql
Analysis Date: 2025-10-23 Repository: https://github.com/geerlingguy/ansible-role-postgresql
Directory Organization
- Pattern: Standard Ansible role structure - ✅ Confirmed
- PostgreSQL has: defaults/, tasks/, handlers/, meta/, molecule/, .github/, vars/, templates/
- Uses templates/ for pg_hba.conf and postgresql.conf (complex config files)
- 4/4 roles confirm standard structure
Task Organization
-
Pattern: tasks/main.yml as router - ✅ Confirmed
- main.yml includes: variables.yml, setup-{Archlinux,Debian,RedHat}.yml, initialize.yml, configure.yml
- imports (not includes) users.yml, databases.yml, users_props.yml for execution order
- Insight: Uses
include_tasksfor conditional includes,import_taskswhen order matters - 4/4 roles use main.yml as router pattern
-
Pattern: Feature-based task files - ✅ Confirmed
- Tasks split by: OS (setup-*.yml), lifecycle (initialize.yml, configure.yml), entity (users.yml, databases.yml)
- More task files than simpler roles (8+ files vs 2-3)
- Pattern scales: Complex roles have more task files, organized by feature and OS
Variable Naming
-
Pattern: Role-prefixed variables - ✅ Confirmed
- All variables prefixed with
postgresql_: postgresql_databases, postgresql_users, postgresql_hba_entries - 4/4 roles confirm this is universal
- All variables prefixed with
-
Pattern: Feature grouping - ✅ Confirmed
- postgresql_global_config_* for server config
- postgresql_hba_* for authentication config
- postgresql_*_enabled for feature flags
- Demonstrates: Feature grouping works at scale (20+ variables)
defaults/ vs vars/ Usage
- Pattern: defaults/ for user config, vars/ for OS-specific - ✅ Confirmed
- defaults/main.yml: Extensive user configuration (100+ lines with inline docs)
- vars/{Archlinux,Debian,RedHat}.yml: OS-specific package names, paths, versions
- 4/4 roles follow this pattern exactly
Task Naming Convention
- Pattern: Descriptive action verb + object - ✅ Confirmed
- "Ensure PostgreSQL Python libraries are installed."
- "Ensure PostgreSQL is started and enabled on boot."
- "Set PostgreSQL environment variables."
- 4/4 roles use identical naming pattern
Advanced Pattern: include_tasks vs import_tasks
-
Pattern Evolution: PostgreSQL demonstrates when to use each:
# Conditional loading - use include_tasks - include_tasks: setup-Archlinux.yml when: ansible_os_family == 'Archlinux' # Ordered execution - use import_tasks - import_tasks: users.yml - import_tasks: databases.yml - import_tasks: users_props.yml- New insight:
include_tasks= dynamic/conditional,import_tasks= static/ordered - Recommendation: Use import when order matters, include when conditional
- New insight:
Complex Variable Documentation Pattern
-
Pattern: Inline documentation in defaults/main.yml - ✅ EXCELLENT EXAMPLE
- PostgreSQL defaults/ has extensive inline examples for complex structures:
postgresql_databases: [] # - name: exampledb # required; the rest are optional # lc_collate: # defaults to 'en_US.UTF-8' # lc_ctype: # defaults to 'en_US.UTF-8' # encoding: # defaults to 'UTF-8'- Validates: Complex dict structures benefit from commented examples in defaults
- Best practice: Show all available keys, even optional ones
Key Validation Findings
What PostgreSQL Role Confirms:
- ✅ Standard directory structure is universal (4/4 roles)
- ✅ tasks/main.yml as router is universal (4/4 roles)
- ✅ Role-prefixed variable naming is universal (4/4 roles)
- ✅ defaults/ vs vars/ separation is universal (4/4 roles)
- ✅ Feature grouping in variable names scales well
- ✅ Descriptive task naming is universal (4/4 roles)
What PostgreSQL Role Demonstrates:
- 🔄 Complex roles have more task files (8+ vs 2-3 for simple roles)
- 🔄 include_tasks vs import_tasks have distinct use cases
- 🔄 Inline documentation in defaults/ is critical for complex variables
- 🔄 templates/ directory becomes important for complex config files
Pattern Confidence After PostgreSQL Validation (4/4 roles):
- Directory structure: UNIVERSAL (4/4 roles identical)
- Task organization: UNIVERSAL (4/4 use main.yml as router)
- Variable naming: UNIVERSAL (4/4 use role prefix)
- defaults/ vs vars/: UNIVERSAL (4/4 follow pattern)
- Task file count: CONTEXTUAL (scales with complexity: 2-3 for simple, 8+ for complex)
- include vs import: CLARIFIED (conditional vs ordered)
Validation: geerlingguy.nginx
Analysis Date: 2025-10-23 Repository: https://github.com/geerlingguy/ansible-role-nginx
Directory Organization
- Pattern: Standard Ansible role structure - ✅ Confirmed
- nginx has: defaults/, tasks/, handlers/, meta/, molecule/, .github/, vars/, templates/
- Heavily uses templates/ directory with 3 template files
- 5/5 roles confirm standard structure
Template Organization - ✨ NEW INSIGHT
-
Pattern: templates/ directory for complex configurations - ✅ CONFIRMED & EXPANDED
- nginx uses templates/ extensively for configuration management:
nginx.conf.j2- Main nginx configuration (extensive Jinja2 logic)vhost.j2- Virtual host configuration templatenginx.repo.j2- Repository configuration template
- Key insight: Templates heavily use Jinja2 blocks for extensibility
- nginx uses templates/ extensively for configuration management:
-
Advanced Template Pattern: Jinja2 Block Inheritance
-
nginx.conf.j2 uses
{% block %}for template extensibility:{% block worker %} worker_processes {{ nginx_worker_processes }}; {% endblock %} {% block http_begin %}{% endblock %} {% block http_basic %}...{% endblock %} {% block http_gzip %}...{% endblock %} {% block http_upstream %}...{% endblock %} {% block http_includes %}...{% endblock %} {% block http_end %}{% endblock %} -
Allows users to override specific template sections without replacing entire template
-
README documents how to extend templates using Jinja2 inheritance
-
-
Template Customization Pattern:
- Variables for template selection:
nginx_conf_template,nginx_vhost_template - Per-vhost template override:
item.templatein vhost definition - Users can provide custom templates while falling back to role defaults
- Variables for template selection:
-
When to Use templates/ vs Other Approaches:
- Use templates/ when:
- Configuration files have complex structure (nginx.conf, vhost configs)
- Need conditional content generation
- Need Jinja2 block inheritance for user extensibility
- Configuration requires looping over variables (upstreams, vhosts)
- Use lineinfile/copy when:
- Simple single-line configuration changes (SSH config)
- Static files that don't need variable substitution
- Use templates/ when:
Task Organization
-
Pattern: tasks/main.yml as router - ✅ Confirmed
- main.yml includes: OS-specific setup files, vhosts.yml, main configuration
- Same conditional include pattern as other roles
- 5/5 roles use main.yml as router pattern
-
Pattern: OS-specific task files - ✅ Confirmed
- setup-RedHat.yml, setup-Ubuntu.yml, setup-Debian.yml, setup-FreeBSD.yml, etc.
- nginx supports more OS families than previous roles (FreeBSD, OpenBSD, Suse, Archlinux)
- Pattern scales to any number of supported platforms
Variable Naming
-
Pattern: Role-prefixed variables - ✅ Confirmed
- All variables prefixed with
nginx_: nginx_worker_processes, nginx_vhosts, nginx_upstreams - 5/5 roles confirm this is universal
- All variables prefixed with
-
Pattern: Template path variables - ✅ NEW SUB-PATTERN
- nginx exposes template paths as variables:
nginx_conf_template,nginx_vhost_template - Allows users to override templates without modifying role
- Recommendation: Always make template paths configurable in roles that use templates
- nginx exposes template paths as variables:
defaults/ vs vars/ Usage
- Pattern: defaults/ for user config, vars/ for OS-specific - ✅ Confirmed
- defaults/main.yml: Extensive user configuration (vhosts, upstreams, worker config)
- vars/{Debian,RedHat,FreeBSD,etc.}.yml: OS-specific package names, paths, service names
- 5/5 roles follow this pattern exactly
Complex Variable Documentation
- Pattern: Inline documentation with examples - ✅ EXCELLENT EXAMPLE
-
nginx_vhosts documented with full example showing all options:
nginx_vhosts: [] # Example vhost below, showing all available options: # - listen: "80" # server_name: "example.com" # root: "/var/www/example.com" # index: "index.html index.htm" # filename: "example.com.conf" # ... -
nginx_upstreams similar pattern with all load balancing options shown
-
Validates: Complex list-of-dict variables need comprehensive inline examples
-
Key Validation Findings
What nginx Role Confirms:
- ✅ Standard directory structure is universal (5/5 roles)
- ✅ tasks/main.yml as router is universal (5/5 roles)
- ✅ Role-prefixed variable naming is universal (5/5 roles)
- ✅ defaults/ vs vars/ separation is universal (5/5 roles)
- ✅ Inline variable documentation is universal (5/5 roles)
- ✅ OS-specific task organization is universal (5/5 roles)
What nginx Role Demonstrates (✨ NEW INSIGHTS):
- ✨ Template organization patterns: Jinja2 blocks for extensibility
- ✨ Template customization: Variables for template paths, per-item overrides
- ✨ README template documentation: Explaining template inheritance
- 🔄 Platform support scales: nginx supports 6+ OS families
- 🔄 Complex variable documentation with full working examples
Pattern Confidence After nginx Validation (5/5 roles):
- Directory structure: UNIVERSAL (5/5 roles identical)
- Task organization: UNIVERSAL (5/5 use main.yml as router)
- Variable naming: UNIVERSAL (5/5 use role prefix)
- defaults/ vs vars/: UNIVERSAL (5/5 follow pattern)
- Template organization: VALIDATED (nginx shows advanced patterns)
- Template extensibility: BEST PRACTICE (Jinja2 blocks for inheritance)
- Template path variables: RECOMMENDED (allow user customization)
Validation: geerlingguy.pip
Analysis Date: 2025-10-23 Repository: https://github.com/geerlingguy/ansible-role-pip
Directory Structure
- Pattern: Minimal role structure - ✅ Confirmed
- pip has only essential directories: tasks/, defaults/, meta/, molecule/
- No templates/, handlers/, vars/, or files/ (not needed for this simple role)
- Key finding: Directory structure scales down appropriately for simple roles
Task Organization
- Pattern: Single file tasks - ✅ Confirmed
- pip role has only tasks/main.yml with 3 tasks total
- No task splitting needed for minimal roles
- Each task still properly named and documented
- Validates: tasks/main.yml sufficient for simple roles
Variable Management
- Pattern: Minimal defaults - ✅ Confirmed
- defaults/main.yml has only 3 variables: pip_package, pip_executable, pip_install_packages
- All variables properly prefixed with role name (pip_)
- Simple list structure for pip_install_packages with documented dict options
- 6/6 roles use role-prefixed variable naming
Key Validation Findings
What pip Role Confirms:
- ✅ Directory structure scales appropriately (only include what's needed)
- ✅ Single-file tasks acceptable for simple roles (3 tasks in main.yml)
- ✅ Role-prefixed variable naming still universal (6/6 roles)
- ✅ defaults/ still used even for minimal variables
- ✅ No vars/ directory when all variables are user-configurable
Pattern Confidence After pip Validation (6/6 roles):
- Directory structure: UNIVERSAL (6/6 roles follow standard, scale appropriately)
- Variable naming: UNIVERSAL (6/6 use role prefix)
- defaults/ for user config: UNIVERSAL (6/6 roles)
- Single-file tasks for simple roles: VALIDATED (pip proves it's acceptable)
Validation: geerlingguy.git
Analysis Date: 2025-10-23 Repository: https://github.com/geerlingguy/ansible-role-git
Directory Structure
- Pattern: Utility role structure - ✅ Confirmed
- git has: tasks/, defaults/, vars/, meta/, molecule/
- Added vars/ for OS-specific package names
- Uses tasks/ for main + import pattern
- Key finding: vars/ appears when OS-specific data needed
Task Organization
-
Pattern: Task file imports - ✅ Confirmed
- git role uses tasks/main.yml as router (4 tasks)
- tasks/install-from-source.yml imported conditionally
- Conditional imports based on git_install_from_source flag
- Validates: import_tasks pattern for optional functionality
-
Pattern: OS-specific task blocks - ✅ Confirmed
- Separate tasks for RedHat vs Debian families
- Conditional execution via ansible_os_family
- Package installation tasks specific to each OS family
- 7/7 roles handle OS differences with when conditions
Variable Management
-
Pattern: defaults/ vs vars/ split - ✅ Confirmed
- defaults/main.yml: User-configurable options (workspace, version, install method)
- vars/: OS-specific package lists (git_packages for Debian vs RedHat)
- All variables still prefixed with role name (git_)
- 7/7 roles use role-prefixed variable naming
-
Pattern: Boolean flags for features - ✅ Confirmed
- git_install_from_source boolean controls installation method
- git_install_force_update boolean controls version updates
- Clear feature flags with sensible defaults
- Validates: Boolean flags for optional features pattern
Key Validation Findings
What git Role Confirms:
- ✅ vars/ directory for OS-specific non-configurable data (7/7 roles)
- ✅ import_tasks for optional/complex functionality (7/7 roles)
- ✅ OS-family conditional tasks universal (7/7 roles)
- ✅ Boolean feature flags best practice (7/7 roles)
- ✅ Task file splitting based on functionality not size
Pattern Confidence After git Validation (7/7 roles):
- Directory structure: UNIVERSAL (7/7 roles follow standard)
- Task organization: UNIVERSAL (7/7 use main.yml as router)
- Variable naming: UNIVERSAL (7/7 use role prefix)
- defaults/ vs vars/: UNIVERSAL (7/7 separate user config from OS data)
- import_tasks pattern: UNIVERSAL (7/7 use for complex/optional features)
- OS-specific conditionals: UNIVERSAL (7/7 handle multi-platform)
Summary
Universal Patterns Identified:
- Standard Ansible role directory structure
- tasks/main.yml as router with include_tasks
- Feature-based task file organization
- Role-prefixed variable names (rolename_feature_attribute)
- defaults/ for user config, vars/ for internal/OS-specific values
- OS-specific variable files loaded dynamically
- Simple, single-purpose handlers
- Descriptive task names starting with action verbs
- Configuration file validation before applying
Key Takeaways:
- Directory structure is standardized and well-understood
- Task organization improves maintainability
- Naming conventions prevent variable conflicts
- Proper defaults/ vs vars/ usage prevents confusion
- Handlers should be simple and focused
- Task files should be feature-based, not too granular
- Complex roles naturally have more task files (don't fight it)
- Inline documentation in defaults/ is critical for complex variables
Next Steps:
All three Virgo-Core roles follow good structure patterns. Primary gaps are testing infrastructure (covered in testing-comprehensive.md) and CI/CD automation.