Complete ansible toolkit with generation and validation capabilities
70
87%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
This guide provides comprehensive best practices for writing clean, maintainable, and reliable Ansible playbooks, roles, and collections.
ansible-project/
├── ansible.cfg # Ansible configuration
├── inventory/ # Inventory files
│ ├── production/
│ │ ├── hosts # Production inventory
│ │ └── group_vars/
│ │ └── all.yml
│ └── staging/
│ ├── hosts # Staging inventory
│ └── group_vars/
│ └── all.yml
├── group_vars/ # Group-specific variables
│ ├── all.yml
│ ├── webservers.yml
│ └── databases.yml
├── host_vars/ # Host-specific variables
│ └── server1.yml
├── roles/ # Reusable roles
│ ├── common/
│ ├── webserver/
│ └── database/
├── playbooks/ # Playbooks
│ ├── site.yml # Master playbook
│ ├── webservers.yml
│ └── databases.yml
├── files/ # Static files
├── templates/ # Jinja2 templates
├── vars/ # Additional variables
│ └── external_vars.yml
└── requirements.yml # Collection dependenciesroles/webserver/
├── README.md # Role documentation
├── defaults/
│ └── main.yml # Default variables (lowest precedence)
├── vars/
│ └── main.yml # Role variables (higher precedence)
├── tasks/
│ ├── main.yml # Main task list
│ ├── install.yml # Installation tasks
│ └── configure.yml # Configuration tasks
├── handlers/
│ └── main.yml # Handlers
├── templates/
│ └── nginx.conf.j2 # Template files
├── files/
│ └── index.html # Static files
├── meta/
│ └── main.yml # Role metadata and dependencies
└── molecule/ # Molecule test scenarios
└── default/
├── molecule.yml
├── converge.yml
└── verify.yml# Descriptive, action-oriented names
- name: Install nginx web server
apt:
name: nginx
state: present
- name: Configure nginx virtual host for example.com
template:
src: vhost.conf.j2
dest: /etc/nginx/sites-available/example.com
- name: Enable and start nginx service
systemd:
name: nginx
state: started
enabled: yes
- name: Create application user with limited privileges
user:
name: appuser
system: yes
shell: /bin/false
home: /var/lib/app# Vague, uninformative names
- name: Install package
apt:
name: nginx
- name: Configure
template:
src: vhost.conf.j2
dest: /etc/nginx/sites-available/example.com
- name: Service
systemd:
name: nginx
state: started
# No name at all
- apt:
name: nginx# ✅ Good - Descriptive, namespaced
nginx_version: "1.18.0"
nginx_worker_processes: 4
nginx_worker_connections: 1024
app_database_host: "db.example.com"
app_database_port: 5432
# ❌ Bad - Generic, collision-prone
version: "1.18.0" # Too generic
workers: 4 # Unclear
db: "db.example.com" # VagueUnderstand variable precedence (from lowest to highest):
# defaults/main.yml - Intended to be overridden
---
nginx_port: 80
nginx_user: www-data
nginx_worker_processes: "auto"
# vars/main.yml - Should not be overridden
---
nginx_config_dir: /etc/nginx
nginx_log_dir: /var/log/nginx
nginx_pid_file: /run/nginx.pid# Use default filter for optional variables
- name: Set API endpoint
set_fact:
api_endpoint: "{{ custom_api_endpoint | default('https://api.example.com') }}"
# Use required filter for mandatory variables
- name: Configure database
template:
src: db.conf.j2
dest: /etc/app/database.conf
vars:
db_password: "{{ database_password | required('database_password must be defined') }}"Idempotency means running the same playbook multiple times produces the same result without making unnecessary changes.
# File module - inherently idempotent
- name: Ensure configuration directory exists
file:
path: /etc/myapp
state: directory
mode: '0755'
# Template module - only changes if content differs
- name: Configure application
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
mode: '0644'
# Package module - idempotent
- name: Install required packages
apt:
name:
- nginx
- python3
- git
state: present
# Service module - idempotent
- name: Ensure service is running
systemd:
name: myapp
state: started
enabled: yes# Command/shell without creates/removes
- name: Download file
command: curl -o /tmp/file.tar.gz https://example.com/file.tar.gz
# This runs every time!
# Fix with creates
- name: Download file
command: curl -o /tmp/file.tar.gz https://example.com/file.tar.gz
args:
creates: /tmp/file.tar.gz
# Or better - use get_url module
- name: Download file
get_url:
url: https://example.com/file.tar.gz
dest: /tmp/file.tar.gz
checksum: sha256:abc123...
# Command that always reports changed
- name: Check service status
command: systemctl status myapp
register: service_status
# Always shows as changed!
# Fix with changed_when
- name: Check service status
command: systemctl status myapp
register: service_status
changed_when: false
failed_when: service_status.rc not in [0, 3]# ❌ Bad - Using shell/command
- name: Create directory
shell: mkdir -p /opt/myapp
- name: Install package
command: apt-get install -y nginx
- name: Add line to file
shell: echo "export PATH=$PATH:/opt/bin" >> ~/.bashrc
# ✅ Good - Using appropriate modules
- name: Create directory
file:
path: /opt/myapp
state: directory
mode: '0755'
- name: Install package
apt:
name: nginx
state: present
- name: Add line to file
lineinfile:
path: ~/.bashrc
line: 'export PATH=$PATH:/opt/bin'
create: yes- name: Handle errors gracefully
block:
- name: Attempt risky operation
command: /usr/local/bin/risky-operation.sh
register: result
- name: Process successful result
debug:
msg: "Operation succeeded: {{ result.stdout }}"
rescue:
- name: Handle failure
debug:
msg: "Operation failed, applying fallback"
- name: Apply fallback configuration
copy:
src: fallback.conf
dest: /etc/app/config.conf
always:
- name: Cleanup temporary files
file:
path: /tmp/operation.lock
state: absent# Custom failure conditions
- name: Check disk space
shell: df -h / | tail -1 | awk '{print $5}' | sed 's/%//'
register: disk_usage
failed_when: disk_usage.stdout | int > 90
# Custom changed conditions
- name: Verify configuration
command: /usr/local/bin/check-config.sh
register: config_check
changed_when: false
failed_when: config_check.rc != 0
# Multiple conditions
- name: Run healthcheck
uri:
url: http://localhost:8080/health
method: GET
register: health
failed_when:
- health.status != 200
- "'healthy' not in health.json.status"# Only when failure is acceptable
- name: Try to stop service (may not exist)
systemd:
name: old-service
state: stopped
ignore_errors: yes
# Better approach - check first
- name: Check if service exists
systemd:
name: old-service
register: service_status
failed_when: false
- name: Stop service if it exists
systemd:
name: old-service
state: stopped
when: service_status.status.ActiveState is defined# Simple condition
- name: Install Apache (Debian)
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
# Multiple conditions (AND)
- name: Install package on Ubuntu 20.04
apt:
name: package
state: present
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version == "20.04"
# OR conditions
- name: Install on RHEL or CentOS
yum:
name: package
state: present
when: ansible_distribution == "RedHat" or ansible_distribution == "CentOS"
# Complex conditions
- name: Configure firewall
ufw:
rule: allow
port: '443'
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true) | bool
- ansible_virtualization_type != "docker"# Simple loop
- name: Install packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- python3
- git
# Loop with hash
- name: Create users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: 'alice', groups: 'developers' }
- { name: 'bob', groups: 'operators' }
# Loop with dict
- name: Create directories
file:
path: "{{ item.path }}"
state: directory
mode: "{{ item.mode }}"
loop:
- { path: '/opt/app', mode: '0755' }
- { path: '/var/log/app', mode: '0755' }
- { path: '/etc/app', mode: '0750' }
# Loop with conditional
- name: Install debug tools (dev only)
apt:
name: "{{ item }}"
state: present
loop:
- strace
- tcpdump
- gdb
when: environment == "development"{# templates/nginx.conf.j2 #}
{# Use comments to explain complex logic #}
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid {{ nginx_pid_file }};
{# Conditionals in templates #}
{% if nginx_enable_ssl %}
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
{% endif %}
{# Loops in templates #}
{% for vhost in nginx_vhosts %}
server {
listen {{ vhost.port }};
server_name {{ vhost.server_name }};
root {{ vhost.document_root }};
{% if vhost.ssl_enabled | default(false) %}
ssl_certificate {{ vhost.ssl_cert }};
ssl_certificate_key {{ vhost.ssl_key }};
{% endif %}
}
{% endfor %}
{# Filters #}
upstream_servers = {{ backend_servers | join(',') }}
max_connections = {{ max_connections | default(1024) }}# String manipulation
- debug:
msg: "{{ 'hello' | upper }}" # HELLO
msg: "{{ 'HELLO' | lower }}" # hello
msg: "{{ ' hello ' | trim }}" # hello
# List operations
- debug:
msg: "{{ [1,2,3] | first }}" # 1
msg: "{{ [1,2,3] | last }}" # 3
msg: "{{ [1,2,3] | length }}" # 3
msg: "{{ [1,2,3] | join(',') }}" # 1,2,3
# Default values
- debug:
msg: "{{ undefined_var | default('default_value') }}"
# Type conversion
- debug:
msg: "{{ '123' | int }}" # 123
msg: "{{ 'true' | bool }}" # True
# JSON and YAML
- debug:
msg: "{{ my_dict | to_json }}"
msg: "{{ my_dict | to_nice_json }}"
msg: "{{ my_dict | to_yaml }}"---
- name: Configure web server
hosts: webservers
tasks:
- name: Install nginx
apt:
name: nginx
tags:
- packages
- nginx
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags:
- configuration
- nginx
- name: Start nginx
systemd:
name: nginx
state: started
tags:
- services
- nginx
- name: Configure firewall
ufw:
rule: allow
port: '80'
tags:
- security
- firewall# Run only nginx tasks
ansible-playbook site.yml --tags nginx
# Run configuration tasks only
ansible-playbook site.yml --tags configuration
# Skip certain tags
ansible-playbook site.yml --skip-tags packages
# Multiple tags
ansible-playbook site.yml --tags "nginx,firewall"# tasks/main.yml
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- Validate nginx configuration
- Restart nginx
- name: Add virtual host
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ vhost_name }}"
notify:
- Reload nginx
# handlers/main.yml
- name: Validate nginx configuration
command: nginx -t
changed_when: false
- name: Restart nginx
systemd:
name: nginx
state: restarted
- name: Reload nginx
systemd:
name: nginx
state: reloadedmeta: flush_handlers to run immediately# Task that supports check mode naturally (file module)
- name: Create directory
file:
path: /opt/myapp
state: directory
# Task that doesn't support check mode, but can run anyway
- name: Check service status
command: systemctl status myapp
check_mode: no # Always run, even in check mode
changed_when: false
# Task that should be skipped in check mode
- name: Apply complex changes
command: /usr/local/bin/complex-script.sh
when: not ansible_check_mode# Run in check mode (dry-run)
ansible-playbook site.yml --check
# Check mode with diff (show changes)
ansible-playbook site.yml --check --diff
# See what would change
ansible-playbook site.yml --check --diff | grep -A 10 "changed:"---
# site.yml - Master playbook for deploying web application
#
# This playbook:
# - Configures common settings on all hosts
# - Deploys web servers
# - Configures databases
# - Sets up load balancers
#
# Usage:
# ansible-playbook -i inventory/production site.yml
#
# Tags:
# - common: Common configuration tasks
# - webserver: Web server setup
# - database: Database configuration
#
# Variables (see group_vars/all.yml):
# - app_version: Application version to deploy
# - environment: Environment name (production/staging)
- name: Configure common settings
hosts: all
roles:
- common
tags: common
- name: Deploy web servers
hosts: webservers
roles:
- webserver
tags: webserver# Webserver Role
## Description
Installs and configures Nginx web server with virtual hosts and SSL support.
## Requirements
- Ansible >= 2.9
- Supported OS: Ubuntu 20.04, Debian 11
## Role Variables
### Required Variables
- `nginx_vhosts`: List of virtual hosts to configure (see example)
### Optional Variables
- `nginx_worker_processes`: Number of worker processes (default: auto)
- `nginx_worker_connections`: Max connections per worker (default: 1024)
- `nginx_enable_ssl`: Enable SSL support (default: false)
## Dependencies
None
## Example Playbook
```yaml
- hosts: webservers
roles:
- role: webserver
vars:
nginx_vhosts:
- server_name: example.com
port: 80
document_root: /var/www/exampleMIT
Your Name
## Testing Best Practices
See the molecule configuration and testing section in the main skill.md for comprehensive testing guidance.
## Performance Tips
1. **Use pipelining** in ansible.cfg
```ini
[ssh_connection]
pipelining = TrueEnable fact caching
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400Limit fact gathering
- hosts: all
gather_facts: no # Don't gather if not neededUse async for long-running tasks
- name: Long running task
command: /usr/local/bin/long-task.sh
async: 3600
poll: 0
register: long_task
- name: Check on long task
async_status:
jid: "{{ long_task.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 30