100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
DevOps

Ansible Best Practices

Field-tested conventions for structuring playbooks, roles, and inventories so Ansible automation stays maintainable, secure, and idempotent as it scales.

PracticeIntermediate10 min readJul 10, 2026
Analogies

Structure Playbooks with Roles, Not Monoliths

The most common mistake in growing Ansible codebases is writing one giant playbook with dozens of tasks instead of decomposing work into roles. A role is a self-contained directory structure (tasks, handlers, templates, files, vars, defaults) that packages a single responsibility, like 'nginx' or 'postgresql', and can be reused across playbooks and even shared publicly via Ansible Galaxy. Following the standard roles/<role_name>/{tasks,handlers,templates,files,vars,defaults}/main.yml layout means anyone opening the repo for the first time immediately understands where to look for a given piece of configuration.

🏏

Cricket analogy: A monolithic playbook is like one player trying to bowl, bat, wicket-keep, and captain simultaneously, whereas roles are like a properly composed XI where each specialist — the opening bowler, the finisher, the keeper — has one clearly defined job, the way Rohit Sharma opens while Jasprit Bumrah closes out overs.

bash
roles/
  nginx/
    tasks/main.yml
    handlers/main.yml
    templates/nginx.conf.j2
    defaults/main.yml
    vars/main.yml
  postgresql/
    tasks/main.yml
    defaults/main.yml

site.yml:
---
- hosts: webservers
  roles:
    - nginx
- hosts: dbservers
  roles:
    - postgresql

Always Write Idempotent, Declarative Tasks

Idempotency means running a playbook twice produces the same end state as running it once, with the second run reporting no changes if nothing needs to change. Ansible's built-in modules like apt, service, and template are idempotent by design, but the moment you reach for the shell or command module to run an arbitrary script, you lose that guarantee unless you explicitly add a creates, removes, or changed_when condition. Best practice is to always prefer a purpose-built module over shell/command whenever one exists, because idempotent tasks make playbooks safe to re-run in production without fear of duplicating work or causing unintended side effects.

🏏

Cricket analogy: Idempotent tasks are like DRS (Decision Review System) reviews — asking for the same review twice on an unchanged delivery gives you the same unchanged result, whereas a raw shell command is like an umpire's un-reviewable on-field call that could differ each time you ask.

A common bug is using shell: systemctl restart nginx inside a task, which always reports 'changed' on every run even when nothing changed, breaking idempotency and polluting change reports. Use the service or systemd module instead, or add changed_when: false to shell tasks that are read-only checks.

Secure Secrets with Ansible Vault

Never commit plaintext passwords, API keys, or TLS private keys into a playbook or inventory file. Ansible Vault encrypts sensitive variables or entire files using AES256, and encrypted content can live safely in the same Git repository as the rest of your automation. Best practice is to keep a single vault.yml per environment (e.g. group_vars/production/vault.yml), reference vault variables indirectly from a separate plaintext vars.yml using a naming convention like vault_db_password, and store the vault password itself in a secrets manager or CI secret store rather than a file checked into version control.

🏏

Cricket analogy: Ansible Vault is like a team's confidential strategy notebook kept in a locked dressing-room safe — the match-day XI selection (the rest of the repo) is shared openly, but the specific bowling plan against a batter like Virat Kohli stays encrypted until it's actually needed.

bash
# Encrypt a secrets file
ansible-vault encrypt group_vars/production/vault.yml

# Edit it later without decrypting to disk
ansible-vault edit group_vars/production/vault.yml

# Run a playbook, prompting for the vault password
ansible-playbook site.yml --ask-vault-pass

# Or point at a password file kept outside version control
ansible-playbook site.yml --vault-password-file ~/.vault_pass.txt

Lint, Tag, and Test Before Production

Run ansible-lint on every playbook to catch style issues and known anti-patterns (like using shell when a module exists, or missing become: true where privilege escalation is needed) before they reach a pull request. Tag tasks and roles meaningfully (--tags deploy, --tags config) so operators can run a narrow slice of a large playbook without executing everything, and use --check (dry-run) combined with --diff to preview exactly what would change against a live system before applying it for real. For genuine pre-deployment testing, tools like Molecule spin up disposable Docker containers to verify a role converges correctly and stays idempotent on a second run.

🏏

Cricket analogy: Running ansible-lint is like a net session before the actual match, catching technical flaws in a batter's stance before they cost runs against real bowling, the way coaches spot and fix a flawed trigger movement in the nets.

Pin exact module and collection versions in requirements.yml and commit a ansible.cfg with explicit roles_path and collections_path settings. Without pinning, an ansible-galaxy collection install on a teammate's machine can silently pull a newer collection version with breaking changes, causing 'works on my machine' failures.

  • Decompose playbooks into roles with the standard tasks/handlers/templates/defaults directory layout for reuse and clarity.
  • Prefer purpose-built modules over shell/command to preserve idempotency; add changed_when/creates when shell is unavoidable.
  • Encrypt all secrets with Ansible Vault and keep the vault password itself out of version control.
  • Run ansible-lint and --check --diff dry runs before applying changes to production.
  • Use meaningful tags so operators can target a narrow slice of a large playbook instead of running everything.
  • Pin role and collection versions in requirements.yml to keep runs reproducible across machines and CI.
  • Test roles with Molecule against disposable containers to verify convergence and idempotency before shipping.

Practice what you learned

Was this page helpful?

Topics covered

#DevOps#AnsibleStudyNotes#AnsibleBestPractices#Ansible#Structure#Playbooks#Roles#StudyNotes#SkillVeris#ExamPrep