feat: adopt the ru-vps Caddy stack, fix cluster quorum, decommission ZeroTier

One ru-vps housekeeping sweep (2026-09-02/03):

- playbooks/ru-vps-base.yml (new): adopt the Caddy compose stack into Ansible
  (pinned image by digest, homelab-caddy.service), and manage the corosync-qnetd
  UFW rule - allow 5403/tcp from homelab_pve_egress_ip, drop the stale rule for
  the retired ZeroTier 10.122.62.0/24. The qdevice had gone silent because its
  only allowed path was the decommissioned ZeroTier network.
- group_vars/all/main.yml: homelab_pve_egress_ip (the NATed home egress the PVE
  nodes reach corosync-qnetd from - a direct path that does not depend on the
  OpenVPN tunnel). Marked dynamic: a change silently re-breaks the qdevice.
- playbooks/status.yml: CLUSTER QUORUM section (pvecm status per PVE node) so a
  repeat failure is visible. Also drops the memoir-bot unit list and moves the
  gitea offsite-restic unit to the gitea host (see the OpenTofu-migration commit).
- playbooks/ru-vps-zerotier-decommission.yml (new): stop the zerotier container,
  disable ssh-zt22.service, remove the interface/9993/9001/10.122.62.0/24 UFW
  rules. Node identity and data are kept; removal is a separate step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uoq5AVK8mkBgg83Mq6o5V
