Skip to content

Running Ansible

Comandos canónicos para ejecutar playbooks desde la Mac. Ansible se usa en modo híbrido: el modo principal es ansible-pull (cada nodo se auto-aplica vía un systemd timer ansible-pull.timer, cada 15min), pero también se puede correr ansible-playbook push-mode desde la Mac para iteración rápida y dry-runs.

Setup local (una sola vez)

Instalación

brew install ansible sops age

Keys + env

mkdir -p ~/.config/sops/age
# Si ya tienes una key generada, copiarla aquí. Si no:
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt

Añadir a ~/.zshrc (o ~/.zshenv):

export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
export ANSIBLE_CONFIG="$HOME/docs-repo/ansible/ansible.cfg"

source ~/.zshrc.

SSH

Los nodos asumen autenticación por key (no password). Verifica:

ssh pmx-50 'hostname'
ssh pmx-51 'hostname'
ssh [email protected] 'hostname'

Si falla, añadir tu pubkey a ~/.ssh/authorized_keys del usuario destino. No subir keys a Vaultwarden ni al repo.

Colecciones Ansible

cd ~/docs-repo/ansible
ansible-galaxy install -r requirements.yml

requirements.yml incluye community.sops, community.general, ansible.posix, y community.postgresql (usado por el rol wp_pulse_host).

Comandos canónicos

Dry-run global

cd ~/docs-repo/ansible
ansible-playbook playbook.yml --check --diff

--check: no aplica, solo simula. --diff: muestra diff de archivos cambiados.

Dry-run limitado a un grupo/host

Los nombres de host en --limit son los del inventario, NO los alias SSH

~/.ssh/config en la Mac tiene alias como pmx-50/pmx-51/vm208 que apuntan a las IPs correctas — pero Ansible no los conoce. El inventory_hostname real (lo único válido en --limit) es proxmox, proxmox2, media-208, etc. — así están las claves en inventory.yml, a propósito, para que ansible-pull --limit %H (que usa hostname -s, ver más abajo) siga funcionando. Verificado contra ansible/inventory.yml (2026-08-01).

ansible-playbook playbook.yml --check --diff --limit proxmox_nodes
ansible-playbook playbook.yml --check --diff --limit proxmox        # pmx-50, NO "pmx-50"
ansible-playbook playbook.yml --check --diff --limit media-208      # NO "vm208"

Grupos definidos en inventory.yml (verificado 2026-08-01, algunos no existían cuando se escribió esta guía por primera vez):

Grupo Hosts (inventory_hostname) Qué es
proxmox_nodes proxmox (pmx-50, .50), proxmox2 (pmx-51, .51) Nodos físicos Proxmox
docker_hubs media-208 (VM 208, .208) Hub Docker
caddy_lxc caddy-primary (LXC 270, .40), caddy-secondary (LXC 271, .41) Caddy HA
rp_servers rp-server (LXC 280, .205) Remote-Pulse server
wp_servers wp-pulse-host (LXC 281, .211) WP-Pulse host
direct_lxcs (vacío) Reservado para LXCs con SSH directo; vacío desde que se destruyó hermesbot/LXC 101 el 2026-07-05
all todos

Aplicar (push)

Quitar --check:

ansible-playbook playbook.yml --diff --limit proxmox_nodes

Push mode + ansible-pull timer

Si haces push manual y el timer ansible-pull.timer corre justo después con una rama remota distinta, sobrescribe tu cambio. Para evitarlo: 1. Empuja primero a Git (git push). 2. Luego corre ansible-playbook (que tira del estado de Git).

Solo tareas con un tag

ansible-playbook playbook.yml --tags caddy
ansible-playbook playbook.yml --tags observability --check

Tags actuales (verificado leyendo ansible/playbook.yml completo, 2026-08-01):

