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.
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:
- postgresqlAlways 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.
# 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.txtLint, 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/commandto preserve idempotency; addchanged_when/createswhen shell is unavoidable. - Encrypt all secrets with Ansible Vault and keep the vault password itself out of version control.
- Run
ansible-lintand--check --diffdry 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.ymlto 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
1. What is the primary benefit of organizing automation into roles instead of one large playbook?
2. Why should the `service` module generally be preferred over `shell: systemctl restart nginx`?
3. What encryption does Ansible Vault use to protect sensitive variables?
4. What does running a playbook with `--check --diff` accomplish?
5. What tool is commonly used to test that an Ansible role converges correctly and remains idempotent using disposable containers?
Was this page helpful?
You May Also Like
Ansible vs Other Config Management Tools
A practical comparison of Ansible against Puppet, Chef, SaltStack, and Terraform, covering architecture, agent requirements, and when to pick each.
Deploying a Web App with Ansible
A hands-on walkthrough of provisioning a server, installing dependencies, and deploying a web application with an Ansible playbook, including zero-downtime rollout patterns.
Ansible Quick Reference
A condensed cheat sheet of the most-used Ansible CLI commands, playbook keywords, and module patterns for day-to-day automation work.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics