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
183 lines
7.4 KiB
YAML
183 lines
7.4 KiB
YAML
---
|
||
# ============================================================================
|
||
# Единый плейбук публикации сервисов через 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 }}"
|