Nota corregida: hermesbot/LXC 101 sí se desplegó y luego se destruyó el 2026-07-05 (estado final archivado en el NAS, backups/hermes/hermes_state_final_20260705.tgz — ver también ansible/inventory.yml, comentario en direct_lxcs). No es que "nunca existiera": el agente vivo hoy es OpenClaw/Vera en CT100, un proyecto distinto. Los roles hermes_* siguen en el repo con tag never porque no hay ningún host llamado hermesbot en el inventario (grupo direct_lxcs vacío a propósito) — aplicarlos hoy es un no-op, no algo "aspiracional que nunca corrió".

  • baseline — usuarios, ssh, motd, base packages (todos los hosts, always)
  • proxmox — config PVE en proxmox_nodes
  • observability — collectors SMART, pvesr en proxmox_nodes
  • promtail — log shipper Promtail v3.6.10 (manual, never)
  • nut — Network UPS Tools primary/secondary (manual, never)
  • caddy_lxc — Caddy + keepalived full-routing en LXC 270/271 (manual, never)
  • caddy_managed_config — wrapper Ansible de homelab-ctl.py sync en VM 208 (manual, never)
  • caddy_full_routing_stage / caddy_full_routing_apply — F4.f cutover steps (manual, never)
  • hermes_agent — systemd unit + /opt/mcps/ rsync, host hermesbot ya no existe en el inventario (manual, never, no-op)
  • docker_env_render / docker_env_swap — render /home/monxas/stacks/.env en VM 208 desde SOPS
  • pmx_env_render / pmx_env_swap — render /root/.morning-report.env en pmx-50 desde SOPS
  • hermes_env_render / hermes_env_swap — render /etc/hermes-secrets/mcp.env, host hermesbot ya no existe (no-op)
  • alertmanager_config — despliega alertmanager.yml a VM 208 + SIGHUP manual (manual, never)
  • grafana_config — dashboards + contact points + notification policy + alert rules en Grafana (VM 208), vía API (manual, never)
  • prometheus_config_render / prometheus_config_swap — render+swap de prometheus.yml/alerts.yml con rollback automático si promtool check falla (manual, never)
  • asus_exporter — exporter Prometheus del router ASUS, en media-208 (manual, never)
  • unifi_clients_exporter — exporter Prometheus de clientes UniFi, en media-208 (manual, never)
  • remote_pulse — despliega remote_pulse_server en rp_servers (LXC 280) (manual, never)

Esta lista de tags no estaba completa en versiones previas de esta guía (faltaban caddy_managed_config, alertmanager_config, grafana_config, prometheus_config_*, asus_exporter, unifi_clients_exporter, remote_pulse) — todos verificados grepeando tags: en playbook.yml.

Forzar pull en un nodo (lo que hace el timer)

Comando corregido 2026-08-01 contra el .service real

El comando de abajo estaba desactualizado en dos cosas, verificadas leyendo systemctl cat ansible-pull.service en pmx-50/pmx-51: (1) el repo es privado, el pull usa una URL SSH con deploy key dedicada ([email protected]:... + --private-key /root/.ssh/id_homelab_infra), no https://github.com/...; (2) --limit usa %H = hostname -s, que en estos nodos es literalmente proxmox/proxmox2 — no pmx-50/pmx-51.

ssh pmx-50 'ansible-pull \
    -U [email protected]:monxas/homelab-infra.git \
    --accept-host-key \
    --private-key /root/.ssh/id_homelab_infra \
    -i ansible/inventory.yml ansible/playbook.yml \
    --limit proxmox'

(Para pmx-51, mismo comando con --limit proxmox2 sobre esa máquina.)

Útil para validar que el repo está reflejado en disk sin tener que esperar el ciclo de 15min del timer.

Comprobar el timer

El pull automático lo dispara systemd, no cron. Verifica estado y próxima ejecución con:

systemctl status ansible-pull.timer
systemctl list-timers ansible-pull.timer

(No aparece en crontab -l — no es una entrada de cron.)

Roles existentes

