maverick-ansible

get2knowio's avatarfrom get2knowio

Ansible playbook, role, and security best practices

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 7, 2026⬇️3downloads

When & Why to Use This Skill

This Claude skill provides expert-level guidance for Ansible automation, focusing on the creation of idempotent playbooks, modular roles, and robust security configurations. It helps DevOps engineers and developers implement Infrastructure as Code (IaC) efficiently while adhering to industry best practices, ensuring system stability and secure secret management.

Use Cases

  • Case 1: Designing and refactoring complex Ansible playbooks to ensure idempotency and prevent configuration drift across multi-server environments.
  • Case 2: Implementing security best practices by using Ansible Vault for secret encryption and managing sensitive data with 'no_log' and privilege escalation controls.
  • Case 3: Structuring reusable automation components through Ansible Roles and Collections to improve code maintainability and team collaboration.
  • Case 4: Troubleshooting and optimizing automation tasks by replacing risky shell/command executions with built-in Ansible modules and FQCN (Fully Qualified Collection Names).
  • Case 5: Setting up automated CI/CD pipelines for infrastructure testing using ansible-lint and check-mode to validate configurations before deployment.
namemaverick-ansible
descriptionAnsible playbook, role, and security best practices
version1.0.0
- "tasks"
- "handlers"
- "roles"
- "hosts"

Ansible Skill

Expert guidance for Ansible playbooks, roles, and automation best practices.

Playbook Structure

Basic Playbook

---
- name: Configure web servers
  hosts: webservers
  become: true
  vars:
    http_port: 80

  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Start nginx service
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Multiple Plays

---
- name: Configure database servers
  hosts: databases
  tasks:
    - name: Install PostgreSQL
      ansible.builtin.apt:
        name: postgresql
        state: present

- name: Configure web servers
  hosts: webservers
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present

Idempotency (CRITICAL)

Idempotent tasks can run multiple times without changing the result after the first run.

✅ Idempotent Examples

# Good - using built-in modules (always idempotent)
- name: Ensure nginx is installed
  ansible.builtin.apt:
    name: nginx
    state: present

- name: Ensure config file has correct content
  ansible.builtin.copy:
    src: nginx.conf
    dest: /etc/nginx/nginx.conf
    owner: root
    mode: '0644'

❌ Non-Idempotent Antipatterns

# BAD - runs every time, always reports "changed"
- name: Configure system
  ansible.builtin.shell: echo "config" >> /etc/app.conf

# GOOD - idempotent alternative
- name: Ensure config line exists
  ansible.builtin.lineinfile:
    path: /etc/app.conf
    line: "config"
    create: true

Making Shell/Command Idempotent

# Use changed_when and creates/removes
- name: Download file
  ansible.builtin.shell: |
    curl -o /tmp/file.tar.gz https://example.com/file.tar.gz
  args:
    creates: /tmp/file.tar.gz
  changed_when: false

# Or check conditions first
- name: Check if configured
  ansible.builtin.stat:
    path: /etc/app/.configured
  register: configured

- name: Run configuration script
  ansible.builtin.shell: /opt/app/configure.sh
  when: not configured.stat.exists

- name: Mark as configured
  ansible.builtin.file:
    path: /etc/app/.configured
    state: touch
  when: not configured.stat.exists

Variables & Jinja2 Templating

