plugins/infrastructure/ansible-workflows/skills/ansible-role-design/SKILL.md
Production-grade Ansible role structure patterns for directory layout, variable organization (defaults vs vars), handlers, and task organization, derived from analysis of 7 geerlingguy production roles.
npx skillsauth add basher83/lunar-claude ansible-role-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Production-grade role structure patterns derived from analysis of 7 geerlingguy roles.
Every Ansible role follows this organizational pattern:
role-name/
├── defaults/
│ └── main.yml # User-configurable defaults (lowest precedence)
├── vars/
│ ├── Debian.yml # OS-specific internal values
│ └── RedHat.yml
├── tasks/
│ ├── main.yml # Task router
│ ├── install.yml # Feature-specific tasks
│ └── configure.yml
├── handlers/
│ └── main.yml # Event-triggered tasks
├── templates/
│ └── config.conf.j2 # Jinja2 templates
├── files/
│ └── static-file.txt # Static files
├── meta/
│ └── main.yml # Role metadata, dependencies
└── README.md # Documentation
| Directory | Purpose | Precedence |
|-----------|---------|------------|
| defaults/ | User-overridable values | Lowest |
| vars/ | Internal/OS-specific values | High |
| tasks/ | Ansible tasks | N/A |
| handlers/ | Service restarts, reloads | N/A |
| templates/ | Jinja2 config files | N/A |
| files/ | Static files to copy | N/A |
| meta/ | Galaxy info, dependencies | N/A |
Only create directories that are actually needed:
templates/ if using only lineinfile or copyhandlers/ if role doesn't manage servicesvars/ if no OS-specific differencesfiles/ if no static files to copyUse tasks/main.yml as a routing file that includes feature-specific files:
# tasks/main.yml
---
- name: Include OS-specific variables
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
- name: Install packages
ansible.builtin.include_tasks: install.yml
- name: Configure service
ansible.builtin.include_tasks: configure.yml
- name: Setup users
ansible.builtin.include_tasks: users.yml
when: role_users | length > 0
| Scenario | Approach |
|----------|----------|
| < 30 lines | Keep in main.yml |
| 30-100 lines | Consider splitting |
| > 100 lines | Definitely split |
| Optional features | Separate file with when: |
| OS-specific logic | Separate files per OS |
Use descriptive, feature-based names:
tasks/
├── main.yml # Router only
├── install.yml # Package installation
├── configure.yml # Configuration tasks
├── users.yml # User management
├── install-Debian.yml # Debian-specific install
└── install-RedHat.yml # RedHat-specific install
| Location | Purpose | User Override? |
|----------|---------|----------------|
| defaults/main.yml | User configuration | Yes (easily) |
| vars/main.yml | Internal constants | Possible but discouraged |
| vars/Debian.yml | OS-specific values | No (internal) |
# defaults/main.yml
---
# User-configurable options
docker_edition: "ce"
docker_service_state: started
docker_service_enabled: true
docker_users: []
# Feature toggles
docker_install_compose: true
docker_compose_version: "2.24.0"
# vars/Debian.yml
---
# OS-specific internal values (not for user override)
docker_package_name: docker-ce
docker_service_name: docker
docker_config_path: /etc/docker/daemon.json
Simple pattern:
- name: Include OS-specific variables
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
Advanced pattern with fallback:
- name: Load OS-specific vars
ansible.builtin.include_vars: "{{ lookup('first_found', params) }}"
vars:
params:
files:
- "{{ ansible_distribution }}.yml"
- "{{ ansible_os_family }}.yml"
- main.yml
paths:
- vars
Prefix variables with role name:
# Pattern: {role_name}_{feature}_{attribute}
# Examples
docker_edition: "ce"
docker_service_state: started
docker_compose_version: "2.24.0"
docker_users: []
# Grouped by feature
security_ssh_port: 22
security_ssh_password_auth: "no"
security_fail2ban_enabled: true
# handlers/main.yml
---
- name: restart docker
ansible.builtin.systemd:
name: docker
state: restarted
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
Use lowercase with action + service pattern:
- name: restart ssh # Not "Restart SSH Service"
- name: reload nginx # Not "Reload Nginx Config"
- name: reload systemd # For daemon-reload
For cluster operations, restart one node at a time:
- name: restart pve-cluster
ansible.builtin.systemd:
name: pve-cluster
state: restarted
throttle: 1
Use templates/ when:
Use lineinfile when:
Expose template paths as variables for user override:
# defaults/main.yml
nginx_conf_template: nginx.conf.j2
nginx_vhost_template: vhost.j2
# tasks/configure.yml
- name: Deploy nginx config
ansible.builtin.template:
src: "{{ nginx_conf_template }}"
dest: /etc/nginx/nginx.conf
notify: reload nginx
# meta/main.yml
---
galaxy_info:
author: your_name
description: Role description
license: MIT
min_ansible_version: "2.12"
platforms:
- name: Debian
versions:
- bullseye
- bookworm
- name: Ubuntu
versions:
- focal
- jammy
dependencies:
- role: common
- role: geerlingguy.docker
when: install_docker | default(false)
Based on geerlingguy role analysis:
| Role Complexity | Directories | Task Files | Examples | |-----------------|-------------|------------|----------| | Minimal | 3-4 | 1 (main.yml) | pip, git | | Standard | 5-6 | 2-4 | security, docker | | Complex | 7+ | 5-8 | postgresql, nginx |
pip/
├── defaults/main.yml
├── tasks/main.yml
├── meta/main.yml
└── README.md
docker/
├── defaults/main.yml
├── vars/{Debian,RedHat}.yml
├── tasks/{main,install,configure}.yml
├── handlers/main.yml
├── meta/main.yml
└── README.md
postgresql/
├── defaults/main.yml
├── vars/{Debian,RedHat,Archlinux}.yml
├── tasks/{main,install,configure,users,databases}.yml
├── handlers/main.yml
├── templates/{postgresql.conf,pg_hba.conf}.j2
├── meta/main.yml
└── README.md
Start task names with action verbs:
# GOOD
- name: Ensure Docker is installed
- name: Configure SSH security settings
- name: Add user to docker group
# BAD
- name: Docker installation
- name: SSH settings
- name: User docker group
Validate critical configuration files:
- name: Update SSH configuration
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "^PermitRootLogin"
line: "PermitRootLogin no"
validate: 'sshd -T -f %s'
notify: restart ssh
- name: Update sudoers
ansible.builtin.lineinfile:
path: /etc/sudoers
line: "{{ user }} ALL=(ALL) NOPASSWD: ALL"
validate: 'visudo -cf %s'
Every role needs a README.md with:
For detailed role design patterns and techniques, consult:
references/role-structure-standards.md - Production role structure patterns from geerlingguy analysisreferences/handler-best-practices.md - Handler design, notification patterns, flush strategiesreferences/meta-dependencies.md - Role dependencies, Galaxy metadata, platform supportreferences/variable-management-patterns.md - Variable naming, scoping, precedence patternsreferences/documentation-templates.md - README templates and documentation standardstesting
Audit and improve CLAUDE.md files in repositories. Use when user asks to check, audit, update, improve, or fix CLAUDE.md files. Scans for all CLAUDE.md files, evaluates quality against templates, outputs quality report, then makes targeted updates. Also use when the user mentions "CLAUDE.md maintenance" or "project memory optimization".
tools
Operational tooling for Talos Linux Kubernetes clusters via Sidero Omni with Proxmox infrastructure provider, covering machine classes, CEL storage selectors, and provider lifecycle management.
tools
Best practices for git workflow automation including atomic commits, branch naming, conventional commit format, and changelog generation.
tools
Summarize the current state of the git repository