Ansible Best Practices¶
Role Design¶
One role, one concern. If you are tempted to add something to a role, ask: "Does every node this role applies to need this?" If no, it belongs in another role.
Defaults over vars. Put everything in defaults/main.yml. Only use
vars/main.yml for constants that should never be overridden.
Never hardcode values in tasks. Every IP, port, username, and path must be a variable. Tasks should read like instructions, not config files.
Idempotent always. Running the playbook 10 times must produce the same result
as running it once. Use state: present, never raw shell commands that don't check first.
Task Writing¶
Always name every task. The name: field is mandatory.
| Bad | Good |
|---|---|
- name: Install packages |
- name: Install base system packages — Debian |
- name: Restart service |
- name: Restart timesyncd after NTP config change |
Use modules, not shell. The apt module over shell: apt-get install.
Modules are idempotent. Shell commands usually are not.
OS conditions. Use when: ansible_os_family == "Debian" to keep roles portable.
Handlers for service restarts. Never restart a service in tasks directly.
# tasks/main.yml
- name: Configure NTP
template:
src: timesyncd.conf.j2
dest: /etc/systemd/timesyncd.conf
notify: restart timesyncd # <-- correct
# handlers/main.yml
- name: restart timesyncd
systemd:
name: systemd-timesyncd
state: restarted
Tags on every task. Allows partial playbook runs.
- name: Install base packages
apt:
name: "{{ base_packages }}"
state: present
tags: [common, packages, baseline]
Variables¶
Variable priority order (lowest to highest):
This means inventories/ot/group_vars/all.yml automatically overrides
roles/common/defaults/main.yml. This is intentional — use it.
Never store secrets in vars. Passwords, tokens, and API keys come from Infisical at runtime. Never from git.
Prefix role-specific vars to avoid collisions between roles.
# Good
common_admin_user: operator
common_ntp_servers:
- 0.ca.pool.ntp.org
# Bad
admin_user: operator # could collide with another role
Security¶
no_log: true on sensitive tasks.
Least privilege. Only use become: true on tasks that need root.
Not at the play level if you can avoid it.
SSH keys only. Never password auth in inventory.
File Structure¶
roles/my_role/
defaults/main.yml # overridable defaults
vars/main.yml # non-overridable constants (rare)
tasks/main.yml # imports task files
tasks/install.yml # split tasks by concern if > 50 lines
tasks/configure.yml
handlers/main.yml # service restarts
templates/ # Jinja2 templates (.j2)
meta/main.yml # role dependencies
Split tasks if over 50 lines:
# tasks/main.yml
- import_tasks: install.yml
tags: [install]
- import_tasks: configure.yml
tags: [configure]
- import_tasks: service.yml
tags: [service]
Role Dependencies¶
Declare dependencies in meta/main.yml, not in playbooks:
This ensures common always runs before docker, regardless of playbook order.
Testing with Molecule¶
Every role gets a Molecule test. See Ansible Molecule Guide.
The One Rule¶
If you have to think about what a task does when reading it 6 months later, rewrite it.