Ansible Best Practices — Write Clean, Maintainable Playbooks
These practices come from real-world production experience managing hundreds of servers with Ansible. Follow them to build automation that scales.
Directory Structure
ansible-project/
├── ansible.cfg
├── inventory/
│ ├── production/
│ │ ├── hosts
│ │ └── group_vars/
│ │ ├── all.yml
│ │ └── webservers.yml
│ └── staging/
│ ├── hosts
│ └── group_vars/
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ └── dbservers.yml
├── roles/
│ ├── common/
│ ├── nginx/
│ └── postgresql/
└── files/
1. Use FQCN for All Modules
Always use Fully Qualified Collection Names:
# ✅ Good
- ansible.builtin.copy:
src: file.conf
dest: /etc/app/file.conf
# ❌ Bad — ambiguous
- copy:
src: file.conf
dest: /etc/app/file.conf
2. Name Every Task
# ✅ Good
- name: Install nginx web server
ansible.builtin.apt:
name: nginx
state: present
# ❌ Bad — unnamed task
- ansible.builtin.apt:
name: nginx
state: present
3. Use Roles for Reusable Logic
# playbooks/site.yml
---
- hosts: webservers
roles:
- common
- nginx
- certbot
- hosts: dbservers
roles:
- common
- postgresql
4. Keep Secrets in Vault
# group_vars/all/vault.yml (encrypted)
vault_db_password: "s3cur3p4ss"
vault_api_key: "abc123"
# group_vars/all/vars.yml (references vault)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
Prefix vault variables with vault_ to make them easy to identify.
5. Write Idempotent Tasks
# ✅ Good — idempotent, declarative
- name: Ensure nginx is installed
ansible.builtin.apt:
name: nginx
state: present
# ❌ Bad — not idempotent
- name: Install nginx
ansible.builtin.command: apt-get install -y nginx
6. Use Handlers for Service Restarts
tasks:
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
7. Lint Your Playbooks
# Install ansible-lint
pip install ansible-lint
# Run linting
ansible-lint playbooks/site.yml
8. Use Tags for Selective Execution
- name: Install packages
ansible.builtin.apt:
name: nginx
tags: [packages, nginx]
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags: [config, nginx]
ansible-playbook site.yml --tags config
ansible-playbook site.yml --skip-tags packages
9. Test with Check Mode
# Dry run — shows what would change
ansible-playbook site.yml --check --diff
10. Use group_vars and host_vars
inventory/production/
├── hosts
├── group_vars/
│ ├── all.yml # All hosts
│ ├── webservers.yml # Web server group
│ └── dbservers.yml # Database group
└── host_vars/
└── special-server.yml # Single host overrides
Quick Reference
| Practice | Why |
|---|---|
| FQCN modules | Avoids name conflicts |
| Name every task | Readable output |
| Use roles | Reusability |
| Vault for secrets | Security |
| Idempotent tasks | Safe to re-run |
| Handlers for restarts | Restart only when needed |
| Lint playbooks | Catch issues early |
| Tags | Selective execution |
| Check mode | Safe testing |
| group_vars/host_vars | Clean variable management |
Related Articles
- FQCN in Ansible — Fully Qualified Collection Names
- Ansible Vault — Encrypt secrets
- Ansible Dry Run — Check and diff mode
- Ansible Error Handling — Rescue and ignore_errors
- Ansible Training — Courses and certifications
- Ansible Configuration Drift — Detect and fix drift
Conclusion
Good Ansible practices aren't optional at scale — they're the difference between automation that saves time and automation that creates problems. Start with FQCN, naming, and roles, then adopt the rest incrementally.