Marketplace

ansible-playbook-design

This skill should be used when creating new Ansible playbooks, designing playbook structure, implementing state-based playbooks with present/absent patterns, organizing plays and tasks, or structuring playbook variables. Covers play organization, variable scoping, and state-based design patterns.

$ Installer

git clone https://github.com/basher83/lunar-claude /tmp/lunar-claude && cp -r /tmp/lunar-claude/plugins/infrastructure/ansible-workflows/skills/ansible-playbook-design ~/.claude/skills/lunar-claude

// tip: Run this command in your terminal to install the skill


name: ansible-playbook-design description: > This skill should be used when creating new Ansible playbooks, designing playbook structure, implementing state-based playbooks with present/absent patterns, organizing plays and tasks, or structuring playbook variables. Covers play organization, variable scoping, and state-based design patterns.

Ansible Playbook Design

Patterns for designing well-structured, maintainable Ansible playbooks.

State-Based Playbook Pattern

Design playbooks to handle both creation and removal via a state variable.

Core Pattern

---
- 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'

Usage

# 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"

Benefits

  • Single source of truth
  • Consistent interface
  • Less code duplication
  • Follows community role conventions

Play Structure

Recommended Play Sections

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

Variable Organization

Variable Precedence (Key Levels)

From lowest to highest precedence:

  1. Role defaults (roles/x/defaults/main.yml)
  2. Inventory group_vars (group_vars/all.yml)
  3. Inventory host_vars (host_vars/hostname.yml)
  4. Play vars (vars: in playbook)
  5. Task vars (vars: on task)
  6. Extra vars (-e on command line) - highest

Organizing Variables

ansible/
├── 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

Variable Naming by Scope

# 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

Task Organization with Includes

When to Split Tasks

Split playbook tasks into separate files when:

  • Tasks exceed 50 lines
  • Logical groupings emerge (networking, storage, users)
  • Conditional sections can be skipped entirely

Include Patterns

# 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]

import_tasks vs include_tasks

Featureimport_tasksinclude_tasks
When evaluatedParse time (static)Runtime (dynamic)
Supports loopsNoYes
Supports conditionals on importLimitedFull
Use caseOrdered executionConditional/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 }}"

Multi-Play Playbooks

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

Handler Best Practices

Define Handlers at Play Level

---
- 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

Handler Execution Order

Handlers run:

  1. At the end of each play
  2. In the order they are defined (not notified)
  3. Only once, even if notified multiple times

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

Playbook Validation

Pre-flight Checks

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."

Template Patterns

Playbook Template Structure

---
# 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"

Additional Resources

For detailed playbook patterns and techniques, consult:

  • references/playbook-role-patterns.md - Comprehensive playbook organization patterns, play structure, import strategies

Related Skills

  • ansible-role-design - When to use roles vs playbooks
  • ansible-fundamentals - Core module selection and naming
  • ansible-error-handling - Block/rescue patterns in playbooks

Repository

basher83
basher83
Author
basher83/lunar-claude/plugins/infrastructure/ansible-workflows/skills/ansible-playbook-design
10
Stars
1
Forks
Updated2d ago
Added5d ago