| name | maverick-ansible |
| description | Ansible playbook, role, and security best practices Use when this capability is needed. |
| metadata | {"author":"get2knowio"} |
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
- 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
- name: Configure system
ansible.builtin.shell: echo "config" >> /etc/app.conf
- name: Ensure config line exists
ansible.builtin.lineinfile:
path: /etc/app.conf
line: "config"
create: true
Making Shell/Command Idempotent
- 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
- 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)
- Role defaults (
roles/*/defaults/main.yml)
- Inventory vars
- Playbook vars
- 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
- 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+)
- name: Install packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis
- 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)
- 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
- name: Configure servers
hosts: webservers
roles:
- role: webserver
vars:
http_port: 8080
Role Dependencies
---
dependencies:
- role: common
- role: firewall
vars:
firewall_allowed_ports:
- 80
- 443
Security Best Practices
Ansible Vault (CRITICAL)
Never commit unencrypted secrets!
ansible-vault encrypt secrets.yml
ansible-vault edit secrets.yml
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Encrypted variables:
---
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653361336136343...
api_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
35323264353938643261623...
Privilege Escalation
Use become carefully:
- name: Configure system
hosts: servers
become: true
tasks:
- name: Install package
ansible.builtin.apt:
name: nginx
- 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
- name: Set database password
ansible.builtin.shell: |
psql -c "ALTER USER postgres PASSWORD '{{ db_password }}'"
no_log: true
- name: Get API token
ansible.builtin.uri:
url: https://api.example.com/token
method: POST
register: token_response
no_log: true
Inventory Security
[databases]
db1 ansible_host=10.0.1.10 ansible_user=admin ansible_password=secret123
[databases]
db1 ansible_host=10.0.1.10 ansible_user=admin
ansible_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
Module Best Practices
Prefer Specific Modules Over shell/command
- name: Install package
ansible.builtin.shell: apt-get install -y nginx
- name: Install package
ansible.builtin.apt:
name: nginx
state: present
- name: Create directory
ansible.builtin.shell: mkdir -p /opt/app
- name: Create directory
ansible.builtin.file:
path: /opt/app
state: directory
mode: '0755'
Fully Qualified Collection Names (Ansible 2.10+)
- name: Install package
ansible.builtin.apt:
name: nginx
- name: Manage Docker container
community.docker.docker_container:
name: myapp
image: nginx:latest
Common Pitfalls
1. Not Using check_mode
- name: Dangerous operation
ansible.builtin.file:
path: /etc/important-config
state: absent
2. Ignoring Return Codes
- name: Run script
ansible.builtin.shell: /opt/app/script.sh
ignore_errors: true
- 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
- name: Check status
ansible.builtin.shell: systemctl status nginx
- name: Check status
ansible.builtin.shell: systemctl status nginx
changed_when: false
check_mode: false
4. Using shell when command suffices
- name: List files
ansible.builtin.shell: ls /tmp
- 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
---
skip_list:
- yaml[line-length]
warn_list:
- experimental
exclude_paths:
- .cache/
- test/
Naming Conventions
- name: Ensure nginx is installed
- name: Copy nginx configuration
- name: Restart nginx service
- name: nginx
- name: Task 1
- apt: name=nginx
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
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
Converted and distributed by TomeVault — claim your Tome and manage your conversions.