Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · DevOps

Ansible: Roles, Variables and Vault

Organise playbooks into roles with defaults, handlers and templates, understand variable precedence, encrypt secrets with Ansible Vault and test roles before they reach production.

4 min read

  • ansible-core 2.15
  • Ansible 8
  • Ubuntu 22.04 LTS
  • ansible-lint

A single playbook is fine for a demo. After a few services it becomes a long file that mixes web servers, database tuning and user accounts, and nobody dares to touch it. Roles are Ansible’s unit of reuse: a directory with a fixed layout that bundles tasks, templates, files, defaults and handlers for one job such as “configure nginx”.

Versions in use on this date. ansible-core 2.15 (released May 2023, needs Python 3.9 or newer on the control node) and the community package Ansible 8, which bundles it with a set of collections. Ubuntu 22.04 provides Python 3.10 on managed hosts. Use fully qualified module names (ansible.builtin.apt) so playbooks keep working as modules move between collections.

How the parts combine

flowchart TB
  PB[site.yml<br/>hosts web, roles nginx and app] --> R
  subgraph R[Role nginx]
    D[defaults main.yml<br/>lowest precedence, safe knobs]
    T[tasks main.yml]
    TPL[templates site.conf.j2]
    H[handlers main.yml<br/>reload nginx]
    M[meta main.yml<br/>role dependencies]
  end
  GV[group_vars and host_vars<br/>environment values] -->|overrides defaults| R
  VAULT[group_vars prod vault.yml<br/>encrypted with Ansible Vault] -->|secrets as vault_ variables| GV
  EV[extra vars -e on command line] -->|highest precedence| R
  T -->|template changed| H
  R --> HOSTS[Managed hosts over SSH]

Create a role

ansible-galaxy init roles/nginx
roles/nginx/
  defaults/main.yml      # variables users are expected to override
  vars/main.yml          # internal constants, high precedence, rarely used
  tasks/main.yml
  handlers/main.yml
  templates/site.conf.j2
  files/                 # static files to copy
  meta/main.yml          # dependencies and metadata

defaults/main.yml:

nginx_server_name: example.com
nginx_listen_port: 80
nginx_upstream: 127.0.0.1:8080
nginx_worker_connections: 1024

tasks/main.yml:

- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true
    cache_valid_time: 3600

- name: Render site configuration
  ansible.builtin.template:
    src: site.conf.j2
    dest: /etc/nginx/conf.d/site.conf
    owner: root
    group: root
    mode: "0644"
  notify: Validate and reload nginx

- name: Ensure nginx is enabled and running
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

handlers/main.yml runs only when the template changed, once, at the end of the play:

- name: Validate and reload nginx
  ansible.builtin.command: nginx -t
  changed_when: false
  notify: Reload nginx

- name: Reload nginx
  ansible.builtin.service:
    name: nginx
    state: reloaded

templates/site.conf.j2:

server {
  listen {{ nginx_listen_port }};
  server_name {{ nginx_server_name }};
  location / {
    proxy_pass http://{{ nginx_upstream }};
    proxy_set_header Host $host;
  }
}

Use the role in a play:

- hosts: web
  become: true
  roles:
    - role: nginx
      vars:
        nginx_server_name: shop.example.com

Variables and precedence

Ansible has more than 20 places where a variable can be set. The rule of thumb that keeps things understandable: role defaults (lowest) are overridden by group_vars and host_vars for the environment, which are overridden by play vars and finally extra vars (-e), which always win.

inventory/
  production.ini
  group_vars/
    all.yml            # values common to all hosts
    web.yml
    prod/
      vars.yml         # plain environment values
      vault.yml        # encrypted
  host_vars/
    web-1.yml

Name variables with the role prefix (nginx_...) to avoid collisions, and put only safe, documented knobs in defaults. Do not use vars/ for settings users should change, because it overrides group variables and surprises people.

Secrets with Vault

ansible-vault create inventory/group_vars/prod/vault.yml
ansible-vault edit inventory/group_vars/prod/vault.yml
ansible-vault encrypt_string 'S3cr3t!' --name 'vault_db_password'     # encrypt one value inline
ansible-playbook site.yml -i inventory/production.ini --ask-vault-pass
ansible-playbook site.yml -i inventory/production.ini --vault-id prod@~/.ansible/vault-prod

Reference the vault variable from a plain file so searches still show where each secret is used:

# group_vars/prod/vars.yml
db_password: "{{ vault_db_password }}"

and set no_log: true on tasks that handle secrets so they are not printed. Keep the vault password in a password manager or CI secret store, never in the repository, and use vault IDs to use different passwords per environment so a developer can edit staging but not production. Rotate by re-encrypting (ansible-vault rekey). For larger teams, consider fetching secrets at run time from AWS Secrets Manager or HashiCorp Vault with a lookup plugin rather than storing encrypted files.

Quality practices

  1. Collections: pin versions in requirements.yml (ansible-galaxy collection install -r requirements.yml) so runs are reproducible.
  2. Lint: ansible-lint and yamllint in CI catch deprecated syntax, missing names and risky shell usage.
  3. Test: try roles on a throwaway container or VM; Molecule automates create, converge, idempotence check and verify.
  4. Idempotence check: run the playbook twice; the second run should report changed=0.
  5. Dry run in production: --check --diff, then --limit to one host as a canary, then everyone with serial.
  6. Tags and --start-at-task help with long plays during debugging.
  7. Documentation: a README.md in each role listing variables and an example.

Common mistakes

  • Putting secrets in defaults or committing an unencrypted vault file; check with a pre-commit hook that fails on $ANSIBLE_VAULT headers missing from vault.yml.
  • Using shell for things a module does; the task is then never idempotent.
  • Role dependencies hidden in meta that nobody expects; document them.
  • Overriding too much with -e; it makes runs hard to reproduce. Prefer variables in version control.