This commit is contained in:
Dmitry
2026-09-03 07:05:11 +03:00
co-authored by Claude Sonnet 5
parent e24b75534e
commit 79878e36f9
4 changed files with 358 additions and 3 deletions
+137
View File
@@ -0,0 +1,137 @@
---
# Базовое состояние публичной VPS и стек Caddy.
#
# ОБЛАСТЬ ОТВЕТСТВЕННОСТИ
# Этот плейбук ПРИНИМАЕТ существующий ru-vps под управление Ansible, а не
# разворачивает его с нуля. Docker и compose-плагин уже установлены вручную
# (Docker CE 29.x из upstream-репозитория, не Ubuntu'шный docker.io), поэтому
# плейбук их проверяет, но не ставит: `apt install docker.io` поверх Docker CE
# на боевой VPS сломал бы все стеки сразу.
#
# Управляется здесь ТОЛЬКО стек Caddy. Остальные каталоги в
# /opt/services/ru-vps (gitea-runner, mihomo, resticprofile, squid, zerotier)
# развёрнуты вручную и осознанно оставлены вне управления — решение по каждому
# принимается отдельно, см. docs/ai/plan.md.
#
# ГРАНИЦА С reverse-proxy.yml
# Содержимое Caddyfile принадлежит playbooks/reverse-proxy.yml. Здесь файл
# только проверяется на наличие: стартовать Caddy без конфига бессмысленно,
# а перезаписать его отсюда — значит потерять все маршруты.
#
# ЧТО МЕНЯЕТСЯ НА ЖИВОМ ХОСТЕ ПРИ ПЕРВОМ ПРОГОНЕ
# 1. Образ caddy:2-alpine закрепляется по digest -> контейнер пересоздаётся.
# 2. Появляется systemd-юнит homelab-caddy.service: до сих пор жизненным
# циклом стека управляла только политика restart=unless-stopped, то есть
# после `docker compose down` его никто не поднимал.
# Это кратковременный перерыв в публичном HTTPS. Прогонять осознанно.
#
# ЗАМЕЧАНИЕ ПРО --check
# До первого реального прогона `--check` заканчивается ошибкой
# "Could not find the requested service homelab-caddy": в check-режиме юнит
# на диск не пишется, поэтому systemd его не видит. Это артефакт проверки,
# а не дефект плейбука. Diff'ы задач выше при этом достоверны.
- name: Adopt the ru-vps Caddy stack into Ansible
hosts: ru-vps
gather_facts: true
# roles: выполняются РАНЬШЕ tasks:, поэтому все проверки идут в pre_tasks —
# иначе стек разложился бы до проверки Caddyfile.
pre_tasks:
- name: Verify the container runtime is present
vars:
ru_vps_docker_binary: /usr/bin/docker
block:
- name: Check the Docker binary
ansible.builtin.command: "{{ ru_vps_docker_binary }} version --format '{{ '{{' }}.Server.Version{{ '}}' }}'"
register: ru_vps_docker_version
changed_when: false
# Read-only проверка, поэтому выполняется и в --check.
check_mode: false
- name: Check the Compose plugin
ansible.builtin.command: "{{ ru_vps_docker_binary }} compose version --short"
register: ru_vps_compose_version
changed_when: false
# Read-only проверка, поэтому выполняется и в --check.
check_mode: false
- name: Report the detected runtime
ansible.builtin.debug:
msg: >-
docker={{ ru_vps_docker_version.stdout | trim }}
compose={{ ru_vps_compose_version.stdout | trim }}
# --- Арбитр кластера ---------------------------------------------------
# corosync-qnetd на ru-vps даёт двухнодовому кластеру третий голос.
# Ноды подключаются к нему по публичному адресу (см. corosync.conf,
# host: 157.22.231.198), а старое правило пускало 5403 только из
# 10.122.62.0/24 — сети ZeroTier, выведенной в июле. После этого qdevice
# молча перестал голосовать.
- name: Allow corosync-qnetd from the PVE nodes
community.general.ufw:
rule: allow
port: "5403"
proto: tcp
src: "{{ homelab_pve_egress_ip }}"
comment: corosync-qnetd for the HomeLab PVE cluster
- name: Drop the qnetd rule for the retired ZeroTier network
community.general.ufw:
rule: allow
port: "5403"
proto: tcp
src: 10.122.62.0/24
delete: true
# Caddyfile принадлежит reverse-proxy.yml. Без него стек стартует пустым,
# поэтому сначала проверяем наличие и только потом раскладываем стек.
- name: Check the Caddyfile managed by reverse-proxy.yml
ansible.builtin.stat:
path: "{{ homelab_reverse_proxy_caddyfile }}"
register: ru_vps_caddyfile
- name: Require the Caddyfile before touching the stack
ansible.builtin.assert:
that:
- ru_vps_caddyfile.stat.exists
- ru_vps_caddyfile.stat.size > 0
fail_msg: >-
{{ homelab_reverse_proxy_caddyfile }} отсутствует или пуст.
Сначала прогони playbooks/reverse-proxy.yml, иначе Caddy стартует
без маршрутов и публичные сервисы лягут.
roles:
- role: compose_service
compose_service_name: "{{ homelab_reverse_proxy_unit }}"
compose_service_description: Caddy reverse proxy for public HomeLab services
compose_service_root: "{{ homelab_reverse_proxy_dir }}"
# Каталог создан вручную с 0755 — сохраняем, чтобы прогон не давал
# косметического diff'а на боевом хосте.
compose_service_root_mode: "0755"
# Имя файла сохраняем: имя compose-проекта выводится из каталога, а сам
# файл должен остаться тем же, иначе стек станет orphan и поднимется
# второй контейнер на тех же 80/443.
compose_service_compose_file: docker-compose.yml
# /opt/data/caddy и /opt/configs/caddy принадлежат ada:ada и содержат
# выпущенные сертификаты. Роль их владельца не трогает намеренно.
compose_service_directories: []
compose_service_compose_content: |
services:
caddy:
image: {{ homelab_reverse_proxy_image | trim }}
container_name: {{ homelab_reverse_proxy_container }}
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- /opt/data/caddy:/data
- /opt/configs/caddy:/config
networks: {}
# Caddy на голый IP отвечает 308 (редирект http -> https), а не 200.
compose_service_health_url: http://127.0.0.1:80/
compose_service_health_status: [308]
compose_service_health_follow_redirects: none