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.

·3 min read
ansibleautomationhomelab

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.