добавлен goatcounter для веб-аналитики
Linting / YAML Lint (push) Canceled after 0s
Linting / Ansible Lint (push) Canceled after 0s

- один инстанс на два сайта, gc-main и gc-photo: нужный выбирается по Host
- домены ведутся в goatcounter_sites (group_vars), оттуда же рендерятся имена в Caddyfile
- сайты и первый пользователь заводятся идемпотентно, креды из vault
This commit is contained in:
av
2026-08-21 22:06:06 +03:00
parent af14732652
commit 889c8a565e
8 changed files with 499 additions and 231 deletions
+164
View File
@@ -0,0 +1,164 @@
---
- name: "Configure goatcounter application"
hosts: all
# goatcounter_sites приходит из group_vars/all/main.yml (его же читает
# caddyproxy), goatcounter_admin_email/password — из vault-файла secrets.yml.
vars:
app_name: "goatcounter"
# Родительский сайт: владелец пользователя, к нему линкуются остальные.
goatcounter_parent_vhost: "{{ goatcounter_sites[0] }}"
app_user: "{{ app_name }}"
app_owner_uid: 1110
app_owner_gid: 1110
base_dir: "{{ (application_dir, app_name) | path_join }}"
data_dir: "{{ (base_dir, 'data') | path_join }}"
# Логин и пароль Postbox goatcounter принимает только внутри SMTP-URL, в
# userinfo, — значит их надо процентно экранировать. Фильтр urlencode
# оставляет `/` как есть (safe='/'), поэтому дожимаем его вручную.
postbox_user_enc: "{{ postbox_user | urlencode | replace('/', '%2F') }}"
postbox_pass_enc: "{{ postbox_pass | urlencode | replace('/', '%2F') }}"
tasks:
- name: "Create user and environment"
ansible.builtin.import_role:
name: owner
vars:
owner_name: "{{ app_user }}"
owner_uid: "{{ app_owner_uid }}"
owner_gid: "{{ app_owner_gid }}"
owner_extra_groups: ["docker"]
- name: "Create application internal directories"
ansible.builtin.file:
path: "{{ item }}"
state: "directory"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ base_dir }}"
- "{{ data_dir }}"
# В data лежит только SQLite-база; бэкапим её restic'ом напрямую, без
# промежуточного дампа — как у tududi. Записи редкие (пачка просмотров раз
# в 10 секунд), ночной прогон почти наверняка застаёт базу в покое, а
# retention страхует от редкого несогласованного снимка.
- name: "Create backup targets file"
ansible.builtin.copy:
dest: "{{ base_dir }}/backup-targets"
content: |
{{ data_dir }}
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Copy docker compose file"
ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
# SMTP-пароль стоит прямым значением в environment: — без no_log его
# печатает --diff. Файловых секретов goatcounter не умеет.
no_log: true
- name: "Run application with docker compose"
community.docker.docker_compose_v2:
project_src: "{{ base_dir }}"
state: "present"
remove_orphans: true
tags:
- run-app
# Сайты заводятся по разу, в том же прогоне, что поднимает контейнер: пока
# сайта нет в базе, goatcounter отдаёт на его домене открытый мастер
# регистрации, и это окно надо закрыть сразу.
#
# `db show site` различает три состояния по rc и stderr: rc=0 — сайт есть;
# rc=1 + "nothing found" — база готова, сайта нет; rc=1 + "doesn't exist" —
# serve ещё не накатил миграции. Последнее и есть гонка со стартом
# контейнера, поэтому ждём в until, пока ответ не станет осмысленным.
- name: "Check which goatcounter sites exist"
community.docker.docker_container_exec:
container: "{{ app_name }}_app"
argv:
- "goatcounter"
- "db"
- "show"
- "site"
- "-find"
- "{{ item }}"
loop: "{{ goatcounter_sites }}"
register: "goatcounter_sites_state"
changed_when: false
failed_when: false
until: "goatcounter_sites_state.rc == 0 or 'nothing found' in goatcounter_sites_state.stderr"
retries: 10
delay: 3
tags:
- run-app
# Пароль нужен только когда заводим родительский сайт: дочерние идут с
# -link и своих пользователей не получают.
- name: "Check credentials for the first goatcounter user are set"
ansible.builtin.assert:
that:
- "goatcounter_admin_email | default('') is match('[^@]+@[^@]+')"
- "goatcounter_admin_password | default('') | length >= 8"
fail_msg: >-
Сайт {{ goatcounter_parent_vhost }} ещё не заведён, а
goatcounter_admin_email / goatcounter_admin_password не заданы.
Пропишите их в group_vars/all/secrets.yml и повторите прогон.
when: "goatcounter_sites_state.results[0].rc != 0"
tags:
- run-app
# Вход дальше — своей парой email/пароль на https://{{ goatcounter_parent_vhost }};
# Authelia тут не участвует, эндпоинт /count обязан быть публичным.
- name: "Create the parent goatcounter site and its user"
community.docker.docker_container_exec:
container: "{{ app_name }}_app"
argv:
- "goatcounter"
- "db"
- "create"
- "site"
- "-vhost"
- "{{ goatcounter_parent_vhost }}"
- "-user.email"
- "{{ goatcounter_admin_email }}"
- "-password"
- "{{ goatcounter_admin_password }}"
when: "goatcounter_sites_state.results[0].rc != 0"
changed_when: true
# Пароль стоит прямым значением в argv — без no_log его печатает --diff.
no_log: true
tags:
- run-app
# -link отдаёт дочернему сайту пользователей родителя и ставит его в
# переключалку сайтов в шапке. Данные при этом раздельные: всё, что видно
# в дашборде, скоупится по site_id.
- name: "Create linked goatcounter sites"
community.docker.docker_container_exec:
container: "{{ app_name }}_app"
argv:
- "goatcounter"
- "db"
- "create"
- "site"
- "-vhost"
- "{{ item.item }}"
- "-link"
- "{{ goatcounter_parent_vhost }}"
loop: "{{ goatcounter_sites_state.results[1:] }}"
loop_control:
label: "{{ item.item }}"
when: "item.rc != 0"
changed_when: true
tags:
- run-app