Files
gh-basher83-lunar-claude-pl…/skills/ansible-best-practices/patterns/role-structure-standards.md
2025-11-29 18:00:24 +08:00

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:

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:

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:

  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:

---
- 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:

---
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: 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:

[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:

---
# 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:

  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:

[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:

  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:

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:

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: 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
  • 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

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: 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_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:

    # 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:
    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: 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 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:

      {% 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:

      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: 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:

  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: 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:

  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.