Add service registry, shared roles and unified reverse proxy

Collect the facts about all 14 services -- VMID, node, address, ports,
domain, pinned images, resources, backup and monitoring participation --
into group_vars/all/services.yml. Values are taken from the existing
playbooks; gaps are marked null rather than invented.

Replace reverse-proxy-{gitea,vaultwarden,grimmory}.yml with a single
playbook iterating over registry entries that declare a domain. It keeps
every check the three had, preserves grimmory's richer Caddy block
byte-for-byte, and restarts Caddy once when any site changed instead of
up to three times. Verified with --check --diff against ru-vps: ok=6
changed=0, so it reproduces the current Caddyfile exactly.

Add two roles factoring out the skeleton duplicated across the pve-*
playbooks: lxc_docker_host (packages, /dev/fuse assertion, fuse-overlayfs
storage driver, UFW baseline) and compose_service (compose file, systemd
unit, config validation, health check). They are not wired into any
playbook yet -- migrating a live service is a separate, per-service step;
compose_service/README.md shows the Gitea example and spells out what
actually changes on the host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71
This commit is contained in:
Dmitry
2026-08-26 22:10:16 +03:00
co-authored by Claude Opus 5
parent ec3b736250
commit 9725d3ea7c
15 changed files with 1503 additions and 218 deletions
+182
View File
@@ -0,0 +1,182 @@
---
# ============================================================================
# Единый плейбук публикации сервисов через Caddy на ru-vps.
#
# Заменяет reverse-proxy-gitea.yml, reverse-proxy-vaultwarden.yml и
# reverse-proxy-grimmory.yml. Источник данных — реестр homelab_services
# (ansible/inventory/group_vars/all/services.yml): обрабатываются все сервисы,
# у которых определён блок `proxy`.
#
# Отличие от трёх старых плейбуков: контейнер Caddy перезапускается ОДИН раз
# в конце, если изменился хотя бы один сайт (раньше — по разу на плейбук).
#
# Запуск:
# ansible-playbook playbooks/reverse-proxy.yml
# ansible-playbook playbooks/reverse-proxy.yml -e 'reverse_proxy_only=[gitea]'
# ============================================================================
- name: Configure public reverse proxy sites on ru-vps
hosts: ru-vps
gather_facts: false
vars:
caddy_dir: "{{ homelab_reverse_proxy_dir | default('/opt/services/ru-vps/caddy') }}"
caddyfile_path: "{{ homelab_reverse_proxy_caddyfile | default('/opt/services/ru-vps/caddy/Caddyfile') }}"
caddy_container: "{{ homelab_reverse_proxy_container | default('caddy') }}"
caddyfile_container_path: /etc/caddy/Caddyfile
# Ограничить прогон подмножеством сервисов: -e 'reverse_proxy_only=[gitea]'
reverse_proxy_only: []
reverse_proxy_services: >-
{{ homelab_services | dict2items
| selectattr('value.proxy', 'defined')
| selectattr('key', 'in', reverse_proxy_only) | list
if reverse_proxy_only | length > 0
else homelab_services | dict2items
| selectattr('value.proxy', 'defined') | list }}
tasks:
- name: Require at least one service with a published domain
ansible.builtin.assert:
that:
- reverse_proxy_services | length > 0
fail_msg: >-
В homelab_services нет ни одного сервиса с блоком `proxy`
(или reverse_proxy_only отфильтровал всё).
# Циклы по именам, а не по записям целиком: иначе assert печатает весь
# словарь сервиса в вывод при каждом прогоне.
- name: Require every proxied service to declare domain and upstream
ansible.builtin.assert:
that:
- svc.domain | default('') | length > 0
- svc.upstream | default('') | length > 0
- svc.caddy_marker | default('') is search('{mark}')
fail_msg: >-
Сервис {{ item }}: в proxy обязаны быть domain, upstream и
caddy_marker с плейсхолдером {mark}.
quiet: true
loop: "{{ reverse_proxy_services | map(attribute='key') | list }}"
vars:
svc: "{{ homelab_services[item].proxy }}"
- name: Require a custom Caddy body to point at the declared upstream
ansible.builtin.assert:
that:
- svc.upstream in svc.caddy_body
fail_msg: >-
Сервис {{ item }}: caddy_body не содержит апстрим
{{ svc.upstream }} — реестр рассинхронизирован.
quiet: true
loop: >-
{{ reverse_proxy_services
| selectattr('value.proxy.caddy_body', 'defined')
| map(attribute='key') | list }}
vars:
svc: "{{ homelab_services[item].proxy }}"
- name: Ensure Caddy service directory exists
ansible.builtin.file:
path: "{{ caddy_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Configure Caddy sites
ansible.builtin.blockinfile:
path: "{{ caddyfile_path }}"
create: true
owner: root
group: root
mode: "0644"
marker: "{{ item.value.proxy.caddy_marker }}"
block: |
{{ item.value.proxy.domain }} {
{% if item.value.proxy.caddy_body is defined %}
{{ item.value.proxy.caddy_body | trim | indent(2, first=True) }}
{% else %}
reverse_proxy {{ item.value.proxy.upstream }}
{% endif %}
}
loop: "{{ reverse_proxy_services }}"
loop_control:
label: "{{ item.key }} ({{ item.value.proxy.domain }})"
register: caddy_sites
# ------------------------------------------------------------------
# МИГРАЦИОННАЯ СЕКЦИЯ. Удаляет неуправляемые секции Caddyfile, оставшиеся
# от прежней инсталляции на 10.122.62.0/24. Держится только ради миграции:
# после подтверждённого прогона на ru-vps эту задачу и поля
# proxy.caddy_legacy_regexp в services.yml можно удалить.
# ------------------------------------------------------------------
- name: Remove legacy unmanaged Caddy sites
ansible.builtin.replace:
path: "{{ caddyfile_path }}"
regexp: "{{ item.value.proxy.caddy_legacy_regexp }}"
replace: ''
loop: "{{ reverse_proxy_services | selectattr('value.proxy.caddy_legacy_regexp', 'defined') | list }}"
loop_control:
label: "{{ item.key }}"
register: caddy_legacy
- name: Validate host Caddy config
ansible.builtin.command:
argv:
- caddy
- validate
- --config
- "{{ caddyfile_path }}"
changed_when: false
- name: Check Caddy container mounted config
ansible.builtin.command:
argv:
- docker
- exec
- "{{ caddy_container }}"
- grep
- -F
- >-
{{ item.value.proxy.caddy_container_check
| default('reverse_proxy ' ~ item.value.proxy.upstream) }}
- "{{ caddyfile_container_path }}"
loop: "{{ reverse_proxy_services }}"
loop_control:
label: "{{ item.key }}"
register: caddy_container_checks
changed_when: false
failed_when: false
- name: Restart Caddy once when any site changed or a bind mount is stale
ansible.builtin.command:
argv:
- docker
- restart
- "{{ caddy_container }}"
when: >-
caddy_sites.changed or
caddy_legacy.changed or
(caddy_container_checks.results
| selectattr('rc', 'defined')
| rejectattr('rc', 'equalto', 0)
| list | length > 0)
- name: Validate Caddy container config after restart
ansible.builtin.command:
argv:
- docker
- exec
- "{{ caddy_container }}"
- caddy
- validate
- --config
- "{{ caddyfile_container_path }}"
changed_when: false
- name: Check service upstreams from ru-vps
ansible.builtin.uri:
url: "http://{{ item.value.proxy.upstream }}{{ item.value.proxy.health.path | default('/') }}"
status_code: "{{ item.value.proxy.health.status_code | default([200]) }}"
follow_redirects: "{{ item.value.proxy.health.follow_redirects | default('none') }}"
return_content: false
loop: "{{ reverse_proxy_services }}"
loop_control:
label: "{{ item.key }} -> {{ item.value.proxy.upstream }}"