Every home lab starts with a wiki page of shell commands and ends with a host that nobody dares rebuild. Ansible is the cheapest way out: no agent, SSH only, and a language (YAML) that your future self can read.
The trap is treating Ansible as remote shell with extra steps. These are the habits that make it actually idempotent.
1. Inventory that mirrors reality
# inventory/hosts.ini
[proxmox]
pve01.lan ansible_host=192.168.10.10
pve02.lan ansible_host=192.168.10.11
[docker]
host01.lan ansible_host=192.168.10.20
host02.lan ansible_host=192.168.10.21
[nas]
truenas.lan ansible_host=192.168.10.30
[all:vars]
ansible_user=ansible
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=accept-new'
Role groups (proxmox, docker, nas) let you target by *purpose*, which is the only grouping that survives hardware changes. Group by hardware and you end up with [intel_nuc], which means nothing in a year.
2. One playbook per concern
roles/
base/ # users, ssh daemon, sysctl, packages
docker_host/ # docker engine, compose plugin, log rotation
backup_agent/ # restic + systemd timer
site-base.yml
site-docker.yml
site-base.yml runs everywhere:
- hosts: all
become: true
roles:
- base
- hosts: docker
become: true
roles:
- docker_host
- backup_agent
Splitting by concern is what makes --tags and --check usable. One 900-line site.yml means every run touches everything, including on the hosts you were not thinking about.
3. Write tasks that can run twice
- name: ensure docker apt key exists
ansible.builtin.get_url:
url: https://download.docker.com/linux/debian/gpg
dest: /etc/apt/keyrings/docker.asc
mode: "0644"
args:
creates: /etc/apt/keyrings/docker.asc
- name: add docker apt repository
ansible.builtin.apt_repository:
repo: "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable"
filename: docker
state: present
update_cache: true
- name: install docker packages
ansible.builtin.apt:
name: [docker-ce, docker-ce-cli, containerd.io, docker-compose-plugin]
state: present
Use modules, not command:. A command: task without creates: or changed_when: reports changed on every run, which trains you to ignore the output — the single most common reason hand-rolled Ansible stops being trustworthy.
When you genuinely need a command, be explicit:
- name: enable ip forwarding
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: "1"
state: present
sysctl_file: /etc/sysctl.d/99-forward.conf
4. Make check mode meaningful
ansible-playbook -i inventory/hosts.ini site-docker.yml --check --diff
ansible-playbook -i inventory/hosts.ini site-docker.yml --check --limit host02.lan
Two rules keep --check honest:
- Never skip check mode with
check_mode: falseunless the task is a read (astat, aslurp). A task that reports "changed" in check mode but does nothing is worse than useless. --diffshows the config drift Ansible is about to reconcile, which is the whole reason to use it rather than a shell script.
Run --check in pre-commit on the playbooks you touched. It is not perfect — modules without check support will skip — but it catches the typo that would otherwise restart the wrong service.
5. Safety rails
# ansible.cfg
[defaults]
interpreter_python = auto_silent
forks = 10
timeout = 30
deprecation_warnings = False
[privilege_escalation]
become_method = sudo
And structurally:
- A dedicated
ansibleuser with NOPASSWD sudo, andPermitRootLogin no. Ansible logging in as root hides which changes were automated. --limitfor everything interactive. A bareansible-playbookon a fresh checkout should make you nervous; that is correct.serial: 1on anything that restarts networking or SSH. Losing the connection mid-play on ten hosts at once is a memorable lesson.- Secrets in
ansible-vault, not ingroup_varsin plaintext.ansible-vault encrypt_stringper value keeps diffs readable.
What this buys you
Not "automation" in the abstract — a rebuild story. A dead SD card or a bricked boot disk becomes: install the base OS, add the SSH key, run two playbooks, walk away. In a home lab, that is the difference between an infrastructure and a pile of machines with a wiki attached.