Roles, Vault and idempotency
Package reusable automation as roles, keep secrets encrypted with Vault, and write tasks that converge instead of repeating themselves.
Roles
A role is a directory with a fixed layout, so Ansible knows where to look without being told.
ansible-galaxy init roles/nginx
# roles/nginx/
# defaults/main.yml lowest-precedence variables, meant to be overridden
# vars/main.yml higher-precedence variables, rarely changed
# tasks/main.yml the work
# handlers/main.yml notified handlers
# templates/ Jinja2 sources for the template module
# files/ static files for the copy module
# meta/main.yml dependencies on other roles# roles/nginx/tasks/main.yml
- name: Install nginx
package:
name: nginx
state: present
notify: Reload nginx
- name: Write the vhost
template:
src: vhost.conf.j2
dest: "/etc/nginx/conf.d/{{ nginx_server_name }}.conf"
mode: "0644"
notify: Reload nginx# site.yml
- hosts: web
become: true
roles:
- role: nginx
vars:
nginx_server_name: shop.example.com
- common # order in this list is the order they run inVault
ansible-vault encrypts a file with AES-256 so it can sit next to the rest of your code in version control. Only the encrypted blob is committed; the password is supplied at run time.
ansible-vault create group_vars/prod/vault.yml
ansible-vault encrypt secrets.yml
ansible-vault edit group_vars/prod/vault.yml
ansible-vault view secrets.yml
ansible-vault rekey secrets.yml # change the password
# run with a password file, chmod 600 and never committed
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# or be prompted instead
ansible-playbook site.yml --ask-vault-pass
# encrypt one value rather than a whole file
ansible-vault encrypt_string 'hunter2' --name 'db_password'- Keep secrets in
group_vars/<group>/vault.ymland refer to them from ordinary variables, so no task ever names a file path. - Encrypted variables are normal variables at run time:
{{ db_password }}works unchanged. - Add
no_log: trueto a task that handles a secret so the value is not echoed into logs. --vault-id dev@promptmanages several vault passwords, one per environment.
Idempotency and check mode
A well-written playbook converges: running it twice leaves the same end state, and the second run reports changed=0. That number is your main quality signal. A run that always reports changes is hiding drift or busywork.
# converge twice; the second run should report changed=0
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml
# rehearse on one host and show the diffs
ansible-playbook -i inventory.ini site.yml --check --diff --limit web1| Symptom | Usual cause | Fix |
|---|---|---|
Always changed | A shell task with no guard | Use a module, or add creates: or changed_when: false |
| File rewritten every run | Template output contains something volatile | Remove the timestamp, or set ansible_managed |
| Handler fires every run | The notifying task always reports changed | Fix the notifying task first |
| Task skipped unexpectedly | when tests a fact that is undefined | Use ansible_facts[...] and check is defined |
⚠️
Never run a brand-new playbook against production first. Run it with
--check --diff --limit on one host, then on the whole group, then everywhere — and make sure the inventory and the previous release let you roll back.FAQ
Where should a variable live?
Role defaults for anything a caller may change, group_vars for environment-specific values, host_vars for genuine per-host exceptions, and extra vars (
-e) for one-off overrides — because extra vars win over everything else.Is an encrypted vault file safe to commit?
Yes, provided the password is strong and stored elsewhere: a password manager, a CI secret, or an interactive prompt. Committing the file and its password together defeats the purpose.
Related
Playbooks and modules Inventories and ad-hoc commands
Last refreshed 2026-09-18.