Skip to content

Ansible

Infrastructure-as-code for the homelab. Manages configuration, package installation, service deployment, and ongoing maintenance across all hosts from a single control repo.

Repo: rampantlemming/homelab-ansible

Control node

Ansible runs from a dedicated LXC container on proxfold.

Property Value
Container ID 104
Hostname control
IP 192.168.1.245
OS Debian 12 (bookworm)
Type Unprivileged LXC
Cores 1
Memory 512 MB
Storage 8G root disk
Features nesting=1

Note

The control node is in the inventory under lxc_containers with ansible_connection: local, so site.yml applies common, security, auto_updates, and beszel_agent to it like any other host without trying to SSH from CT104 to itself. See playbooks/control.yml for the role list and Drift Detection for how the daily timer runs from this CT.

A WSL box is the primary cold-start control node for disaster recovery (CT104 doesn't exist until proxfold is rebuilt and its backup restored). Both pull from the same GitHub remote and run the same playbooks. See WSL control node bootstrap.

What it manages

Host Group Role
proxfold (192.168.1.250) proxmox Proxmox VE host — ZFS, NFS, GPU drivers, NUT, PBS client
arrstack (192.168.1.252) virtual_machines Docker VM — media stack, MediaBot
n8n (192.168.1.248) virtual_machines Docker host VM — n8n + Hawser (Phase 5C)
plex (192.168.1.230) lxc_containers Plex Media Server LXC
pbs (192.168.1.246) lxc_containers Proxmox Backup Server LXC (Phase 5A)
beszel (192.168.1.247) lxc_containers Beszel monitoring hub LXC (Phase 5B)
edge (192.168.1.244) lxc_containers Caddy reverse proxy LXC (Phase 5D)
vintage (192.168.1.235) lxc_containers Vintage Story dedicated server LXC (host-level only)
control (192.168.1.245) lxc_containers Ansible control node LXC — ansible_connection: local

Repo structure

homelab-ansible/
├── ansible.cfg              # Ansible configuration
├── requirements.yml         # Galaxy collection dependencies
├── inventory/
│   ├── hosts.yml            # Host inventory and groups
│   ├── group_vars/all/
│   │   ├── vars.yml         # Shared variables for all hosts
│   │   └── vault.yml        # Encrypted secrets (Ansible Vault)
│   └── host_vars/
│       ├── proxfold.yml
│       ├── arrstack.yml
│       ├── n8n.yml
│       ├── plex.yml
│       ├── pbs.yml
│       ├── beszel.yml
│       ├── edge.yml          # Phase 5D
│       ├── vintage.yml
│       └── control.yml
├── playbooks/
│   ├── site.yml             # Master playbook (imports the host playbooks below)
│   ├── proxmox-host.yml
│   ├── arrstack.yml
│   ├── plex.yml
│   ├── pbs.yml
│   ├── beszel.yml
│   ├── n8n.yml
│   ├── edge.yml             # Phase 5D
│   ├── vintage.yml
│   ├── control.yml
│   └── auto-updates.yml
├── rebuild/                 # Proxmox auto-install kit (answer + ISO builder)
├── drift-detection/         # Daily timer + drift-check.sh wrapper for CT104
├── stacks/                  # Docker Compose files deployed via Dockhand
│   ├── arrstack/
│   ├── korrosync/
│   └── n8n/
└── roles/
    ├── common/              # Base system config (all hosts)
    ├── security/            # SSH hardening, Fail2ban (all hosts)
    ├── auto_updates/        # Unattended-upgrades wrapper (all hosts)
    ├── proxmox/             # PVE host baseline + PBS client + notifications
    ├── docker/              # Docker CE installation
    ├── zfs/                 # ZFS tuning, scrub timer, ZED Discord webhook
    ├── nvidia/              # GPU drivers, NVENC patch, IPMI fan fix
    ├── nfs/                 # NFS server and client
    ├── nut/                 # Network UPS Tools
    ├── pbs/                 # Proxmox Backup Server (Phase 5A)
    ├── beszel_hub/          # Beszel hub LXC install (Phase 5B)
    ├── beszel_agent/        # Per-host Beszel metrics agent (Phase 5B)
    ├── hawser/              # Dockhand remote-host agent (Phase 5C)
    ├── caddy/               # Caddy reverse proxy on edge LXC (Phase 5D)
    ├── arrstack/            # Media stack Docker Compose deployment
    └── plex/                # Plex Media Server installation

Prerequisites

pip install ansible
ansible-galaxy collection install -r requirements.yml

Galaxy collections required:

Collection Min version Purpose
community.general 9.0.0 General admin modules
ansible.posix 1.5.0 POSIX system modules
community.docker 3.0.0 Docker management

Running playbooks

# Full run — all hosts, all roles
ansible-playbook playbooks/site.yml

# Dry run — show what would change, no modifications
ansible-playbook playbooks/site.yml --check --diff

# Single host
ansible-playbook playbooks/site.yml --limit proxfold

# Single role via tag
ansible-playbook playbooks/site.yml --tags zfs

# Combine — single host, single role
ansible-playbook playbooks/site.yml --limit arrstack --tags docker

Secrets

Secrets are stored in group_vars/all/vault.yml encrypted with Ansible Vault. To edit:

ansible-vault edit group_vars/all/vault.yml

The vault password is stored locally and excluded from git via .gitignore. See Variables & Vault for the full list of vaulted variables.

Linting & quality gates

ansible-lint runs as a pre-commit hook on every commit (.pre-commit-config.yaml). The active profile is set in .ansible-lint.

Profile State Notes
basic retired Initial profile (Phase 3D); flipped to moderate 2026-04-25
moderate retired Held briefly on 2026-04-25 between the basic→moderate flip and the production ratchet
production active Reached 2026-04-25 after the no-changed-when sweep, no-handler citations on inline apt-cache refreshes, risky-shell-pipe rewrite, and FQCN canonicalisation

Existing rule violations sit in .ansible-lint-ignore so the gate is non-regressive — new violations fail the hook, existing ones are tolerated until intentionally addressed. The current baseline is dominated by var-naming[no-role-prefix] on inventory-scoped variables (see Ansible and Galaxy conventions below).

Regenerate the baseline after a bulk fix sweep:

ansible-lint --generate-ignore

Don't bypass the hook with --no-verify. If a hook fails, fix the underlying issue or extend the baseline with a citation. The # noqa: <rule> annotation on a task name is preferred for one-off justified exceptions — the rationale stays next to the code (see roles/docker/tasks/main.yml and roles/plex/tasks/main.yml for the inline-not-handler pattern, or roles/nfs/tasks/main.yml for the deliberately-mode-less NFS mount-point dir).

Ansible and Galaxy conventions

One ansible-lint convention is deliberately not enforced and lives in the baseline:

  • var-naming[no-role-prefix] — variables in inventory/host_vars/ and inventory/group_vars/ aren't role-prefixed because they're inventory-scoped, not role-scoped. ansible-lint's view assumes Galaxy-distributable roles where every variable belongs to one role; in this repo the inventory is the source of truth and a single variable (e.g. zfs_pool) feeds multiple roles by design.

Revisit if the role set is ever broken out for Galaxy distribution.

Handler names follow Title case to match ansible-lint's name[casing] rule (renamed from lowercase across the role set on 2026-04-25). Use the same casing in any notify: reference, since Ansible matches handlers by exact-string.