Imported from josmase/ansible (
AGENTS.md). Install upstream withnpx skills add josmase/ansible. Copyright stays with the author.
Ansible Best Practices Guide
Directory Structure
ansible/
├── ansible.cfg # Ansible configuration file
├── inventory/ # Inventory directory
│ ├── production.ini # Production inventory file
│ ├── staging.ini # Staging inventory file
│ ├── group_vars/ # Group-specific variables
│ │ ├── all/ # Variables for all groups
│ │ │ ├── main.yml # Non-sensitive variables
│ │ │ └── vault.yml # Group-level encrypted variables
│ │ └── [group]/ # Group-specific variables
│ └── host_vars/ # Host-specific variables
├── collections/ # Collections requirements
│ └── requirements.yml # Collection dependencies
├── playbooks/ # Playbook files
│ ├── site.yml # Main entry point playbook
│ ├── vars/ # Playbook variables
│ │ ├── common.yml # Common variables
│ │ └── vault.yml # Global encrypted variables
│ ├── setup/ # Setup playbooks
│ ├── maintenance/ # Maintenance tasks
│ └── services/ # Service deployment playbooks
└── roles/ # Reusable roles grouped by domain
├── core/
├── container/
├── platform/
├── storage/
├── services/
├── workstation/
└── validation/
└── role_name/ # Individual role
├── defaults/ # Default variables
├── files/ # Static files
├── handlers/ # Notification handlers
├── meta/ # Role metadata
├── tasks/ # Task definitions
├── templates/ # Jinja2 templates
└── vars/ # Role variables
Naming Conventions
-
Files and Directories
- Use lowercase letters
- Use underscores for spaces
- Use
.ymlextension for YAML files - Use descriptive names:
setup_docker.yml, notsetup.yml
-
Variables
- Use snake_case:
nfs_mount_point, notnfsMountPoint - Prefix role variables with role name:
docker_compose_dir - Use descriptive names:
maintenance_calendar, notcal
- Use snake_case:
Role Independence and Variable Organization
-
Role Independence Principles
- Roles should be self-contained units
- All required variables must have defaults
- Never rely on variables from other roles
- Document any optional external variables
- Example of role independence:
# Bad - Relying on global variables tasks/main.yml: - name: Create docker directories file: path: "{{ global_docker_dir }}/{{ item }}" state: directory # Good - Self-contained role defaults/main.yml: docker_base_dir: /opt/docker docker_subdirs: - compose - scripts tasks/main.yml: - name: Create docker directories file: path: "{{ docker_base_dir }}/{{ item }}" state: directory loop: "{{ docker_subdirs }}"
-
Variable Precedence Order (highest to lowest):
- Command line
-evariables - Playbook vars/ directory variables
- Host variables (
inventory/host_vars/) - Group variables (
inventory/group_vars/) - Role defaults (
roles/x/defaults/) - Inventory variables
- Command line
-
Variable File Organization
# group_vars/all/main.yml - Non-sensitive group variables kubernetes_config_dir: "~/.kube" ansible_sudo_pass: "{{ vault_default_sudo_pass }}" # vars/vault.yml - Sensitive variables (encrypted) vault_default_sudo_pass: "secretpassword" # roles/docker_setup/defaults/main.yml - Role defaults docker_compose_dir: "/opt/docker/compose" -
Distribution-specific Variables
# roles/usb_module_setup/defaults/main.yml # Package names by distribution debian_packages: - usbutils - linux-modules-extra-{{ ansible_kernel }} arch_packages: - usbutils - linux-headers
Role Organization
-
Role Structure
roles/docker/ ├── README.md # Role documentation and usage examples ├── defaults/ # Default variables │ └── main.yml # Default role variables (lowest precedence) ├── files/ # Static files ├── handlers/ # Notification handlers │ └── main.yml # Handler definitions ├── meta/ # Role metadata and dependencies │ └── main.yml # Dependencies and compatibility info ├── tasks/ # Task definitions │ ├── main.yml # Main task entry point │ ├── install.yml # Installation tasks │ └── config.yml # Configuration tasks ├── templates/ # Jinja2 templates └── vars/ # Role variables (higher precedence) └── main.yml # Fixed role variables -
Role Independence
- Keep roles independent of global variables
- Define all required variables in defaults/main.yml
- Override defaults through inventory or playbook vars when needed
- Example of a self-contained role:
# roles/docker_setup/defaults/main.yml --- # Base paths - don't rely on global variables docker_base_dir: "/opt/docker" docker_compose_dir: "{{ docker_base_dir }}/compose" docker_scripts_dir: "{{ docker_base_dir }}/scripts" docker_data_dir: "{{ docker_base_dir }}/data" # Timer configuration - self-contained in the role docker_timers: - name: maintenance schedule: "*-*-* 04:00:00" - name: staleFileHandleHandler schedule: "*-*-* *:00:00" # Distribution-specific packages docker_debian_packages: - docker-ce - docker-ce-cli docker_arch_packages: - docker
-
Role Best Practices
- Keep roles focused and single-purpose
- Make roles distribution-agnostic:
# Example from USB module setup - name: Install required packages for Debian-based systems apt: name: "{{ debian_packages }}" state: present when: ansible_os_family == "Debian" - name: Install required packages for Arch-based systems pacman: name: "{{ arch_packages }}" state: present when: ansible_os_family == "Archlinux" - Use descriptive task names for better logging
- Document role variables in defaults/main.yml with examples
- Use handlers for service restarts
- Include role dependencies in meta/main.yml
- Split complex task files into logical subfiles
- Provide working examples in README.md
-
Role Documentation
# Docker Setup Role Sets up Docker environment with maintenance scripts and timers. ## Variables All variables are defined in defaults/main.yml with sensible defaults. No external variables required. ### Optional Variables Override these in your inventory if needed: - docker_base_dir: Base directory for Docker files - docker_timers: List of maintenance timers ## Dependencies None. This role is self-contained and does not rely on external roles or variables.
Security Best Practices
-
Vault Organization
- Store global encrypted variables in
playbooks/vars/vault.yml - Store group-specific encrypted variables in
inventory/group_vars/[group]/vault.yml - Never commit unencrypted sensitive data
- Use meaningful vault IDs:
--vault-id prod@prompt
- Store global encrypted variables in
-
Vault Usage in Playbooks
- Always use absolute paths with playbook_dir:
vars_files: - "{{ playbook_dir }}/../../vars/vault.yml" # From playbooks/setup/ - "{{ playbook_dir }}/../vars/vault.yml" # From playbooks/ - Define vault_vars_file in common.yml for consistent reference:
# In playbooks/vars/common.yml vault_vars_file: "{{ playbook_dir }}/../vars/vault.yml" # In playbooks vars_files: - vars/common.yml - "{{ vault_vars_file }}" - Separate sensitive and non-sensitive variables:
# In group_vars/all/main.yml ansible_sudo_pass: "{{ vault_default_sudo_pass }}" # In vars/vault.yml (encrypted) vault_default_sudo_pass: "secretpassword"
- Always use absolute paths with playbook_dir:
-
File Permissions
- Set explicit file permissions with
mode - Use restrictive permissions for sensitive files
- Always set owner and group for files
- Set explicit file permissions with
Task Writing
-
General Guidelines
- Use YAML dictionary syntax for tasks
- Always name your tasks descriptively
- Use
state: presentexplicitly (don't rely on defaults) - Include
whenconditions at the task level, not play level - Split complex plays into separate task files:
# main.yml - name: Include installation tasks include_tasks: install.yml - name: Include configuration tasks include_tasks: config.yml
-
Task Organization
- Group related tasks in separate files
- Use include_tasks for better maintainability
- Keep task files focused and manageable
- Use meaningful file names:
tasks/ ├── main.yml # Main entry point ├── install.yml # Installation tasks ├── config.yml # Configuration tasks └── service.yml # Service management
-
Idempotency and Safety
- Tasks should be idempotent (safe to run multiple times)
- Use
creates,removes, orstatmodule to check existing state - Prefer package modules over command modules
- Handle system-specific differences:
- name: Ensure kernel modules are loaded modprobe: name: "{{ item }}" state: present loop: - ftdi_sio - usbserial when: ansible_system == "Linux"
Inventory Management
-
Group Organization
- Use meaningful group names
- Use group_vars for common configurations
- Use children groups for hierarchy
- Document group purpose in inventory
-
Host Management
- Use FQDN or IP addresses consistently
- Group hosts by function AND environment
- Use
ansible_hostfor IP addresses when hostname differs
Testing and Validation
-
Before Running
- Use
--checkmode to verify changes - Use
--diffto see file changes - Validate syntax:
ansible-playbook --syntax-check
- Use
-
During Development
- Test on a single host before running on many
- Use tags for selective execution
- Use
--limitto restrict scope
Error Handling
-
Task Level
- Use
ignore_errorssparingly and document why - Use
failed_whenfor custom failure conditions - Use
changed_whento control changed status
- Use
-
Play Level
- Set
any_errors_fatalfor critical plays - Use
max_fail_percentagefor controlled failures - Implement proper error handling in custom modules
- Set
Documentation
-
In Code
- Document non-obvious task purposes
- Include examples in role README.md
- Document required variables
- Explain complex conditions or loops
-
External
- Maintain inventory documentation
- Document deployment procedures
- Keep change logs
- Document recovery procedures
Playbook Organization and Best Practices
-
Playbook Structure
# site.yml - Main entry point - name: Validate configuration hosts: all vars_files: - vars/common.yml - "{{ vault_vars_file }}" roles: - variable_validation # setup/base.yml - Setup playbook - name: Base system configuration hosts: all_servers vars_files: - "{{ playbook_dir }}/../../vars/vault.yml" roles: - base_setup -
Distribution-agnostic Design
- Use OS family facts for conditionals:
when: ansible_os_family == "Debian" when: ansible_os_family == "Archlinux" - Define OS-specific variables in defaults
- Use package module for cross-platform support
- Handle service differences per OS
- Use OS family facts for conditionals:
-
Timer and Service Management
- Use proper systemd calendar formats
- Implement consistent scheduling:
# Example from docker_setup timers: - name: maintenance schedule: "*-*-* 04:00:00" - name: staleFileHandleHandler schedule: "*-*-* *:00:00" - Handle service dependencies
- Use handlers for restarts
-
Maintenance and Updates
- Implement rolling updates
- Handle reboots safely
- Validate configurations
- Monitor service health
- Add monitoring role
- Improve error handling
-
Development Workflow
- Add development environment
- Implement testing framework
- Add CI/CD pipeline
- Improve documentation