platform/platform-infra/ansible
Bart Van Geyt 7a65e7f7c6 fix(ansible/backup): create /etc/sanoid and install its defaults
Some sanoid packages (e.g. on Ubuntu 26.04) don't ship /etc/sanoid, so the
config template failed with 'Destination directory does not exist'. Create
the directory explicitly, and copy the packaged sanoid.defaults.conf into
it (sanoid requires it beside sanoid.conf) so the first timer run succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 05:28:08 +02:00
..
group_vars fix(ansible/backup): create /etc/sanoid and install its defaults 2026-07-09 05:28:08 +02:00
inventory Phase 1: Ansible host baseline for Ubuntu VM 2026-07-07 12:45:42 +02:00
roles fix(ansible/backup): create /etc/sanoid and install its defaults 2026-07-09 05:28:08 +02:00
ansible.cfg Phase 1: Ansible host baseline for Ubuntu VM 2026-07-07 12:45:42 +02:00
README.md docs(ansible): warn against mixed apt+pip ansible installs 2026-07-09 05:05:44 +02:00
requirements.yml Phase 1: Ansible host baseline for Ubuntu VM 2026-07-07 12:45:42 +02:00
site.yml Phase 5: backup/DR automation (backup Ansible role) 2026-07-07 18:04:41 +02:00

Phase 1 — Host baseline (Ansible)

Idempotent host configuration for the heleos platform, targeting Ubuntu 24.04 LTS. Roles: base packages → ZFS pool/datasets → Docker (data-root on ZFS) → nftables firewall + container egress filter → SSH hardening → backup/DR (sanoid snapshots, per-DB dumps, offsite).

Prerequisites

  1. An Ubuntu 24.04 VM you can SSH into as a sudo-capable user.
  2. A dedicated second virtual disk attached to the VM for the ZFS pool (e.g. /dev/sdb or /dev/vdb) — separate from the OS disk.
  3. Ansible on your workstation, installed one way only — prefer pipx install --include-deps ansible (isolated). Mixing apt's ansible with a pip ansible causes version-skew errors such as No module named 'ansible.module_utils.six.moves'; if you hit that, remove the duplicates and reinstall via pipx.

Configure

cd platform-infra/ansible
ansible-galaxy collection install -r requirements.yml

cp inventory/hosts.yml.example inventory/hosts.yml   # edit host/user (git-ignored)
$EDITOR group_vars/all.yml                            # set zfs_pool_disk, keys, etc.

Key variables in group_vars/all.yml:

Variable Meaning
zfs_pool_disk The dedicated disk for the pool. Its contents will be destroyed.
zfs_pool_force Must be true to create a pool on a non-empty disk (safety gate).
admin_authorized_keys Public keys for the admin — required before disabling passwords.
ssh_disable_password_auth Leave false until key login is verified, then flip to true.
smtp_relay_host Optional; if set, containers may reach SMTP only on this host.

Run

ansible-playbook site.yml --syntax-check   # no-host pre-flight
ansible-playbook site.yml -K               # apply (-K prompts for the sudo/become
                                           # password; omit only if the user has
                                           # passwordless sudo on the VM)

Everything runs via become (root), so -K is required unless the target user has passwordless sudo (/etc/sudoers.d/… NOPASSWD:ALL).

Run a single layer with tags: --tags zfs, --tags docker, --tags firewall, --tags ssh, --tags base.

Running from Windows / WSL

Files on the /mnt/c drive mount are world-writable (mode 0777), so Ansible ignores ansible.cfg there (a security measure) — which then loses the inventory path and you get "no hosts matched". Two ways around it:

  • Preferred: copy the repo into your WSL home and run from there, where permissions are normal (also much faster):
    cp -r /mnt/c/claude/heleosv2 ~/heleosv2 && cd ~/heleosv2/platform-infra/ansible
    
  • Or force the config explicitly (bypasses the world-writable check):
    export ANSIBLE_CONFIG=$(pwd)/ansible.cfg
    

Either way, create the inventory first (cp inventory/hosts.yml.example inventory/hosts.yml and edit it). WSL itself is not a valid target host (no spare disk for the ZFS pool); point the inventory at your Ubuntu VM.

Pre-flight without a host: ansible-playbook --syntax-check site.yml (--check is not meaningful on the first run — see Safety notes).

Safety notes

  • ZFS is destructive: the play refuses to create a pool on a disk that already has a filesystem/partition unless zfs_pool_force: true. Double-check zfs_pool_disk points at the empty spare disk, not the OS disk. For production prefer a stable /dev/disk/by-id/... path over /dev/sdb.
  • SSH lock-out: the play asserts that admin_authorized_keys is non-empty before it will disable password authentication. Verify you can log in with your key before setting ssh_disable_password_auth: true.
  • Firewall coexistence: host inbound rules live in a dedicated inet heleos nftables table and never flush the global ruleset, so Docker's own iptables/nft rules are left intact. Container SMTP egress is blocked via a DOCKER-USER jump applied by the heleos-docker-egress service.

What this sets up

  • ZFS pool tank with platform datasets and the customers parent (see ../../docs/03-naming-conventions.md).
  • Docker Engine + Compose plugin, data-root on tank/platform/docker using the native zfs storage driver; per-site network address pool preconfigured.
  • nftables default-deny inbound (allow SSH/80/443 + established + loopback + ICMP); outbound SMTP blocked from containers.
  • Hardened SSH (key-first, root prohibit-password) and the sftponly group that per-customer SFTP accounts will join in Phase 4.
  • Backup/DR (Phase 5): sanoid snapshot policy + timer, nightly per-database dumps with rotation, and optional offsite zfs send (syncoid) / rsync — enable offsite by setting zfs_offsite_target / db_offsite_target.

Verify after running

zpool status && zfs list
docker info | grep -E 'Storage Driver|Docker Root Dir'
sudo nft list table inet heleos
sudo iptables -S DOCKER-USER