# 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:** - - - - - - - ## Pattern Confidence Levels (Historical) Analyzed 2 geerlingguy roles: security, github-users **Universal Patterns (Both roles use identical approach):** 1. ✅ **Standard directory structure** - Both follow defaults/, tasks/, meta/, molecule/, .github/ structure 2. ✅ **Role-prefixed variable names** - security_*, github_users_* (prevents conflicts) 3. ✅ **Descriptive task names** - Action verb + object pattern ("Ensure...", "Add...", "Update...") 4. ✅ **defaults/ for user configuration** - All user-overridable values in defaults/main.yml 5. ✅ **Snake_case naming** - Consistent variable naming convention 6. ✅ **Inline validation** - validate parameter for critical config files 7. ✅ **File permissions** - Explicit mode settings on all files 8. ✅ **Quality control files** - .ansible-lint, .yamllint, .gitignore present **Contextual Patterns (Varies by role complexity):** 1. ⚠️ **Task file organization** - security splits tasks (ssh.yml, fail2ban.yml), github-users keeps single main.yml (role is simpler) 2. ⚠️ **vars/ directory** - security has OS-specific vars files, github-users doesn't need them 3. ⚠️ **templates/ usage** - security uses templates for fail2ban config, github-users has no templates 4. ⚠️ **handlers/** - security has 3 handlers (services to restart), github-users has none (no services managed) 5. ⚠️ **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:** ```text 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:** ```yaml --- - 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:** 1. **include_vars at top** - Load OS-specific variables first 2. **Logical grouping** - Each include_tasks represents a feature 3. **Conditional includes** - Only run tasks when needed 4. **Comments as section headers** - Improve readability 5. **Boolean filter** - `| bool` ensures proper boolean evaluation 6. **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:** ```yaml --- - 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:** 1. **Validation parameters:** - `validate: 'sshd -T -f %s'` - Test SSH config before applying - `validate: 'visudo -cf %s'` - Validate sudoers syntax - Prevents breaking critical system files 2. **Idempotent configuration:** - lineinfile with regexp - Updates or adds lines - state: present - Ensures line exists - Anchored regexps (^) - Match start of line 3. **Conditional execution:** - `when: security_ssh_allowed_users | length > 0` - Skip if empty list - Prevents unnecessary file modifications 4. **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 5. **File permissions:** - `mode: 0644` for SSH config (readable by all) - `mode: 0440` for 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:** ```yaml --- 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:** ```text {role_name}_{feature}_{attribute} ``` Examples: - `security_ssh_port` - Role: security, Feature: ssh, Attribute: port - `security_fail2ban_enabled` - Role: security, Feature: fail2ban, Attribute: enabled - `security_autoupdate_reboot_time` - Role: security, Feature: autoupdate, Attribute: reboot_time **Key Elements:** 1. **Role prefix** - All variables start with "security_" 2. **Feature grouping** - Related variables have common prefix (security_ssh_, security_fail2ban_) 3. **Descriptive names** - Full words, not abbreviations 4. **Underscore separation** - snake_case, not camelCase 5. **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:** ```text [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:** 1. **Start with action verb** - Ensure, Update, Add, Install, Configure, Remove 2. **Be specific** - "SSH daemon" not just "daemon" 3. **Add context** - "to be more secure" explains why 4. **Use present tense** - "Ensure" not "Ensuring" 5. **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 configuration - `vars/Debian.yml` - Debian-specific internal values - `vars/RedHat.yml` - RedHat-specific internal values **defaults/main.yml Example:** ```yaml --- # 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:** ```yaml --- # Internal OS-specific values (high precedence) security_ssh_config_path: /etc/ssh/sshd_config security_sshd_name: ssh ``` **vars/RedHat.yml Example (inferred structure):** ```yaml --- # 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:** ```yaml - 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:** ```yaml --- - 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:** 1. **Descriptive names** - Action + service (restart ssh, reload fail2ban) 2. **Single responsibility** - Each handler does one thing 3. **Configurable state** - Uses variable for restart/reload state 4. **Lowercase names** - "reload systemd" not "Reload Systemd" 5. **Service vs systemd_service** - Use appropriate module **Handler Naming Pattern:** ```text [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:** ```text 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:** 1. **Nice-to-have:** Add vars/ files if supporting multiple OS families (30 min) 2. **Critical:** Add molecule/ directory (covered in testing-comprehensive.md) ### proxmox_access Role **Structure Analysis:** ```text 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:** 1. **Critical:** Add molecule/ directory (covered in testing-comprehensive.md) 2. **Nice-to-have:** Add vars/ files for Proxmox-specific paths (1 hour) ### proxmox_network Role **Structure Analysis:** ```text 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:** 1. **Critical:** Add molecule/ directory with network verification (covered in testing-comprehensive.md) ## Validation: geerlingguy.docker **Analysis Date:** 2025-10-23 **Repository:** ### 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 - **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: ```yaml - 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 ### Key Validation Findings **What Docker Role Confirms:** 1. ✅ Standard directory structure is universal 2. ✅ tasks/main.yml as router is standard 3. ✅ Role-prefixed variable naming is universal 4. ✅ defaults/ vs vars/ separation is universal 5. ✅ Feature grouping in variable names is universal 6. ✅ Descriptive task naming is universal **What Docker Role Evolves:** 1. 🔄 Advanced include_vars with first_found lookup (better than simple include_vars) 2. 🔄 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:** ### 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_tasks` for conditional includes, `import_tasks` when 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** - **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: ```yaml # 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 ### Complex Variable Documentation Pattern - **Pattern: Inline documentation in defaults/main.yml** - ✅ **EXCELLENT EXAMPLE** - PostgreSQL defaults/ has extensive inline examples for complex structures: ```yaml 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:** 1. ✅ Standard directory structure is universal (4/4 roles) 2. ✅ tasks/main.yml as router is universal (4/4 roles) 3. ✅ Role-prefixed variable naming is universal (4/4 roles) 4. ✅ defaults/ vs vars/ separation is universal (4/4 roles) 5. ✅ Feature grouping in variable names scales well 6. ✅ Descriptive task naming is universal (4/4 roles) **What PostgreSQL Role Demonstrates:** 1. 🔄 Complex roles have more task files (8+ vs 2-3 for simple roles) 2. 🔄 include_tasks vs import_tasks have distinct use cases 3. 🔄 Inline documentation in defaults/ is critical for complex variables 4. 🔄 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:** ### 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 template - `nginx.repo.j2` - Repository configuration template - **Key insight:** Templates heavily use Jinja2 blocks for extensibility - **Advanced Template Pattern: Jinja2 Block Inheritance** - nginx.conf.j2 uses `{% block %}` for template extensibility: ```jinja2 {% 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.template` in vhost definition - Users can provide custom templates while falling back to role defaults - **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 ### 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** - **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 ### 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: ```yaml 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:** 1. ✅ Standard directory structure is universal (5/5 roles) 2. ✅ tasks/main.yml as router is universal (5/5 roles) 3. ✅ Role-prefixed variable naming is universal (5/5 roles) 4. ✅ defaults/ vs vars/ separation is universal (5/5 roles) 5. ✅ Inline variable documentation is universal (5/5 roles) 6. ✅ OS-specific task organization is universal (5/5 roles) **What nginx Role Demonstrates (✨ NEW INSIGHTS):** 1. ✨ **Template organization patterns:** Jinja2 blocks for extensibility 2. ✨ **Template customization:** Variables for template paths, per-item overrides 3. ✨ **README template documentation:** Explaining template inheritance 4. 🔄 Platform support scales: nginx supports 6+ OS families 5. 🔄 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:** ### 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:** 1. ✅ Directory structure scales appropriately (only include what's needed) 2. ✅ Single-file tasks acceptable for simple roles (3 tasks in main.yml) 3. ✅ Role-prefixed variable naming still universal (6/6 roles) 4. ✅ defaults/ still used even for minimal variables 5. ✅ 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:** ### 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:** 1. ✅ vars/ directory for OS-specific non-configurable data (7/7 roles) 2. ✅ import_tasks for optional/complex functionality (7/7 roles) 3. ✅ OS-family conditional tasks universal (7/7 roles) 4. ✅ Boolean feature flags best practice (7/7 roles) 5. ✅ 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:** 1. Standard Ansible role directory structure 2. tasks/main.yml as router with include_tasks 3. Feature-based task file organization 4. Role-prefixed variable names (rolename_feature_attribute) 5. defaults/ for user config, vars/ for internal/OS-specific values 6. OS-specific variable files loaded dynamically 7. Simple, single-purpose handlers 8. Descriptive task names starting with action verbs 9. 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.