Tabla ampliada 2026-08-01 — faltaban 6 roles

ls ansible/roles/ tiene 16 roles hoy, no 10. Faltaban en esta guía: caddy_managed_config, remote_pulse_server, wp_pulse_host, asus_exporter, unifi_clients_exporter, tailscaled_rfrobredo. La cifra de cobertura "~70%" no se pudo re-verificar (no hay un cálculo documentado de qué es el 100%) — TODO: re-auditar cobertura real o quitar la cifra si nadie sabe cómo se calculó.

Role Hosts Qué hace
baseline all Usuarios, sudoers, SSH config, NTP, base packages
proxmox_node proxmox_nodes /etc/pve/datacenter.cfg, jobs.cfg, sysctl tuning, kernel pinning
observability_collector proxmox_nodes SMART collectors, pvesr metrics, node_exporter scrape config
promtail proxmox_nodes Promtail v3.6.10 + systemd unit + per-host LXC jobs
nut proxmox_nodes NUT primary (pmx-50, USB UPS) + secondary (pmx-51, netclient)
caddy_lxc caddy_lxc (LXC 270/271) Caddy binary + keepalived + Caddyfile + managed.caddy sync (full-routing default)
caddy_managed_config docker_hubs (VM 208) Wrapper Ansible para homelab-ctl.py sync
hermes_agent hermesbot (host ya no existe en el inventario) systemd unit hermes-gateway.service + /opt/mcps/ rsync — no-op hoy
docker_host_env docker_hubs (VM 208) Render ~/stacks/.env desde SOPS (F3.c)
hermes_mcp_env hermesbot (host ya no existe) Render /etc/hermes-secrets/mcp.env desde SOPS — no-op hoy
morning_report_env proxmox (pmx-50) Render /root/.morning-report.env desde SOPS
remote_pulse_server rp_servers (LXC 280) Deploy Remote-Pulse server (ADR-0008)
wp_pulse_host wp_servers (LXC 281) Deploy WP-Pulse host (ADR-0010)
asus_exporter docker_hubs (VM 208) Exporter Prometheus del router ASUS TUF-AX6000
unifi_clients_exporter docker_hubs (VM 208) Exporter Prometheus de clientes UniFi
tailscaled_rfrobredo No referenciado hoy en playbook.yml ni en ansible/playbooks/*.yml — TODO confirmar si es un rol huérfano o se invoca desde otro sitio no revisado en esta auditoría

Out-of-scope deliberado: provisioning VMs/LXCs desde cero, Docker compose stacks (gestionados con homelab-ctl.py), configs runtime apps (Grafana dashboards/alertas y Prometheus config SÍ tienen tags dedicados hoy — grafana_config, prometheus_config_render/_swap — pero corren --connection local contra la API/bind-mount de VM 208, no gestión de paquetes/servicio).

Workflow típico

  1. Edita un template/config en el repo: vim ansible/templates/grafana.ini.j2.
  2. Dry-run: ansible-playbook playbook.yml --check --diff --limit media-208 --tags observability.
  3. Revisa el diff. Si pinta bien:
  4. Aplica: misma línea sin --check.
  5. Verifica: ssh media-208 'systemctl status grafana' (o el nombre de host SSH que tengas en tu ~/.ssh/config).
  6. Commit + push: git add ansible/templates/grafana.ini.j2 && git commit -m '...' && git push.

Troubleshooting

Síntoma Fix
community.sops not found ansible-galaxy collection install community.sops
Failed to get the data key en lookup SOPS Tu key no es recipient del archivo. Ver SOPS key rotation
Permission denied (publickey) a un nodo SSH agent sin la key cargada: ssh-add ~/.ssh/id_ed25519
Cambios no se aplican aunque corra sin --check Tag equivocado, o when: filtro excluye el host. -vvv para ver decisiones
kernel of pve-1 changed warning Reboot pendiente del host. Ver Kernel jump