12 KiB
name, description
| name | description |
|---|---|
| ansible-best-practices | Ansible playbook and role patterns using ansible.builtin modules, community.general, community.proxmox, ansible.posix collections, molecule testing, ansible-lint validation, and Infisical secrets management. Covers idempotency patterns (changed_when, failed_when, register), YAML playbook structure, Jinja2 templating, handler patterns, and variable precedence rules. This skill should be used when writing Ansible playbooks, developing Ansible roles, testing with molecule/ansible-lint, managing secrets with Infisical, implementing idempotent task patterns with changed_when/failed_when directives, or configuring Proxmox/network automation. |
Ansible Playbook Best Practices
Expert guidance for writing maintainable, idempotent, and testable Ansible playbooks based on real-world patterns from this repository.
Quick Reference
Pattern Decision Guide
| Need | Use Pattern | Details |
|---|---|---|
| Use secrets? | Infisical Secret Management | patterns/secrets-management.md |
| Resource management? | State-Based Playbooks | patterns/playbook-role-patterns.md |
| No native module? | Hybrid Module Approach | See Hybrid Module section below |
| Task failing? | Proper Error Handling | patterns/error-handling.md |
| Repeating blocks? | Task Organization | patterns/task-organization.md |
| Network config? | Network Automation | patterns/network-automation.md |
| Tasks show 'changed'? | Idempotency Patterns | reference/idempotency-patterns.md |
Golden Rules
- Use
uv runprefix - Always:uv run ansible-playbook - Fully qualify modules -
ansible.builtin.copynotcopy - Secrets via Infisical - Use reusable task pattern
- Control
command/shell- Always usechanged_when,failed_when - Use
set -euo pipefail- In all shell scripts - Tag sensitive tasks - Use
no_log: true - Idempotency first - Check before create, verify after
Common Commands
# Lint
mise run ansible-lint
# Analyze complexity
./tools/analyze_playbook.py ansible/playbooks/my-playbook.yml
# Check idempotency
./tools/check_idempotency.py ansible/playbooks/my-playbook.yml
# Run with secrets
cd ansible && uv run ansible-playbook playbooks/my-playbook.yml
Core Patterns from This Repository
1. Infisical Secret Management
This repository uses Infisical for centralized secrets management.
Quick Pattern:
- name: Retrieve Proxmox credentials
ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
vars:
secret_name: 'PROXMOX_PASSWORD'
secret_var_name: 'proxmox_password'
fallback_env_var: 'PROXMOX_PASSWORD' # Optional
Key Features: Validates authentication, proper no_log, fallback to env vars, reusable across playbooks.
See patterns/secrets-management.md for complete guide including authentication methods, security best practices, and CI/CD integration.
2. State-Based Playbooks
Pattern: Single playbook handles both create and remove via state variable.
# Create user (default)
uv run ansible-playbook playbooks/create-admin-user.yml \
-e "admin_name=alice" -e "admin_ssh_key='ssh-ed25519 ...'"
# Remove user (add state=absent)
uv run ansible-playbook playbooks/create-admin-user.yml \
-e "admin_name=alice" -e "admin_state=absent"
Why: Follows community role patterns, single source of truth, consistent interface, less duplication.
See patterns/playbook-role-patterns.md for complete implementation details and advanced patterns.
3. Hybrid Module Approach
Pattern: Use native modules where available, fall back to command when needed.
# GOOD: Native module
- name: Create Linux system user
ansible.builtin.user:
name: "{{ system_username }}"
state: present
# ACCEPTABLE: Command when no native module exists
- name: Create Proxmox API token
ansible.builtin.command: >
pveum user token add {{ system_username }}@{{ proxmox_user_realm }}
register: token_result
changed_when: "'already exists' not in token_result.stderr"
failed_when:
- token_result.rc != 0
- "'already exists' not in token_result.stderr"
Key: changed_when and failed_when make command module idempotent.
4. Proper Error Handling
- name: Check if resource exists
ansible.builtin.command: check-resource {{ resource_id }}
register: resource_check
changed_when: false # Read-only operation
failed_when: false # Don't fail, check in next task
- name: Fail if resource missing
ansible.builtin.fail:
msg: "Resource {{ resource_id }} not found"
when: resource_check.rc != 0
See patterns/error-handling.md for comprehensive patterns.
5. Task Organization
Reusable Tasks Pattern:
# In playbook
- name: Get database password
ansible.builtin.include_tasks: "{{ playbook_dir }}/../tasks/infisical-secret-lookup.yml"
vars:
secret_name: 'DB_PASSWORD'
secret_var_name: 'db_password'
Extract common patterns to tasks/ directory, use include_tasks with clear variable contracts.
See patterns/task-organization.md and patterns/reusable-tasks.md.
6. Network Automation
Pattern: Use community.general.interfaces_file for network configuration.
- name: Enable VLAN-aware bridging
community.general.interfaces_file:
iface: vmbr1
option: bridge-vlan-aware
value: "yes"
backup: true
state: present
notify: Reload network interfaces
Declarative config, automatic backup, handler pattern for reload.
See patterns/network-automation.md for advanced patterns including VLAN, bonding, and verification.
7. Idempotency Patterns
Use changed_when and failed_when:
# Check before create
- name: Check if VM exists
ansible.builtin.shell: |
set -o pipefail
qm list | awk '{print $1}' | grep -q "^{{ template_id }}$"
args:
executable: /bin/bash
register: vm_exists
changed_when: false # Checking doesn't change anything
failed_when: false # Don't fail if not found
# Conditional create
- name: Create VM
ansible.builtin.command: qm create {{ template_id }} ...
when: vm_exists.rc != 0
See reference/idempotency-patterns.md for comprehensive patterns.
Variable Organization
Quick Summary
Precedence: Extra vars (-e) > Role vars > Defaults
Organization:
ansible/
├── group_vars/all.yml # Variables for ALL hosts
├── group_vars/proxmox.yml # Group-specific
├── host_vars/foxtrot.yml # Host-specific
└── playbooks/
└── my-playbook.yml # Use vars: for playbook-specific
Key principle: Use defaults/main.yml for configurable options, vars/main.yml for constants.
See reference/variable-precedence.md for complete precedence rules (22 levels) and patterns/variable-management-patterns.md for advanced patterns.
Module Selection
Prefer ansible.builtin
Always use fully qualified collection names (FQCN):
# GOOD
- name: Ping hosts
ansible.builtin.ping:
# BAD (deprecated short names)
- name: Ping hosts
ping:
Community Collections in Use
community.general- General utilities (interfaces_file, etc.)community.proxmox- Proxmox VE managementinfisical.vault- Secrets managementansible.posix- POSIX system managementcommunity.docker- Docker management
See ../../ansible/requirements.yml and reference/collections-guide.md.
Testing
With ansible-lint
# Run all linters
mise run lint-all
# Just Ansible
mise run ansible-lint
Common Issues: Missing name: on tasks, using shell instead of command, not using
changed_when, deprecated short names, missing no_log on sensitive tasks.
With Molecule
cd tools/molecule/default
molecule create # Create test environment
molecule converge # Run playbook
molecule verify # Run tests
molecule destroy # Clean up
See reference/testing-guide.md and patterns/testing-comprehensive.md for CI/CD integration.
Common Anti-Patterns
See anti-patterns/common-mistakes.md for detailed examples.
Quick List
1. Not Using set -euo pipefail
# GOOD
- name: Run script
ansible.builtin.shell: |
set -euo pipefail
command1 | command2
args:
executable: /bin/bash
2. Missing no_log on Secrets
# GOOD
- name: Set password
ansible.builtin.command: set-password {{ password }}
no_log: true
3. Using shell When command Suffices
Use shell ONLY when you need shell features (pipes, redirects, etc.).
# GOOD: No shell features needed
- name: List files
ansible.builtin.command: ls -la
See anti-patterns/common-mistakes.md for complete list and anti-patterns/refactoring-guide.md for improvement strategies.
Tools Available
Python Analysis Tools (uv)
# Complexity metrics
./tools/analyze_playbook.py playbook.yml
# Find non-idempotent patterns
./tools/check_idempotency.py playbook.yml
# Variable organization helper
./tools/extract_variables.py playbook.yml
Linting
# Run all linters
./tools/lint-all.sh
Testing
# Molecule test scenarios
./tools/molecule/default/
Progressive Disclosure
Start here, drill down as needed:
Quick Reference (Read First)
- Playbook & Role Patterns - State-based playbooks, public API variables, validation
- Secrets Management - Infisical integration, authentication, security
Deep Patterns (Read When Needed)
- Testing Comprehensive - Molecule, CI/CD, test strategies
- Role Structure Standards - Directory org, naming conventions
- Documentation Templates - README structure, variable docs
- Variable Management Patterns - defaults vs vars, naming
- Handler Best Practices - Handler usage patterns
- Meta Dependencies - galaxy_info, dependencies
Advanced Automation (from ProxSpray Analysis)
- Cluster Automation - Proxmox cluster formation with idempotency
- Network Automation - Declarative network configuration
- CEPH Automation - Complete CEPH storage deployment
Core Reference
- Roles vs Playbooks - Organization patterns
- Variable Precedence - Complete precedence rules (22 levels)
- Idempotency Patterns - Advanced idempotency techniques
- Module Selection - Builtin vs community decision guide
- Testing Guide - Molecule and ansible-lint deep dive
- Collections Guide - Using and managing collections
- Production Repos - Studied geerlingguy roles index
Patterns & Anti-Patterns
- Error Handling - Proper error handling patterns
- Task Organization - Reusable tasks and includes
- Common Mistakes - What to avoid
- Refactoring Guide - How to improve existing playbooks
Related Skills
- Proxmox Infrastructure - Playbooks for template creation and network config
- NetBox + PowerDNS - Dynamic inventory and secrets management patterns