Variable Precedence (low to high)

  1. Role defaults (roles/*/defaults/main.yml)
  2. Inventory vars
  3. Playbook vars
  4. Extra vars (-e / --extra-vars)

Using Variables

---
- name: Deploy application
  hosts: app_servers
  vars:
    app_version: "1.2.3"
    app_user: "appuser"

  tasks:
    - name: Download application version {{ app_version }}
      ansible.builtin.get_url:
        url: "https://releases.example.com/app-{{ app_version }}.tar.gz"
        dest: "/opt/app-{{ app_version }}.tar.gz"
        owner: "{{ app_user }}"

Facts

# Gather facts automatically (default: true)
- name: Show system facts
  hosts: all
  tasks:
    - name: Display OS family
      ansible.builtin.debug:
        msg: "OS family is {{ ansible_os_family }}"

    - name: Display IP address
      ansible.builtin.debug:
        msg: "IP is {{ ansible_default_ipv4.address }}"

Conditionals

- name: Install package on Debian-based systems
  ansible.builtin.apt:
    name: nginx
    state: present
  when: ansible_os_family == "Debian"

- name: Install package on RedHat-based systems
  ansible.builtin.yum:
    name: nginx
    state: present
  when: ansible_os_family == "RedHat"

Handlers

Handlers run once at the end of a play, after all tasks.

---
- name: Configure web server
  hosts: webservers
  tasks:
    - name: Copy nginx config
      ansible.builtin.copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: Restart nginx

    - name: Copy site config
      ansible.builtin.template:
        src: site.conf.j2
        dest: /etc/nginx/sites-available/mysite
      notify: Restart nginx

  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

Key points:

  • Handlers only run if notified
  • Handlers run once, even if notified multiple times
  • Handlers run at the end of the play
  • Use meta: flush_handlers to run handlers immediately

Loops

Modern Loop Syntax (Ansible 2.5+)

# Preferred
- name: Install packages
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - postgresql
    - redis

# With dictionary
- name: Create users
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
  loop:
    - { name: 'alice', groups: 'admin' }
    - { name: 'bob', groups: 'users' }

Legacy Loop Syntax (avoid)

# OLD - avoid in new playbooks
- name: Install packages
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  with_items:
    - nginx
    - postgresql

Roles

Role Structure

roles/
└── webserver/
    ├── tasks/
    │   └── main.yml          # Main task list
    ├── handlers/
    │   └── main.yml          # Handlers
    ├── templates/
    │   └── nginx.conf.j2     # Jinja2 templates
    ├── files/
    │   └── index.html        # Static files
    ├── vars/
    │   └── main.yml          # Variables
    ├── defaults/
    │   └── main.yml          # Default variables
    ├── meta/
    │   └── main.yml          # Role metadata/dependencies
    └── README.md

Using Roles

---
- name: Configure servers
  hosts: webservers
  roles:
    - common
    - webserver
    - monitoring

# Or with parameters
- name: Configure servers
  hosts: webservers
  roles:
    - role: webserver
      vars:
        http_port: 8080

Role Dependencies

# roles/webserver/meta/main.yml
---
dependencies:
  - role: common
  - role: firewall
    vars:
      firewall_allowed_ports:
        - 80
        - 443

Security Best Practices

Ansible Vault (CRITICAL)

Never commit unencrypted secrets!

# Encrypt a file
ansible-vault encrypt secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Run playbook with vault password
ansible-playbook site.yml --ask-vault-pass
# Or use password file
ansible-playbook site.yml --vault-password-file ~/.vault_pass

Encrypted variables:

# vars/secrets.yml (encrypted with ansible-vault)
---
db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386439653361336136343...
api_key: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          35323264353938643261623...

Privilege Escalation

Use become carefully:

# Entire play
- name: Configure system
  hosts: servers
  become: true  # Run all tasks as root
  tasks:
    - name: Install package
      ansible.builtin.apt:
        name: nginx

# Specific task
- name: Configure system
  hosts: servers
  tasks:
    - name: Install package (requires root)
      ansible.builtin.apt:
        name: nginx
      become: true

    - name: Copy user file (no root needed)
      ansible.builtin.copy:
        src: .bashrc
        dest: ~/.bashrc

Become user:

- name: Run as specific user
  ansible.builtin.command: whoami
  become: true
  become_user: appuser

Hiding Sensitive Output

# Don't log sensitive data
- name: Set database password
  ansible.builtin.shell: |
    psql -c "ALTER USER postgres PASSWORD '{{ db_password }}'"
  no_log: true

# Register without logging
- name: Get API token
  ansible.builtin.uri:
    url: https://api.example.com/token
    method: POST
  register: token_response
  no_log: true

Inventory Security

# Don't commit passwords in inventory
# BAD
[databases]
db1 ansible_host=10.0.1.10 ansible_user=admin ansible_password=secret123

# GOOD - use vault for passwords
[databases]
db1 ansible_host=10.0.1.10 ansible_user=admin

# In encrypted group_vars/databases.yml
ansible_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          ...

Module Best Practices

Prefer Specific Modules Over shell/command

# BAD - using shell when module exists
- name: Install package
  ansible.builtin.shell: apt-get install -y nginx

# GOOD - using built-in module
- name: Install package
  ansible.builtin.apt:
    name: nginx
    state: present

# BAD - using shell for file operations
- name: Create directory
  ansible.builtin.shell: mkdir -p /opt/app

# GOOD - using file module
- name: Create directory
  ansible.builtin.file:
    path: /opt/app
    state: directory
    mode: '0755'

Fully Qualified Collection Names (Ansible 2.10+)

# GOOD - explicit collection
- name: Install package
  ansible.builtin.apt:
    name: nginx

# Also acceptable for clarity
- name: Manage Docker container
  community.docker.docker_container:
    name: myapp
    image: nginx:latest

Common Pitfalls

1. Not Using check_mode

# Always test with --check first
# ansible-playbook site.yml --check

- name: Dangerous operation
  ansible.builtin.file:
    path: /etc/important-config
    state: absent
  # Test with --check before running!

2. Ignoring Return Codes

# BAD - ignores failures
- name: Run script
  ansible.builtin.shell: /opt/app/script.sh
  ignore_errors: true

# GOOD - handle failures explicitly
- name: Run script
  ansible.builtin.shell: /opt/app/script.sh
  register: script_result
  failed_when: script_result.rc != 0 and script_result.rc != 2

3. Not Handling "changed" Status

# BAD - always reports changed
- name: Check status
  ansible.builtin.shell: systemctl status nginx

# GOOD - mark as not changed
- name: Check status
  ansible.builtin.shell: systemctl status nginx
  changed_when: false
  check_mode: false

4. Using shell when command suffices

# BAD - unnecessary shell features
- name: List files
  ansible.builtin.shell: ls /tmp

# GOOD - use command (safer, no shell injection)
- name: List files
  ansible.builtin.command: ls /tmp
  changed_when: false

Ansible Lint

Common Rules

yaml[line-length] - Keep lines under 160 characters name[missing] - All tasks must have names fqcn[action-core] - Use fully qualified collection names no-changed-when - shell/command should have changed_when risky-shell-pipe - Avoid pipes in shell commands

Configuration

# .ansible-lint
---
skip_list:
  - yaml[line-length]  # Skip if you have long lines

warn_list:
  - experimental  # Warn on experimental features

exclude_paths:
  - .cache/
  - test/

Naming Conventions

# GOOD - descriptive names in imperative mood
- name: Ensure nginx is installed
- name: Copy nginx configuration
- name: Restart nginx service

# BAD - vague or missing names
- name: nginx
- name: Task 1
- apt: name=nginx  # Missing name entirely

Tags for Selective Execution

---
- name: Configure system
  hosts: all
  tasks:
    - name: Install packages
      ansible.builtin.apt:
        name: "{{ item }}"
      loop:
        - nginx
        - postgresql
      tags:
        - packages
        - install

    - name: Configure firewall
      ansible.builtin.ufw:
        rule: allow
        port: 80
      tags:
        - firewall
        - security

# Run only specific tags
# ansible-playbook site.yml --tags "packages"
# ansible-playbook site.yml --skip-tags "firewall"

Review Severity Guidelines

  • CRITICAL: Unencrypted secrets in files, using shell with user input, privilege escalation without justification
  • MAJOR: Non-idempotent tasks, using shell/command when module exists, missing no_log on sensitive tasks, not using ansible-vault
  • MINOR: Missing task names, not using FQCN, missing changed_when on commands, using legacy with_items
  • SUGGESTION: Could use tags for organization, could extract to role, could use handlers instead of multiple restarts

References