src/maverick/skills/maverick_ansible/SKILL.md
Ansible playbook, role, and security best practices
npx skillsauth add get2knowio/maverick maverick-ansibleInstall 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.
Expert guidance for Ansible playbooks, roles, and automation best practices.
---
- 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
---
- 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
Idempotent tasks can run multiple times without changing the result after the first run.
# 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'
# 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
# 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
roles/*/defaults/main.yml)-e / --extra-vars)---
- 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 }}"
# 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 }}"
- 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 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:
meta: flush_handlers to run handlers immediately# 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' }
# OLD - avoid in new playbooks
- name: Install packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
with_items:
- nginx
- postgresql
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
---
- name: Configure servers
hosts: webservers
roles:
- common
- webserver
- monitoring
# Or with parameters
- name: Configure servers
hosts: webservers
roles:
- role: webserver
vars:
http_port: 8080
# roles/webserver/meta/main.yml
---
dependencies:
- role: common
- role: firewall
vars:
firewall_allowed_ports:
- 80
- 443
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...
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
# 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
# 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
...
# 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'
# 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
# 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!
# 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
# 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
# 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
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
# .ansible-lint
---
skip_list:
- yaml[line-length] # Skip if you have long lines
warn_list:
- experimental # Warn on experimental features
exclude_paths:
- .cache/
- test/
# 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
---
- 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"
shell with user input, privilege escalation without justificationno_log on sensitive tasks, not using ansible-vaultchanged_when on commands, using legacy with_itemsdevelopment
Rust unsafe code, FFI, and safety invariants
development
Rust testing patterns (unit, integration, property-based)
development
Rust performance optimization and zero-cost abstractions
development
Rust ownership, borrowing, and lifetimes