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 in

Vault

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.yml and 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: true to a task that handles a secret so the value is not echoed into logs.
  • --vault-id dev@prompt manages 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
SymptomUsual causeFix
Always changedA shell task with no guardUse a module, or add creates: or changed_when: false
File rewritten every runTemplate output contains something volatileRemove the timestamp, or set ansible_managed
Handler fires every runThe notifying task always reports changedFix the notifying task first
Task skipped unexpectedlywhen tests a fact that is undefinedUse 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.

Playbooks and modules Inventories and ad-hoc commands

Last refreshed 2026-09-18.