From ansible-workflows
Designs Ansible playbooks using state-based patterns for present/absent states, organizes plays with pre_tasks/roles/handlers, and structures variables by precedence levels.
npx claudepluginhub basher83/lunar-claude --plugin ansible-workflowsThis skill uses the workspace's default tool permissions.
Patterns for designing well-structured, maintainable Ansible playbooks.
Guides Ansible playbook creation, roles, inventories, project structure, modules, handlers, error handling, and debugging YAML syntax, module failures, variable precedence.
Provides examples and best practices for writing Ansible playbooks to automate configuration management, server setup, and infrastructure orchestration.
Generates or scaffolds Ansible playbooks, roles, tasks, handlers, inventory, and vars from requests. Handles full projects, snippets, or docs with templates and best practices.
Share bugs, ideas, or general feedback.
Patterns for designing well-structured, maintainable Ansible playbooks.
Design playbooks to handle both creation and removal via a state variable.
---
- name: Manage admin user account
hosts: all
become: true
vars:
admin_state: present # or absent
tasks:
- name: Create admin user
ansible.builtin.user:
name: "{{ admin_name }}"
groups: "{{ admin_groups }}"
state: "{{ admin_state }}"
- name: Configure SSH key
ansible.posix.authorized_key:
user: "{{ admin_name }}"
key: "{{ admin_ssh_key }}"
state: "{{ admin_state }}"
when: admin_state == 'present'
# Create user (default)
uv run ansible-playbook playbooks/manage-admin.yml \
-e "admin_name=alice" \
-e "admin_ssh_key='ssh-ed25519 AAAA...'"
# Remove user
uv run ansible-playbook playbooks/manage-admin.yml \
-e "admin_name=alice" \
-e "admin_state=absent"
Order sections consistently across all playbooks:
---
- name: Descriptive play name
hosts: target_group
become: true
gather_facts: true
vars:
# Play-level variables
app_version: "2.0.0"
vars_files:
# External variable files
- vars/secrets.yml
pre_tasks:
# Tasks that must run before roles
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
roles:
# Role includes
- role: common
- role: app_deploy
vars:
deploy_version: "{{ app_version }}"
tasks:
# Play-specific tasks
- name: Verify deployment
ansible.builtin.uri:
url: http://localhost:8080/health
post_tasks:
# Cleanup or finalization
- name: Send deployment notification
ansible.builtin.debug:
msg: "Deployment complete"
handlers:
# Event-triggered tasks
- name: restart app
ansible.builtin.systemd:
name: myapp
state: restarted
From lowest to highest precedence:
roles/x/defaults/main.yml)group_vars/all.yml)host_vars/hostname.yml)vars: in playbook)vars: on task)-e on command line) - highestansible/
├── group_vars/
│ ├── all.yml # Variables for ALL hosts
│ ├── proxmox.yml # Proxmox cluster hosts
│ └── docker_hosts.yml # Docker host group
├── host_vars/
│ ├── node01.yml # Host-specific overrides
│ └── node02.yml
└── playbooks/
└── deploy.yml # Uses vars: for playbook-specific
# group_vars/all.yml - Global defaults
default_timezone: "UTC"
ntp_servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
# group_vars/proxmox.yml - Group-specific
proxmox_api_host: "192.168.1.10"
proxmox_cluster_name: "production"
# host_vars/node01.yml - Host-specific overrides
proxmox_node_id: 1
ceph_osd_devices:
- /dev/sdb
- /dev/sdc
Split playbook tasks into separate files when:
# playbooks/setup-cluster.yml
---
- name: Setup Proxmox cluster
hosts: proxmox
become: true
tasks:
- name: Configure networking
ansible.builtin.include_tasks: tasks/networking.yml
- name: Setup storage
ansible.builtin.include_tasks: tasks/storage.yml
when: setup_storage | default(true)
- name: Initialize cluster
ansible.builtin.include_tasks: tasks/cluster-init.yml
when: inventory_hostname == groups['proxmox'][0]
| Feature | import_tasks | include_tasks |
|---|---|---|
| When evaluated | Parse time (static) | Runtime (dynamic) |
| Supports loops | No | Yes |
| Supports conditionals on import | Limited | Full |
| Use case | Ordered execution | Conditional/looped |
# Static import - always loaded, order matters
- ansible.builtin.import_tasks: users.yml
- ansible.builtin.import_tasks: permissions.yml
# Dynamic include - conditional, looped
- ansible.builtin.include_tasks: "setup-{{ ansible_os_family }}.yml"
- ansible.builtin.include_tasks: deploy-app.yml
loop: "{{ applications }}"
Use multiple plays for different host groups or privilege levels:
---
# Play 1: Gather facts from all nodes
- name: Gather cluster information
hosts: proxmox
gather_facts: true
tasks:
- name: Set cluster facts
ansible.builtin.set_fact:
cluster_node_count: "{{ groups['proxmox'] | length }}"
# Play 2: Initialize primary node
- name: Initialize cluster on primary
hosts: proxmox[0]
become: true
tasks:
- name: Create cluster
ansible.builtin.command: pvecm create {{ cluster_name }}
when: not cluster_exists
# Play 3: Join secondary nodes
- name: Join cluster on secondary nodes
hosts: proxmox[1:]
become: true
serial: 1 # One node at a time
tasks:
- name: Join cluster
ansible.builtin.command: pvecm add {{ primary_node }}
when: not node_in_cluster
---
- name: Configure web server
hosts: webservers
become: true
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: reload nginx
- name: Update SSL certificates
ansible.builtin.copy:
src: "{{ item }}"
dest: /etc/nginx/ssl/
loop:
- cert.pem
- key.pem
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
Handlers run:
Force immediate handler execution:
- name: Update critical config
ansible.builtin.template:
src: config.j2
dest: /etc/app/config.yml
notify: restart app
- name: Flush handlers now
ansible.builtin.meta: flush_handlers
- name: Verify app is running
ansible.builtin.uri:
url: http://localhost:8080/health
Add validation at the start of playbooks:
---
- name: Deploy application
hosts: app_servers
become: true
tasks:
- name: Validate required variables
ansible.builtin.assert:
that:
- app_version is defined
- app_version | regex_search('^\d+\.\d+\.\d+$')
- deploy_env in ['staging', 'production']
fail_msg: "Invalid configuration. Check app_version and deploy_env."
- name: Check disk space
ansible.builtin.assert:
that: ansible_mounts | selectattr('mount', 'equalto', '/') | map(attribute='size_available') | first > 1073741824
fail_msg: "Insufficient disk space. Need at least 1GB free."
---
# playbooks/template-playbook.yml
# Description: [What this playbook does]
# Usage: uv run ansible-playbook playbooks/template-playbook.yml -e "var=value"
# Requirements: [Any prerequisites]
- name: [Descriptive play name]
hosts: [target_group]
become: [true/false]
gather_facts: [true/false]
vars:
# Configurable variables with defaults
resource_state: present
tasks:
- name: Validate inputs
ansible.builtin.assert:
that:
- required_var is defined
fail_msg: "required_var must be defined"
# Main tasks...
- name: Verify completion
ansible.builtin.debug:
msg: "Playbook completed successfully"
For detailed playbook patterns and techniques, consult:
references/playbook-role-patterns.md - Comprehensive playbook organization patterns, play structure, import strategies