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
-65
View File
@@ -1,65 +0,0 @@
- name: Configure Gitea reverse proxy on ru-vps
hosts: ru-vps
gather_facts: false
vars:
caddy_dir: /opt/services/ru-vps/caddy
caddyfile_path: /opt/services/ru-vps/caddy/Caddyfile
gitea_domain: git.ada-dev.ru
gitea_upstream: 192.168.1.25:3000
tasks:
- name: Ensure Caddy service directory exists
ansible.builtin.file:
path: "{{ caddy_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Configure Gitea Caddy site
ansible.builtin.blockinfile:
path: "{{ caddyfile_path }}"
create: true
owner: root
group: root
mode: "0644"
marker: "# {mark} ANSIBLE MANAGED GITEA SITE"
block: |
{{ gitea_domain }} {
reverse_proxy {{ gitea_upstream }}
}
register: gitea_caddy_site
- name: Remove legacy unmanaged Gitea Caddy site
ansible.builtin.replace:
path: "{{ caddyfile_path }}"
regexp: '(?ms)^git\.ada-dev\.ru \{\n\s*reverse_proxy 10\.122\.62\.51:3002\n\}\n+'
replace: ''
register: gitea_legacy_site
- name: Validate host Caddy config
ansible.builtin.command: caddy validate --config {{ caddyfile_path }}
changed_when: false
- name: Check Caddy container mounted config
ansible.builtin.command: docker exec caddy grep -F 'reverse_proxy {{ gitea_upstream }}' /etc/caddy/Caddyfile
register: gitea_container_caddyfile
changed_when: false
failed_when: false
- name: Restart Caddy when config changed or bind mount is stale
ansible.builtin.command: docker restart caddy
when: >-
gitea_caddy_site.changed or
gitea_legacy_site.changed or
gitea_container_caddyfile.rc != 0
- name: Validate Caddy container config after restart
ansible.builtin.command: docker exec caddy caddy validate --config /etc/caddy/Caddyfile
changed_when: false
- name: Check Gitea upstream from ru-vps
ansible.builtin.command: curl -fsS -o /dev/null -w '%{http_code}' http://{{ gitea_upstream }}/
register: gitea_upstream_http
changed_when: false
failed_when: gitea_upstream_http.stdout != '200'
@@ -1,88 +0,0 @@
---
- name: Configure Grimmory reverse proxy on ru-vps
hosts: ru-vps
gather_facts: false
vars:
caddy_dir: /opt/services/ru-vps/caddy
caddyfile_path: /opt/services/ru-vps/caddy/Caddyfile
grimmory_domain: books.ada-dev.ru
grimmory_upstream: 192.168.1.34:6060
tasks:
- name: Ensure Caddy service directory exists
ansible.builtin.file:
path: "{{ caddy_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Configure Grimmory Caddy site
ansible.builtin.blockinfile:
path: "{{ caddyfile_path }}"
create: true
owner: root
group: root
mode: "0644"
marker: "# {mark} ANSIBLE MANAGED GRIMMORY SITE"
block: |
{{ grimmory_domain }} {
@grimmory_opds_atom path /api/v1/opds /api/v1/opds/libraries /api/v1/opds/shelves /api/v1/opds/magic-shelves /api/v1/opds/authors /api/v1/opds/series /api/v1/opds/catalog /api/v1/opds/recent /api/v1/opds/surprise
handle @grimmory_opds_atom {
reverse_proxy {{ grimmory_upstream }} {
header_up Accept "application/atom+xml"
header_up Accept-Encoding identity
transport http {
compression off
}
}
}
@grimmory_opds_search path /api/v1/opds/search.opds
handle @grimmory_opds_search {
reverse_proxy {{ grimmory_upstream }} {
header_up Accept "application/opensearchdescription+xml"
header_up Accept-Encoding identity
transport http {
compression off
}
}
}
@grimmory_device_api path /api/koreader /api/koreader/* /api/v1/opds /api/v1/opds/*
handle @grimmory_device_api {
reverse_proxy {{ grimmory_upstream }} {
header_up Accept-Encoding identity
transport http {
compression off
}
}
}
handle {
reverse_proxy {{ grimmory_upstream }}
}
}
register: grimmory_caddy_site
- name: Validate host Caddy config
ansible.builtin.command: caddy validate --config {{ caddyfile_path }}
changed_when: false
- name: Check Caddy container mounted config
ansible.builtin.command: docker exec caddy grep -F '@grimmory_device_api path /api/koreader' /etc/caddy/Caddyfile
register: grimmory_container_caddyfile
changed_when: false
failed_when: false
- name: Restart Caddy when config changed or bind mount is stale
ansible.builtin.command: docker restart caddy
when: grimmory_caddy_site.changed or grimmory_container_caddyfile.rc != 0
- name: Validate Caddy container config after restart
ansible.builtin.command: docker exec caddy caddy validate --config /etc/caddy/Caddyfile
changed_when: false
- name: Check Grimmory upstream from ru-vps
ansible.builtin.uri:
url: http://{{ grimmory_upstream }}/api/v1/healthcheck
status_code: 200
@@ -1,65 +0,0 @@
- name: Configure Vaultwarden reverse proxy on ru-vps
hosts: ru-vps
gather_facts: false
vars:
caddy_dir: /opt/services/ru-vps/caddy
caddyfile_path: /opt/services/ru-vps/caddy/Caddyfile
vaultwarden_domain: pass.ada-dev.ru
vaultwarden_upstream: 192.168.1.24:80
tasks:
- name: Ensure Caddy service directory exists
ansible.builtin.file:
path: "{{ caddy_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Configure Vaultwarden Caddy site
ansible.builtin.blockinfile:
path: "{{ caddyfile_path }}"
create: true
owner: root
group: root
mode: "0644"
marker: "# {mark} ANSIBLE MANAGED VAULTWARDEN SITE"
block: |
{{ vaultwarden_domain }} {
reverse_proxy {{ vaultwarden_upstream }}
}
register: vaultwarden_caddy_site
- name: Remove legacy unmanaged Vaultwarden Caddy site
ansible.builtin.replace:
path: "{{ caddyfile_path }}"
regexp: '(?ms)^pass\.ada-dev\.ru \{\n\s*reverse_proxy 10\.122\.62\.95:10380\n\}\n+'
replace: ''
register: vaultwarden_legacy_site
- name: Validate host Caddy config
ansible.builtin.command: caddy validate --config {{ caddyfile_path }}
changed_when: false
- name: Check Caddy container mounted config
ansible.builtin.command: docker exec caddy grep -F 'reverse_proxy {{ vaultwarden_upstream }}' /etc/caddy/Caddyfile
register: vaultwarden_container_caddyfile
changed_when: false
failed_when: false
- name: Restart Caddy when config changed or bind mount is stale
ansible.builtin.command: docker restart caddy
when: >-
vaultwarden_caddy_site.changed or
vaultwarden_legacy_site.changed or
vaultwarden_container_caddyfile.rc != 0
- name: Validate Caddy container config after restart
ansible.builtin.command: docker exec caddy caddy validate --config /etc/caddy/Caddyfile
changed_when: false
- name: Check Vaultwarden upstream from ru-vps
ansible.builtin.command: curl -fsS -o /dev/null -w '%{http_code}' http://{{ vaultwarden_upstream }}/
register: vaultwarden_upstream_http
changed_when: false
failed_when: vaultwarden_upstream_http.stdout != '200'
+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 }}"