Files
infra/ansible
DmitryandClaude Sonnet 5 a3fe8031fe feat: migrate all managed LXC provisioning to OpenTofu (blue-green)
Blue-green: a new container is created beside the old one, data is copied, the
IP is moved onto it, and the old container is kept stopped as rollback for at
least a week. Keeping the IP means only the VMID changes, and its consumers
(backup jobs, backup audit) already derive it from the registry.

Batch 1 (2026-09-02): emergency-bot 148->151, docker-test 145->152,
gitea 141->153, vaultwarden 140->154, monitoring 146->155, gyro 150->156,
grimmory 149->157.
Batch 2 (2026-09-03): adguard 144->158, mihomo 143->159, ovpn-mini 132->160.
All migratable LXC are now provisioner: tofu. hermes-ai (frozen) and pbs stay.

- tofu/services.tf + tofu/svc-*.tf: one resource per service, reproducing the
  pct-config etalon. /dev/fuse -> features.fuse; /dev/net/tun ->
  device_passthrough (first live use on mihomo and ovpn-mini); gitea bind mount
  -> datastore volume (data finally reaches PBS); console { type = "shell" }
  declared explicitly (provider tracks cmode there).
- services.yml: vmid + provisioner: tofu for every migrated service; features
  strings and device notes updated to the tofu representation; also drops the
  memoir-bot entry and adds homelab_reverse_proxy_image/_unit.
- pve-*.yml: configuration play target is `{{ pve_config_target | default(...) }}`
  so it can run against <name>-new on a temp address (a bare --limit zeroes the
  play instead of retargeting it). Container-creation plays are gated behind
  `provisioner != 'tofu'` / `pve_provisioning_enabled` (meta: end_play), so a
  stray run cannot pct start a stopped OLD VMID on a live IP. Override for
  intentional legacy rollback: -e pve_<svc>_legacy_provisioning_enabled=true.
- ssh_config: drop memoir-bot; ovpn-mini gets ProxyJump none (a jump via ru-vps
  would route through the very tunnel ovpn-mini terminates).
- gyro.yml / uptime-kuma.yml: same pve_config_target override.
- roles/uptime_kuma: only freeze homelab-monitoring when the unit actually
  exists (a fresh blue-green container never had it).
- offsite-restic-yadisk.yml: the gitea restic profile now runs inside the LXC
  (hosts: gitea), since the bind-mount host path is gone after the volume move;
  lost+found excluded (unreadable in an unprivileged LXC, restic exit 3).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uoq5AVK8mkBgg83Mq6o5V
2026-09-03 07:05:46 +03:00
..

HomeLab Ansible

Ansible is the control plane for HomeLab infrastructure changes.

Contract

  • The operator keeps passwords, SSH access and network reachability working.
  • The agent changes infrastructure only through inventory, roles and playbooks in this directory.
  • Manual server changes are allowed only for break-glass recovery; afterwards they must be captured in Ansible.
  • Secrets stay outside git. Use .env, vault files or local prompt input, not committed variables.

Layout

  • inventory/hosts.yml — canonical host list and host-specific facts.
  • playbooks/ — entry points for tasks.
  • roles/ — reusable configuration units.

Current Groups

  • ru-vps — public VPS, JumpHost, qdevice, OpenVPN server.
  • pve_nodes — Proxmox hosts: cloud-pc, mini-pc.
  • lxc_infra — infrastructure LXC containers, including the outbound-only gyro investment allocator host.
  • monitoring_server — monitoring LXC; Prometheus stack is frozen and Uptime Kuma is active.
  • monitoring_exporters — hosts exposing Node Exporter metrics.
  • monitoring_smart_exporters — Proxmox nodes exposing SMART metrics.
  • vpn_openvpn — OpenVPN transport hosts: ru-vps, ovpn-mini.
  • shell_hosts — hosts with unified bash config: ru-vps, cloud-pc, mini-pc.
  • servers — all managed hosts.

First Checks

Install control-node dependencies locally:

python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
ansible-galaxy collection install -r requirements.yml -p collections

Run from ansible/:

ansible-playbook playbooks/check.yml

Controlled Updates

Service updates are manual and use pinned tag@sha256:digest image references only; floating tags and auto-update agents are not used.

Run the dedicated playbook for the target service from ansible/:

.venv/bin/ansible-playbook playbooks/vaultwarden-update.yml
.venv/bin/ansible-playbook playbooks/gitea-update.yml
.venv/bin/ansible-playbook playbooks/adguard-update.yml
.venv/bin/ansible-playbook playbooks/mihomo-update.yml
.venv/bin/ansible-playbook playbooks/grimmory-update.yml

Update flow is always: fresh backup/audit first, then the update playbook, then health verification.

  • Gitea, Vaultwarden and Grimmory use app-aware offsite restic backups/audits before update.
  • AdGuard and Mihomo use fresh PBS LXC backups before update.
  • grimmory-update.yml currently validates the existing pinned release; any future actual app image upgrade must be preceded by release-note and migration review.

Mihomo Hardening

Use the dedicated hardening playbook only when explicitly approved:

.venv/bin/ansible-playbook playbooks/ru-vps-mihomo-harden.yml -e ru_vps_mihomo_harden_confirm=true

