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

PracticeWhy
FQCN modulesAvoids name conflicts
Name every taskReadable output
Use rolesReusability
Vault for secretsSecurity
Idempotent tasksSafe to re-run
Handlers for restartsRestart only when needed
Lint playbooksCatch issues early
TagsSelective execution
Check modeSafe testing
group_vars/host_varsClean variable management

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.