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