It rotates the live Mihomo SOCKS credentials on ru-vps, locks the proxy to loopback, and removes the public UFW exposure for ports 7890 and 7891.

The rotated credentials are not recoverable for clients unless you roll back the saved config backup.

For Proxmox API playbooks, create ignored .env from .env.example and load it:

cp .env.example .env
. ./.env
.venv/bin/ansible-playbook playbooks/pve-ovpn-mini.yml

Or bootstrap the token from mini-pc with sudo:

.venv/bin/ansible-playbook playbooks/bootstrap-pve-api-token.yml -K

Create the separate read-only PVE token used by the monitoring exporter:

.venv/bin/ansible-playbook playbooks/bootstrap-monitoring-pve-token.yml

OpenVPN transport:

.venv/bin/ansible-playbook playbooks/openvpn-vps-mini.yml -K
.venv/bin/ansible-playbook playbooks/openvpn-check.yml

Monitoring is provisioned in two steps after loading the monitoring secrets from ignored .env or Ansible Vault:

. ./.env
.venv/bin/ansible-playbook playbooks/pve-monitoring.yml
.venv/bin/ansible-playbook playbooks/monitoring.yml

pve-monitoring.yml creates CT 146 (monitoring, 192.168.1.30) on cloud-pc. monitoring.yml configures exporters, the ru-vps probe vantage point, and the central Prometheus stack. It is frozen while Uptime Kuma is in use; do not run it unless restoring Prometheus monitoring.

Gyro Investment Allocator

pve-gyro.yml creates unprivileged CT 150 (gyro, 192.168.1.35) on mini-pc. gyro.yml installs Python 3.13+, pinned uv, the gyro service user, a container-local GitHub deploy key, restrictive firewall rules, and a weekday systemd timer.

UFW is the currently enforced isolation layer: inbound is denied except SSH from LAN/OpenVPN, and east-west outbound is denied except the Mihomo HTTP proxy. The equivalent CT 150 Proxmox firewall is staged, but the cluster-wide PVE firewall remains disabled; do not enable it without auditing every node and guest with firewall=1.

The role keeps deployment and the timer disabled by default. The active host vars deploy git@github.com:ada-dmitry/t_tech-gyro.git with GitHub's verified ED25519 host key; the timer still requires the ignored Vault file:

cp inventory/host_vars/gyro/vault.yml.example inventory/host_vars/gyro/vault.yml
ansible-vault encrypt inventory/host_vars/gyro/vault.yml

After encrypting the secrets, set gyro_timer_enabled: true in main.yml and apply with --ask-vault-pass. DRY_RUN_OVERRIDE remains true until real trading is explicitly approved.

. ./.env
.venv/bin/ansible-playbook playbooks/pve-gyro.yml
.venv/bin/ansible-playbook playbooks/gyro.yml --ask-vault-pass

The timer runs at 11:00 Europe/Moscow from Monday through Friday and uses OnFailure= for a best-effort Telegram alert. CT 150 is included in the daily mini-pc PBS job and backup freshness audit.

Uptime Kuma uses the existing monitoring LXC and stops/disables homelab-monitoring without deleting its configuration or data. Its UI is available only from the LAN at http://192.168.1.30:3001; create monitors and notification settings in the UI.

.venv/bin/ansible-playbook playbooks/uptime-kuma.yml

Emergency Reverse SSH

pve-emergency-bot.yml creates CT 148 (emergency-bot, 192.168.1.32) on mini-pc. emergency-access.yml configures the bot, a TTL-limited reverse tunnel from mini-pc to ru-vps, and the restricted SSH identities used between them.

Before applying, set EMERGENCY_BOT_TOKEN, EMERGENCY_ALLOWED_USER_IDS, EMERGENCY_VPS_HOST_KEY, and EMERGENCY_MINI_PC_HOST_KEY in ignored .env or Ansible Vault. The host-key variables must be verified public host keys, not values obtained during deployment.

. ./.env
.venv/bin/ansible-playbook playbooks/pve-emergency-bot.yml
.venv/bin/ansible-playbook playbooks/emergency-access.yml

From an authorized private Telegram chat, use the Enable SSH, Status, and Stop buttons or /emergency ssh, /emergency status, and /emergency stop. /emergency ssh enables a 60-minute tunnel only; it does not expose a public port. Connect while it is active with:

The bot uses the LAN Mihomo HTTP proxy at 192.168.1.27:7890 because direct Telegram TCP access is unavailable from HomeLab.

ssh -i ~/.ssh/id_ed25519_homelab_ansible -o IdentitiesOnly=yes -J vps -p 22010 ansible@127.0.0.1

The target account is ansible; it has no password login. Use the existing private key ~/.ssh/id_ed25519_homelab_ansible.

Bootstrap the Ansible service account on shell hosts:

.venv/bin/ansible-playbook -i inventory/hosts.yml playbooks/bootstrap-ansible-user.yml -K

When a task needs privilege escalation:

ansible-playbook playbooks/<name>.yml -K

Workflow

  1. Describe the desired infrastructure change.
  2. Add or update a role/playbook.
  3. Run the smallest safe check or playbook.
  4. Document non-obvious decisions in the project notes.