27 KiB
Variable Management Patterns
Summary: Pattern Confidence
Analyzed 7 geerlingguy roles: security, users, docker, postgresql, nginx, pip, git
Universal Patterns (All 7 roles):
- Role-prefixed variable names preventing conflicts (7/7 roles use rolename_feature_attribute)
- Snake_case naming convention throughout (7/7 roles)
- Feature grouping with shared prefixes (7/7 roles: security_ssh_, postgresql_global_config_)
- defaults/ for user configuration at low precedence (7/7 roles)
- vars/ for OS-specific values at high precedence (7/7 roles when needed)
- Empty list defaults [] for safety (7/7 roles)
- Unquoted Ansible booleans (true/false) for role logic (7/7 roles)
- Quoted string booleans ("yes"/"no") for config files (7/7 roles with config management)
- Descriptive full names without abbreviations (7/7 roles)
- Inline variable documentation in defaults/main.yml (7/7 roles)
Contextual Patterns (Varies by requirements):
- vars/ directory presence: only when OS-specific non-configurable data needed (4/7 roles have it)
- Variable count scales with role complexity: minimal roles have 3-5 variables, complex roles have 20+
- Complex list-of-dict structures: database/service roles (postgresql, nginx) vs simple list variables (pip, git)
- Conditional variable groups: feature-toggle variables activate groups of related configuration (git_install_from_source)
Evolving Patterns (Newer roles improved):
- PostgreSQL demonstrates best practice for complex dict structures: show ALL possible keys with inline comments, mark required vs optional vs defaults
- Flexible dict patterns: item.name | default(item) supports both simple strings and complex dicts (github-users role)
- Advanced variable loading: first_found lookup (docker) vs simple include_vars (security) for better fallback support
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):
- ✅ Role-prefixed variable names - All variables start with role name (security_, github_users_)
- ✅ Snake_case naming - Consistent use of underscores, never camelCase
- ✅ Feature grouping - Related variables share prefix (security_ssh_, github_users_authorized_keys_)
- ✅ Empty lists as defaults - Default to
[]for list variables, not undefined - ✅ Boolean defaults - Use lowercase
true/falsefor Ansible booleans - ✅ String booleans for configs - Quote yes/no when they're config values
(e.g.,
"no"for SSH config) - ✅ Descriptive full names - No abbreviations (security_ssh_port, not security_ssh_prt)
- ✅ defaults/ for user config - All user-overridable values in defaults/main.yml
- ✅ Inline variable documentation - Comments in defaults/ file with examples
Contextual Patterns (Varies by role requirements):
- ⚠️ vars/ for OS-specific values - security uses vars/{Debian,RedHat}.yml, github-users doesn't need OS-specific vars
- ⚠️ Complex variable structures - security has simple scalars/lists, github-users uses list of strings OR dicts pattern
- ⚠️ Variable count - security has ~20 variables (complex role), github-users has 4 (simple role)
- ⚠️ Default URL patterns - github-users has configurable URL (github_url), security doesn't need this pattern
Key Finding: Variable management is highly consistent. The role name prefix pattern prevents ALL variable conflicts in complex playbooks.
Overview
This document captures variable management patterns from production-grade Ansible roles, demonstrating how to organize, name, and document variables for clarity and maintainability.
Pattern: defaults/ vs vars/ Usage
Description
Use defaults/ for user-configurable values (low precedence, easily overridden) and vars/ for internal/OS-specific values (high precedence, should not be overridden).
File Paths
defaults/main.yml- User-facing configurationvars/Debian.yml- Debian-specific internal values (optional)vars/RedHat.yml- RedHat-specific internal values (optional)
defaults/main.yml Pattern
geerlingguy.security example:
---
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_fail2ban_enabled: true
security_fail2ban_custom_configuration_template: "jail.local.j2"
geerlingguy.github-users example:
---
github_users: []
# You can specify an object with 'name' (required) and 'groups' (optional):
# - name: geerlingguy
# groups: www-data,sudo
# Or you can specify a GitHub username directly:
# - geerlingguy
github_users_absent: []
# You can specify an object with 'name' (required):
# - name: geerlingguy
# Or you can specify a GitHub username directly:
# - geerlingguy
github_users_authorized_keys_exclusive: true
github_url: https://github.com
Key Elements:
- Role prefix - Every variable starts with role name
- Feature grouping - ssh variables together, autoupdate together, etc.
- Inline comments - Examples shown as comments
- Default values - Sensible defaults that work out-of-box
- Empty lists - Default to [] not undefined
- Quoted strings - "no", "yes" for SSH config values (prevents YAML boolean interpretation)
vars/ OS-Specific Pattern
geerlingguy.security vars/Debian.yml:
---
security_ssh_config_path: /etc/ssh/sshd_config
security_sshd_name: ssh
geerlingguy.security vars/RedHat.yml:
---
security_ssh_config_path: /etc/ssh/sshd_config
security_sshd_name: sshd
Loading Pattern in tasks/main.yml:
- name: Include OS-specific variables.
include_vars: "{{ ansible_os_family }}.yml"
Decision Matrix
| Variable Type | Location | Precedence | Use Case | Override |
|---|---|---|---|---|
| User configuration | defaults/ | Low | Settings users customize | Easily overridden in playbook |
| OS-specific paths | vars/ | High | File paths, service names | Should not be overridden |
| Feature toggles | defaults/ | Low | Enable/disable features | User choice |
| Internal constants | vars/ | High | Values role needs to work | Role implementation detail |
When to Use
defaults/ - Use for:
- Port numbers users might change
- Feature enable/disable flags
- List of items users configure
- Behavioral options
- Template paths users might override
vars/ - Use for:
- Service names that differ by OS (ssh vs sshd)
- Configuration file paths
- Package names that vary by OS
- Internal role constants
- Values that should rarely/never be overridden
Anti-pattern
- ❌ Don't put user-facing config in vars/ (can't be easily overridden)
- ❌ Don't put OS-specific paths in defaults/ (users shouldn't need to change)
- ❌ Avoid duplicating values between defaults/ and vars/
- ❌ Don't use vars/ for what should be defaults/ (breaks override mechanism)
Pattern: Variable Naming Conventions
Description
Use a consistent, hierarchical naming pattern: {role_name}_{feature}_{attribute}
Naming Pattern Structure
{role_name}_{feature}_{attribute}_{sub_attribute}
Examples from security role
security_ssh_port- Role: security, Feature: ssh, Attribute: portsecurity_ssh_password_authentication- Role: security, Feature: ssh, Attribute: password_authenticationsecurity_fail2ban_enabled- Role: security, Feature: fail2ban, Attribute: enabledsecurity_autoupdate_reboot_time- Role: security, Feature: autoupdate, Attribute: reboot_timesecurity_ssh_restart_handler_state- Role: security, Feature: ssh, Attribute: restart_handler_state
Examples from github-users role
github_users- Role: github-users (shortened to github), Feature: users (implicit)github_users_absent- Role: github, Feature: users, Attribute: absentgithub_users_authorized_keys_exclusive- Role: github, Feature: users, Attribute: authorized_keys_exclusivegithub_url- Role: github, Feature: url (API endpoint)
Naming Guidelines
- Always use role prefix - Prevents variable name collisions
- Use full words - No abbreviations (password not pwd, configuration not cfg)
- Snake_case only - Underscores, never camelCase or kebab-case
- Feature grouping - Related vars share feature prefix for logical grouping
- Hierarchical structure - General to specific (ssh → password → authentication)
- Boolean naming - Use
_enabled,_disabled, or descriptive names (not just_flag) - Descriptive, not cryptic - Variable name should explain purpose
When to Use
- All role variables without exception
- Internal variables (loop vars, registered results) can skip prefix if scope is limited
- Consistently apply pattern across all variables in the role
Anti-pattern
- ❌ Generic names:
port,enabled,users(conflicts in complex playbooks) - ❌ Abbreviations:
cfg,pwd,usr(harder to read) - ❌ camelCase:
githubUsersAbsent(not Ansible convention) - ❌ Inconsistent prefixes: Some vars with prefix, some without
- ❌ Overly long names:
security_ssh_configuration_password_authentication_setting(be descriptive, not verbose)
Pattern: Boolean vs String Values
Description
Distinguish between Ansible booleans and configuration file string values. Quote strings that look like booleans.
Ansible Booleans (unquoted)
Use for feature flags, task conditions, role logic:
security_fail2ban_enabled: true
security_autoupdate_enabled: true
github_users_authorized_keys_exclusive: true
Valid Ansible boolean values:
true/false(preferred)yes/noon/off1/0
Configuration Strings (quoted)
Use for values written to config files:
security_ssh_password_authentication: "no"
security_ssh_permit_root_login: "no"
security_ssh_usedns: "no"
security_autoupdate_reboot: "false"
Rationale:
When Ansible sees no or false without quotes, it converts to boolean. When
this boolean is then written to a config file (via lineinfile or template), it
becomes False or false, which might not match the config file's expected
format (e.g., SSH expects no/yes).
Pattern from security role
# Ansible boolean (role logic)
# Controls whether to install fail2ban
security_fail2ban_enabled: true
# Config string (written to /etc/ssh/sshd_config)
# Literal string "no" for SSH
security_ssh_password_authentication: "no"
When to Use
Unquoted booleans:
- Feature enable/disable flags (
role_feature_enabled) - Task conditionals (
when:clauses) - Handler behavior
- Internal role logic
Quoted strings:
- Values written to config files
- Values that must preserve exact format
- Values that look like booleans but aren't
Anti-pattern
- ❌ Unquoted yes/no for config values (becomes
True/Falsein file) - ❌ Quoted booleans for feature flags (unnecessarily complex)
- ❌ Inconsistent quoting across similar variables
Pattern: List and Dictionary Structures
Description
Use flexible data structures that support both simple and complex use cases.
Simple List Pattern
github-users simple list:
github_users:
- geerlingguy
- fabpot
- johndoe
security simple list:
security_sudoers_passwordless:
- deployuser
- admin
security_ssh_allowed_users:
- alice
- bob
List of Dictionaries Pattern
github-users complex pattern:
github_users:
- name: geerlingguy
groups: www-data,sudo
- name: fabpot
groups: developers
- johndoe # Still supports simple string
Task handling both patterns:
- name: Ensure GitHub user accounts are present.
user:
# Handles both dict and string
name: "{{ item.name | default(item) }}"
# Optional attribute
groups: "{{ item.groups | default(omit) }}"
Key technique: {{ item.name | default(item) }}
- If item is a dict with 'name' key → use item.name
- If item is a string → default to item itself
- Supports both simple and complex usage
Dictionary Pattern
security dictionary example (inferred, not in role):
security_ssh_config:
port: 22
password_auth: "no"
permit_root: "no"
This pattern is less common in geerlingguy roles (flat variables preferred for simplicity).
When to Use
Simple lists:
- When each item needs only one value
- User management (simple usernames)
- Package lists
- Simple configuration items
List of dicts:
- When items have multiple optional attributes
- Users with groups, shells, home directories
- Complex configuration items
- When backwards compatibility with simple list is needed
Flat variables:
- When configuration is not deeply nested
- When clarity is more important than brevity
- When users need to override individual values
Anti-pattern
- ❌ Deep nesting (3+ levels) - Hard to override, hard to document
- ❌ Inconsistent structure - Some items as strings, others as dicts without handling
- ❌ Required attributes in complex structures without defaults
- ❌ Over-engineering simple use cases
Pattern: Default Value Strategies
Description
Choose appropriate default values that balance security, usability, and least surprise.
Empty List Defaults
github_users: []
github_users_absent: []
security_ssh_allowed_users: []
security_sudoers_passwordless: []
Rationale:
- Safe default (no users created/removed)
- Allows conditional logic:
when: github_users | length > 0 - Users must explicitly configure
- No surprising side effects
Secure Defaults
security_ssh_password_authentication: "no"
security_ssh_permit_root_login: "no"
github_users_authorized_keys_exclusive: true
Rationale:
- Security-first approach
- Users can relax security if needed
- Prevents accidental insecure configurations
Service State Defaults
security_sshd_state: started
security_ssh_restart_handler_state: restarted
Rationale:
- Explicit state management
- Allows users to override (e.g., for testing)
- Documents expected state
Feature Toggles
security_fail2ban_enabled: true
security_autoupdate_enabled: true
Rationale:
- Enable useful features by default
- Easy to disable if not wanted
- Clear intent
Sensible Configuration Defaults
security_ssh_port: 22
github_url: https://github.com
Rationale:
- Standard/expected values
- Users only change when needed
- Reduces configuration burden
When to Use
- Empty lists - When no default action is safe
- Secure defaults - For security-sensitive settings
- Enabled by default - For beneficial features with no downsides
- Standard values - For well-known defaults (port 22, standard URLs)
Anti-pattern
- ❌ Undefined defaults - Use
[]or explicitnull, not absent - ❌ Insecure defaults - Don't default to
password_authentication: "yes" - ❌ Surprising defaults - Don't create users/change configs by default
- ❌ Missing defaults - Every variable in defaults/main.yml should have a value
Comparison to Virgo-Core Roles
system_user Role
Variable Analysis:
# From system_user/defaults/main.yml
system_user_name: ""
system_user_groups: []
system_user_shell: /bin/bash
system_user_ssh_keys: []
system_user_sudo_access: "full"
system_user_sudo_commands: []
system_user_state: present
Matches geerlingguy patterns:
- ✅ Role prefix (system_user_*)
- ✅ Snake_case naming
- ✅ Empty list defaults
- ✅ Descriptive names
- ✅ All in defaults/main.yml
Gaps:
- ⚠️ No feature grouping (all variables are related to user management, so not needed)
- ⚠️ Could use string for sudo_access ("full", "commands", "none" vs full/limited)
- ✅ No vars/ directory needed (no OS-specific values)
Pattern Match: 95% - Excellent variable management
proxmox_access Role
Variable Analysis (sample):
# From proxmox_access/defaults/main.yml
proxmox_access_roles: []
proxmox_access_groups: []
proxmox_access_users: []
proxmox_access_tokens: []
proxmox_access_acls: []
proxmox_access_export_terraform_env: false
Matches:
- ✅ Role prefix (proxmox_access_*)
- ✅ Snake_case naming
- ✅ Empty list defaults
- ✅ Boolean flag for optional feature
- ✅ Feature grouping (access_roles, access_groups, access_users)
Gaps:
- ✅ No OS-specific vars needed (Proxmox-specific role)
- ✅ Good variable organization
Pattern Match: 100% - Perfect variable management
proxmox_network Role
Variable Analysis (sample):
# From proxmox_network/defaults/main.yml
proxmox_network_bridges: []
proxmox_network_vlans: []
proxmox_network_verify_connectivity: true
Matches:
- ✅ Role prefix (proxmox_network_*)
- ✅ Snake_case naming
- ✅ Empty list defaults
- ✅ Boolean flag
- ✅ Feature grouping
Gaps:
- ✅ Excellent pattern adherence
Pattern Match: 100% - Perfect variable management
Summary
Universal Variable Management Patterns:
- Role-prefixed variable names (prevents conflicts)
- Snake_case naming convention
- Feature grouping with shared prefixes
- defaults/ for user configuration (low precedence)
- vars/ for OS-specific values (high precedence)
- Empty lists as safe defaults (
[]) - Quoted string booleans for config files (
"no","yes") - Unquoted Ansible booleans for feature flags
- Flexible list/dict patterns with
item.name | default(item) - Descriptive full names, no abbreviations
Key Takeaways:
- Variable naming is not just convention - it prevents real bugs
- defaults/ vs vars/ distinction is critical for override behavior
- Quote config file values that look like booleans
- Support both simple and complex usage patterns when possible
- Default to secure, safe, empty values
- Feature grouping makes variable relationships clear
Validation: geerlingguy.postgresql
Analysis Date: 2025-10-23 Repository: https://github.com/geerlingguy/ansible-role-postgresql
Role-Prefixed Variable Names
- Pattern: Role prefix on ALL variables - ✅ Confirmed
- PostgreSQL: All variables start with
postgresql_ - Examples: postgresql_databases, postgresql_users, postgresql_hba_entries, postgresql_global_config_options
- 4/4 roles confirm this is universal
- PostgreSQL: All variables start with
Complex Data Structures
-
Pattern: List of dicts with comprehensive inline documentation - ✅ EXCELLENT EXAMPLE
- PostgreSQL has multiple complex list-of-dict variables:
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' # template: # defaults to 'template0' # login_host: # defaults to 'localhost' # login_password: # defaults to not set # login_user: # defaults to 'postgresql_user' # state: # defaults to 'present' postgresql_users: [] # - name: jdoe #required; the rest are optional # password: # defaults to not set # encrypted: # defaults to not set # role_attr_flags: # defaults to not set # db: # defaults to not set # state: # defaults to 'present'- Validates: Complex dict structures work beautifully with inline documentation
- Best practice: Show ALL possible keys, mark required vs optional, document defaults
defaults/ vs vars/ Usage
- Pattern: defaults/ for user config, vars/ for OS-specific -
✅ Confirmed
- defaults/main.yml: 100+ lines of user-configurable variables with extensive inline docs
- vars/{Archlinux,Debian,RedHat}.yml: OS-specific package names, paths, service names, versions
- 4/4 roles follow this pattern exactly
Empty List Defaults
- Pattern: Default to [] for list variables - ✅ Confirmed
- postgresql_databases: []
- postgresql_users: []
- postgresql_privs: []
- 4/4 roles use empty list defaults for safety
Feature Grouping
- Pattern: Feature-based variable prefixes - ✅ Confirmed
- postgresql_global_config_* for server configuration
- postgresql_hba_* for host-based authentication
- postgresql_unix_socket_* for socket configuration
- Demonstrates: Feature grouping scales to large variable sets (20+ variables)
Variable Documentation Pattern
- Pattern: Inline comments in defaults/main.yml -
✅ BEST PRACTICE EXAMPLE
- Every complex variable has commented examples
- Shows required vs optional keys
- Documents default values inline
- Provides usage context
- This is THE gold standard for complex variable documentation
Advanced Pattern: Flexible Dict Structures
- Pattern: Optional attributes with sensible defaults - ✅ NEW INSIGHT
- PostgreSQL variables accept dicts with only required keys
- Optional keys fall back to role defaults
- Task code:
item.login_host | default('localhost') - Pattern: Design dict structures so only required keys are necessary
Key Validation Findings
What PostgreSQL Role Confirms:
- ✅ Role-prefixed variable names are universal (4/4 roles)
- ✅ Snake_case naming is universal (4/4 roles)
- ✅ Feature grouping is universal (4/4 roles)
- ✅ Empty list defaults are universal (4/4 roles)
- ✅ defaults/ vs vars/ separation is universal (4/4 roles)
- ✅ Inline documentation is critical for complex variables
What PostgreSQL Role Demonstrates:
- 🔄 Complex list-of-dict variables can have 10+ optional attributes
- 🔄 Inline documentation prevents user confusion for complex structures
- 🔄 Show ALL possible keys, even optional ones
- 🔄 Mark required vs optional vs defaults in comments
- 🔄 Large variable sets (20+) benefit from logical grouping
Pattern Confidence After PostgreSQL Validation (4/4 roles):
- Role prefixes: UNIVERSAL (4/4 roles use them)
- Snake_case: UNIVERSAL (4/4 roles use it)
- Feature grouping: UNIVERSAL (4/4 roles group related variables)
- Empty list defaults: UNIVERSAL (4/4 roles use [])
- defaults/ vs vars/: UNIVERSAL (4/4 roles follow pattern)
- Complex dict structures: VALIDATED (postgresql shows best practices at scale)
- Inline documentation: CRITICAL (essential for complex variables)
Validation: geerlingguy.pip and geerlingguy.git
Analysis Date: 2025-10-23 Repositories:
Minimal Variables Pattern (pip role)
-
Pattern: Only essential variables - ✅ Confirmed
- pip has only 3 variables: pip_package, pip_executable, pip_install_packages
- All variables role-prefixed with pip_
- defaults/main.yml is under 10 lines
- Key finding: Minimal roles maintain same naming discipline
-
Pattern: String defaults with alternatives - ✅ Confirmed
- pip_package:
python3-pip(shows python-pip alternative in README) - pip_executable:
pip3(auto-detected, can override) - 6/6 roles document alternatives in README or comments
- pip_package:
-
Pattern: List variable with dict options - ✅ Confirmed
- pip_install_packages: defaults to
[] - Supports simple strings or dicts with keys: name, version, state, virtualenv, extra_args
- Validates: List-of-string-or-dict pattern is universal
- pip_install_packages: defaults to
Utility Role Variables Pattern (git role)
-
Pattern: Feature-toggle booleans - ✅ Confirmed
- git_install_from_source:
false(controls installation method) - git_install_force_update:
false(controls version management) - 7/7 roles use boolean flags for optional features
- git_install_from_source:
-
Pattern: Conditional variable groups - ✅ Confirmed
- Source install variables: workspace, version, path, force_update
- Only relevant when git_install_from_source: true
- Grouped together in defaults/main.yml
- Validates: Conditional features have grouped variables
-
Pattern: Platform-specific vars/ - ✅ Confirmed
- git role uses vars/Debian.yml and vars/RedHat.yml (implied from structure)
- vars/ contains non-configurable OS-specific data
- defaults/ contains all user-configurable options
- 7/7 roles use vars/ for OS-specific package lists
Key Validation Findings
What pip + git Roles Confirm:
- ✅ Role-prefix naming universal across all role sizes (7/7 roles)
- ✅ Snake_case universal (7/7 roles)
- ✅ Empty list defaults universal (7/7 roles use [])
- ✅ Boolean flags for features universal (7/7 roles)
- ✅ defaults/ vs vars/ separation universal (7/7 roles)
- ✅ Variable grouping applies even to simple roles (7/7 roles)
Pattern Confidence After Utility Role Validation (7/7 roles):
- Role prefixes: UNIVERSAL (7/7 roles use them)
- Snake_case: UNIVERSAL (7/7 roles use it)
- Feature grouping: UNIVERSAL (7/7 roles group related variables)
- Empty list defaults: UNIVERSAL (7/7 roles use [])
- defaults/ vs vars/: UNIVERSAL (7/7 roles follow pattern)
- Boolean feature toggles: UNIVERSAL (7/7 roles use them)
- Conditional variable groups: VALIDATED (git proves pattern for optional features)
- Minimal variables principle: CONFIRMED (pip shows simplicity is acceptable)
Virgo-Core Assessment:
All three Virgo-Core roles demonstrate excellent variable management practices. They follow geerlingguy patterns closely and have no critical gaps. Minor enhancements could include more inline documentation in defaults/ files, especially for any complex dict structures.
Next Steps:
Apply these patterns rigorously in new roles. The variable management discipline in existing roles should be maintained and used as a template. For any future roles with complex variables, follow the postgresql pattern of comprehensive inline documentation.