Ansible: From Ad-Hoc Commands to Roles
How to structure Ansible as your homelab grows — ad-hoc commands, then playbooks, then roles, then collections. With signs you've outgrown each stage.
Every Ansible journey passes through the same four stages. Recognising where you are keeps the tooling from becoming its own burden.
Stage 1: Ad-Hoc Commands
You have one server and one task. Use ansible directly:
ansible all -i host, -m apt -a "name=htop state=present" --become
Note the trailing comma after the host — that’s how you tell Ansible the inventory is a single host rather than a filename. This is the kind of detail that costs you twenty minutes the first time.
Stage 2: Playbooks
The third time you ran the same ad-hoc command, it became a playbook.
- hosts: all
become: true
tasks:
- name: Install baseline packages
ansible.builtin.apt:
name:
- htop
- tmux
- git
state: present
Run it with ansible-playbook -i inventory.ini site.yml. At this stage a
single site.yml is fine.
Stage 3: Roles
The signs you’ve outgrown a monolithic playbook:
- The file is longer than 200 lines.
- You have
when: inventory_hostname == ...checks. - You’re copy-pasting task blocks between playbooks.
Roles give you structure: tasks/, handlers/, defaults/, vars/,
templates/, files/. Create them with ansible-galaxy init rather than by
hand — the directory structure matters.
roles/
baseline/
tasks/main.yml
defaults/main.yml
Then in your playbook:
- hosts: all
roles:
- baseline
- docker
The key insight: defaults/ is for variables the role author thinks are
reasonable; vars/ is for variables the role author thinks are required.
External callers override defaults/, not vars/.
Stage 4: Collections
When you’re publishing roles or sharing them between homelabs, collections are the unit of distribution. A collection packages roles, modules, playbooks, and plugins under a namespace:
ansible-galaxy collection init homelab.base
Collections are what you install with ansible-galaxy collection install.
The format is namespace.collection. If you ever publish to Galaxy, this is
the contract.
Practical Tips
Idempotency first. Every task should be safe to run twice. The built-in
modules handle this; raw command: calls usually don’t. Prefer the
specialised module.
Templates over lineinfile. ansible.builtin.lineinfile works for a
single line. For anything more complex, ship a template — debugging regex
substitutions at 2am is no fun.
Test with Molecule early. Molecule is Ansible’s test harness. It’s opinionated about role structure, so adopt it when you hit Stage 3 — earlier is painful because there’s no role to test.
Tag everything. Tags let you run a subset of a playbook:
- name: Install packages
ansible.builtin.apt: ...
tags: [packages]
ansible-playbook --tags packages skips everything else. Indispensable for
iteration.
Wherever You Stop
Plenty of homelabs never need Stage 4. That’s fine. The trap is jumping stages because the docs make it look impressive — you end up maintaining a collection structure for ten servers and four roles. Match the tooling to the actual complexity.