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
174 lines
6.8 KiB
YAML
174 lines
6.8 KiB
YAML
---
|
|
- name: Validate compose_service parameters
|
|
ansible.builtin.assert:
|
|
that:
|
|
- compose_service_name | length > 0
|
|
- (compose_service_compose_content | length > 0)
|
|
!= (compose_service_compose_template | length > 0)
|
|
fail_msg: >-
|
|
Задайте compose_service_name и ровно один из
|
|
compose_service_compose_content / compose_service_compose_template.
|
|
|
|
- name: Ensure the service root directory exists
|
|
ansible.builtin.file:
|
|
path: "{{ compose_service_root }}"
|
|
state: directory
|
|
owner: "{{ compose_service_owner }}"
|
|
group: "{{ compose_service_group }}"
|
|
mode: "{{ compose_service_root_mode }}"
|
|
|
|
- name: Ensure the service data directories exist
|
|
ansible.builtin.file:
|
|
path: "{{ item.path }}"
|
|
state: directory
|
|
owner: "{{ item.owner | default(compose_service_owner) }}"
|
|
group: "{{ item.group | default(compose_service_group) }}"
|
|
mode: "{{ item.mode | default('0750') }}"
|
|
loop: "{{ compose_service_directories }}"
|
|
loop_control:
|
|
label: "{{ item.path }}"
|
|
|
|
# --- Файл окружения --------------------------------------------------------
|
|
# Создаётся один раз под umask 077. Секреты никогда не попадают в вывод:
|
|
# задача целиком под no_log.
|
|
- name: Generate the environment file once
|
|
ansible.builtin.shell: |
|
|
set -eu
|
|
umask 077
|
|
if [ -e {{ compose_service_env_path | quote }} ]; then
|
|
exit 0
|
|
fi
|
|
: > {{ compose_service_env_path | quote }}
|
|
{% for key, value in compose_service_env_static.items() %}
|
|
printf '%s\n' {{ (key ~ '=' ~ value) | quote }} >> {{ compose_service_env_path | quote }}
|
|
{% endfor %}
|
|
{% for key in compose_service_env_generated %}
|
|
printf '%s=%s\n' {{ key | quote }} "$(openssl rand -hex {{ compose_service_env_secret_bytes }})" >> {{ compose_service_env_path | quote }}
|
|
{% endfor %}
|
|
printf created
|
|
args:
|
|
executable: /bin/sh
|
|
register: compose_service_env_result
|
|
changed_when: compose_service_env_result.stdout == 'created'
|
|
no_log: true
|
|
when: >-
|
|
compose_service_env_static | length > 0 or
|
|
compose_service_env_generated | length > 0
|
|
|
|
- name: Enforce the environment file permissions
|
|
ansible.builtin.file:
|
|
path: "{{ compose_service_env_path }}"
|
|
owner: "{{ compose_service_owner }}"
|
|
group: "{{ compose_service_group }}"
|
|
mode: "{{ compose_service_env_mode }}"
|
|
when: >-
|
|
compose_service_env_static | length > 0 or
|
|
compose_service_env_generated | length > 0
|
|
|
|
- name: Install the environment example
|
|
ansible.builtin.copy:
|
|
dest: "{{ compose_service_env_path }}.example"
|
|
owner: "{{ compose_service_owner }}"
|
|
group: "{{ compose_service_group }}"
|
|
mode: "{{ compose_service_env_example_mode }}"
|
|
content: "{{ compose_service_env_example }}"
|
|
when: compose_service_env_example | length > 0
|
|
|
|
# --- compose.yml -----------------------------------------------------------
|
|
- name: Install the Compose configuration from inline content
|
|
ansible.builtin.copy:
|
|
dest: "{{ compose_service_root }}/{{ compose_service_compose_file }}"
|
|
owner: "{{ compose_service_owner }}"
|
|
group: "{{ compose_service_group }}"
|
|
mode: "{{ compose_service_compose_mode }}"
|
|
content: "{{ compose_service_compose_content }}"
|
|
when: compose_service_compose_content | length > 0
|
|
register: compose_service_compose_inline
|
|
|
|
- name: Install the Compose configuration from a template
|
|
ansible.builtin.template:
|
|
src: "{{ compose_service_compose_template }}"
|
|
dest: "{{ compose_service_root }}/{{ compose_service_compose_file }}"
|
|
owner: "{{ compose_service_owner }}"
|
|
group: "{{ compose_service_group }}"
|
|
mode: "{{ compose_service_compose_mode }}"
|
|
when: compose_service_compose_template | length > 0
|
|
register: compose_service_compose_templated
|
|
|
|
- name: Record whether the Compose configuration changed
|
|
ansible.builtin.set_fact:
|
|
compose_service_compose_changed: >-
|
|
{{ (compose_service_compose_inline.changed | default(false)) or
|
|
(compose_service_compose_templated.changed | default(false)) }}
|
|
|
|
# --- systemd ---------------------------------------------------------------
|
|
- name: Install the systemd unit
|
|
ansible.builtin.copy:
|
|
dest: "{{ compose_service_unit_path }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
content: |
|
|
[Unit]
|
|
Description={{ compose_service_description }}
|
|
Wants=network-online.target
|
|
After=network-online.target docker.service{{ (' ' ~ compose_service_after | join(' ')) if compose_service_after else '' }}
|
|
Requires=docker.service{{ (' ' ~ compose_service_requires | join(' ')) if compose_service_requires else '' }}
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
WorkingDirectory={{ compose_service_root }}
|
|
ExecStart={{ compose_service_docker_binary }} compose -f {{ compose_service_root }}/{{ compose_service_compose_file }} up -d --remove-orphans
|
|
ExecStop={{ compose_service_docker_binary }} compose -f {{ compose_service_root }}/{{ compose_service_compose_file }} down
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
register: compose_service_unit
|
|
notify: compose_service_daemon_reload
|
|
|
|
- name: Apply the pending systemd daemon reload
|
|
ansible.builtin.meta: flush_handlers
|
|
|
|
# --- Валидация -------------------------------------------------------------
|
|
# no_log: сообщение об ошибке `docker compose config` подставляет значения
|
|
# переменных из .env.
|
|
- name: Validate the Compose configuration
|
|
ansible.builtin.command:
|
|
argv:
|
|
- "{{ compose_service_docker_binary }}"
|
|
- compose
|
|
- -f
|
|
- "{{ compose_service_root }}/{{ compose_service_compose_file }}"
|
|
- config
|
|
- --quiet
|
|
args:
|
|
chdir: "{{ compose_service_root }}"
|
|
changed_when: false
|
|
no_log: true
|
|
when: compose_service_validate
|
|
|
|
# --- Запуск ----------------------------------------------------------------
|
|
- name: Enable and start the service
|
|
ansible.builtin.systemd:
|
|
name: "{{ compose_service_name }}"
|
|
enabled: "{{ compose_service_enabled }}"
|
|
state: "{{ 'restarted' if compose_service_needs_restart else 'started' }}"
|
|
vars:
|
|
compose_service_needs_restart: >-
|
|
{{ (compose_service_compose_changed | bool) or
|
|
(compose_service_unit.changed | default(false)) or
|
|
(compose_service_restart_triggers | select | list | length > 0) }}
|
|
|
|
- name: Wait for the service health endpoint
|
|
ansible.builtin.uri:
|
|
url: "{{ compose_service_health_url }}"
|
|
status_code: "{{ compose_service_health_status }}"
|
|
follow_redirects: "{{ compose_service_health_follow_redirects }}"
|
|
return_content: false
|
|
register: compose_service_health
|
|
retries: "{{ compose_service_health_retries }}"
|
|
delay: "{{ compose_service_health_delay }}"
|
|
until: compose_service_health.status in (compose_service_health_status | map('int') | list)
|
|
when: compose_service_health_url | length > 0
|