Lesson 6 of 9 · 24m
Jinja2 Templates and Vault Secrets
template vs copy vs lineinfile, the Jinja2 idioms that show up in every config file, and how vault encryption keeps secrets in git without leaking them.
Someone pasted a Slack export into a retro doc once: "prod was down because the deploy script wrote the staging DB password into the prod config." The script was a sed one-liner. Templates exist so that class of bug — wrong file, wrong values, wrong environment — becomes structurally impossible, and vault exists so the secrets in those templates can live next to the code without leaking.
template: render Jinja2 into place
- name: Ship app config
ansible.builtin.template:
src: app.env.j2 # resolves from templates/ or the role's templates/
dest: /opt/acme/app.env
owner: acme
group: acme
mode: "0600"
validate: /usr/sbin/app --check-config %s # test the rendered file before installing
notify: Restart acme-appThe validate: arg is criminally underused — sshd -t, nginx -t, visudo -cf all work the same way: render to temp, validate, install only on pass. A bad config never lands.
{# templates/app.env.j2 #}
# Managed by Ansible — manual edits will be overwritten
DATABASE_URL=postgres://{{ db_user }}:{{ db_password }}@{{ db_host }}:5432/{{ db_name }}
LOG_LEVEL={{ log_level | default('info') }}
ALLOWED_HOSTS={{ allowed_hosts | join(',') }}
{% for host in groups['metrics'] %}
METRICS_PEER_{{ loop.index }}={{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }}
{% endfor %}The idioms worth memorizing: | default('x') for optional knobs, | join(',') for lists, {% for %} + loop.index for peer enumeration, groups[]/hostvars[] for cross-host values. {# comments #} don't render; // and # inside the file do.
copy vs lineinfile vs template — pick by ownership
template:— you own the whole file. Rendered from Jinja, drift-proof.copy: content:— you own the whole file, it's short or static.lineinfile:/blockinfile:— you own one line or block inside a file other tools also touch (/etc/fstab, sudoers fragments). It manages marked regions; it's not a merge.
Using lineinfile to patch a file you actually own is a smell — just template it and put the "managed by Ansible" header on it.
vault: secrets that can live in git
ansible-vault encrypt group_vars/dbservers/vault.yml # encrypt a whole file
ansible-vault encrypt_string 's3cr3t' --name db_password # one var, inline
ansible-vault edit group_vars/dbservers/vault.yml # decrypt → edit → re-encrypt
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass.txtThe file pattern that survives team growth: every group_vars/<group>/ gets a vars.yml (plaintext) and a vault.yml (encrypted). Plaintext vars reference vaulted vars — db_password: "{{ vault_db_password }}" — so the name is greppable but the value never is. Vault encrypts at rest; once decrypted in memory it's indistinguishable from any other variable.
Field notes
no_log: trueon tasks that handle secrets — otherwise the decrypted value prints in-voutput and lands in CI logs.- Vault IDs (
--vault-id prod@~/.vault_prod) let one repo hold per-environment passwords; Ansible prompts/loads the right one. ansible-vault rekeychanges the vault password; rotating a secret value isedit. Never commit the password file —.vault_pass.txtbelongs in.gitignoreon day one.