Docs: add docs and drafts

This commit is contained in:
2026-05-22 19:13:05 +03:00
parent 4a5db6e2bc
commit 893996f0c9
2 changed files with 367 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# Алерты на проблемные контейнеры
## Контекст
Случай с wakapi: при старте упали миграции, контейнер встал в restart-loop и
несколько дней крутился по кругу — никто не узнал. Из этого две проблемы:
1. Контейнеры могут бесконечно перезапускаться при ошибке.
2. Нет алертов о таких ситуациях.
## Что есть и что использовать
- **Netdata** — Docker-collector через cgroups + Docker API: состояние,
restart count, healthcheck status. Алерты в `health.d/*.conf`, нотификации
через `health_alarm_notify.conf` (Telegram/Discord/email/ntfy).
- **Dozzle** — только для просмотра логов после факта, нормальных алертов нет.
- **Caddy** — мог бы участвовать в healthcheck снаружи, но это отдельный слой.
## План — три слоя
### 1. Healthchecks в compose (фундамент)
Без них Docker считает контейнер «running», пока процесс жив, — wakapi с
падающими миграциями этому условию удовлетворял. Добавить в каждый
`docker-compose.yml.j2`:
```yaml
healthcheck:
test: ["CMD", "wget", "-q", "-O-", "http://localhost:PORT/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s # окно на миграции — failed-проверки до истечения не считаются
```
`start_period` — ключевая штука для случая wakapi: даём миграциям отработать,
до его истечения healthcheck не убивает контейнер.
### 2. Алерты через Netdata (главное)
Два разных сигнала:
- **Restart loop** — алерт на `docker.container_state` или счётчик
перезапусков (растёт > N за M минут). Это и есть «контейнер крутится по
кругу».
- **Unhealthy** — после healthcheck выше алерт на
`docker.container_health_status != healthy` дольше M минут.
Канал нотификаций: один, проще всего Telegram-бот. Настройка в
`health_alarm_notify.conf`.
### 3. Restart policy — что менять (или не менять)
Скорее **оставить `unless-stopped`**. Альтернативы и их минусы:
- `on-failure:5` — Docker сам остановит после 5 попыток. Минус: после ребута
сервера сервис не поднимется (только `always`/`unless-stopped` встают на
старте докера). Серьёзный регресс для домашнего сервера.
- Внешний sidecar, слушающий `docker events` и останавливающий контейнер
после N рестартов в окне — лишняя сложность ради того, что уже сделает
алерт.
Лучше: алерт пришёл → решаем вручную, остановить или чинить.
## Опционально (вне netdata)
- **Uptime Kuma** — внешний HTTP-чек по публичным URL. Ловит случаи, когда
контейнер «здоров», но прокся/DNS/Caddy сломаны. Свои нотификации, дашборд.
Не дублирует netdata, проверяет с другой стороны.
## Шаги при реализации
1. Добавить healthcheck + start_period в compose-шаблоны (начать с wakapi,
потом по списку).
2. Проверить, что netdata собирает Docker-метрики (collector включён).
3. Настроить один канал нотификаций (Telegram/ntfy/email — выбрать).
4. Написать пару алертов: restart-loop и unhealthy.
## Открытые вопросы
- Какой канал нотификаций использовать.
- Добавлять ли Uptime Kuma сразу или потом.
+285
View File
@@ -0,0 +1,285 @@
# Gitea runner on-demand в Yandex Cloud
## Контекст
В YC планируется развернуть self-hosted раннер для Gitea Actions. Сборки —
несколько раз в неделю, в среднем ~10 в неделю по ~5 минут. ВМ 24/7 даёт
утилизацию в районе 1%, остальное оплачивается впустую.
Цель — раннер активен только во время сборки и небольшого окна простоя
после, без ручных команд от разработчика.
## Архитектура
```
push → Gitea ──webhook──► Cloud Function ──Compute API──► ВМ (раннер)
(HMAC validate, │
start logic) ▼
act_runner (docker)
probe + decide
└─REST self-stop
```
Без API Gateway: функция публикуется напрямую через свой HTTPS-эндпоинт
`https://functions.yandexcloud.net/<id>`. Этот URL вписывается в Gitea
webhook. Аутентификация — HMAC-SHA256 в заголовке `X-Gitea-Signature`,
проверяется внутри функции.
Поток событий:
1. Push в Gitea → System Webhook на URL функции.
2. Функция валидирует HMAC, читает state ВМ, действует по стейт-машине
(см. ниже).
3. ВМ стартует, docker поднимает контейнер `act_runner`, тот подключается
к Gitea и забирает джобу.
4. На ВМ работают probe (раз в 30 сек собирает телеметрию) и decide
(раз в 1 мин принимает решение).
5. После idle-окна decide дёргает Compute REST API на gas самой себя.
## Cloud-side
### Ресурсы в YC
- Один фолдер на старте — общий с Gitea-сервером. Принятый риск: SA
самогашения формально может остановить любую ВМ в фолдере. Перенос в
отдельный фолдер — миграция на потом.
- Два сервисных аккаунта:
- `runner-self-stop` (привязан к ВМ): `compute.instances.stop`,
`compute.instances.get`.
- `runner-starter-fn` (привязан к функции): `compute.instances.start`,
`compute.instances.get`.
- Cloud Function `runner-starter`, runtime Python 3.11, 256 MB, timeout
30 сек. Публичный HTTPS-эндпоинт включён.
- Алерт Cloud Monitoring: `compute.instance.status = RUNNING` дольше 24 ч
подряд → нотификация (канал — на этапе внедрения).
### Bootstrap-скрипты
```
scripts/
├── runner-starter/ # код Cloud Function
│ ├── handler.py # webhook → start, стейт-машина
│ └── requirements.txt
├── runner_bootstrap.py # one-time: создать SA, ВМ, функцию, алерт
└── runner_deploy_function.py # обновить версию функции (yc CLI)
```
Скрипты на Python поверх `yc` CLI (через `subprocess`). Идемпотентность —
проверкой существования ресурсов перед созданием. Terraform не вводим:
ресурсов мало, оверкилл.
### Стейт-машина функции
| State ВМ | Действие |
| ------------------------------------- | ------------------------------------------------------- |
| `RUNNING`, `STARTING`, `RESTARTING` | 200, ничего не делаем |
| `STOPPED` | `instances:start` → 200 |
| `STOPPING` | poll до `STOPPED` (до 25 сек), затем `start` → 200 |
| `PROVISIONING`, `UPDATING` | 503 (временное состояние, retry клиентом) |
| `ERROR`, `CRASHED` | 500 + лог ошибки (нужен человек) |
| `DELETING`, `DELETED` | 500 + лог ошибки (что-то очень не так) |
## Host-side
### ВМ
- 2 vCPU (100%), 4 GB RAM, 25 GB network-hdd.
- Ubuntu 22.04 LTS.
- Без публичного IP при возможности (все исходящие к Gitea — через NAT
или внутренний адрес).
- Привязан SA `runner-self-stop`.
- Регистрация в Gitea Actions делается **один раз** при первой настройке.
Registration token берётся в Site Admin → Actions → Runners, кладётся
в Vault. Плейбук проверяет наличие `.runner` файла на ВМ; если есть —
пропускает регистрацию.
### Плейбук `playbook-gitea-runner.yml`
Стандартная структура проекта:
- `roles/owner` — пользователь `gitea-runner` (uid/gid выделить
отдельные, в группе `docker`).
- `files/gitea-runner/`:
- `docker-compose.template.yml``act_runner` в docker
(`gitea/act_runner:<pinned>`), `restart: unless-stopped`, mount
docker socket для запуска job-контейнеров.
- `act-runner-config.template.yaml` — конфиг раннера.
- `runner-probe.template.py` + `runner-probe.template.service` +
`runner-probe.template.timer`.
- `runner-decide.template.py` + `runner-decide.template.service` +
`runner-decide.template.timer`.
- `samples-logrotate.template.conf` — ротация `samples.log`.
Расширения шаблонов — `.template.<ext>`, не `.j2` (соглашение проекта).
### Раннер в docker
`act_runner` стартует через `docker compose up -d` под пользователем
`gitea-runner`. Поскольку `restart: unless-stopped`, дополнительный
systemd-юнит для самого раннера не нужен — после `Start` ВМ docker
поднимет контейнер автоматически.
Идентификатор контейнера фиксированный (`gitea_runner_app`), чтобы probe
мог исключать его из счёта.
### Probe и decide
Два независимых юнита, телеметрия — append-only лог.
`runner-probe` (timer раз в 30 сек):
```bash
# pseudocode
busy_count=$(docker ps -q | grep -v <runner_container_id> | wc -l)
state=$([ "$busy_count" -gt 0 ] && echo busy || echo idle)
echo "$(date -u +%FT%TZ) $state containers=$busy_count" \
>> /var/lib/runner-idle/samples.log
```
В реальной реализации — Python, фильтр по docker SDK или по результату
`docker ps --format '{{.Names}}'`.
`runner-decide` (timer раз в 1 мин):
1. Читает хвост `samples.log`.
2. Находит `last_busy_at` — timestamp последней `busy`-строки.
3. Находит `last_sample_at` — timestamp последней любой строки.
4. Логика:
- `now - last_sample_at > STALE_THRESHOLD` (5 мин) → **probe сломан**,
не гасим, логируем error. Алерт CM поймает по uptime.
- `now - last_busy_at > IDLE_THRESHOLD` (10 мин) → `instances:stop`
через REST.
- Иначе → ничего.
Параметры (`IDLE_THRESHOLD`, `STALE_THRESHOLD`) — переменные в шаблоне,
тюнятся по эксплуатации.
### Самогашение через REST
Без `yc` CLI. Decide-скрипт получает IAM-токен из metadata-сервиса и
дёргает Compute REST:
```python
TOKEN_URL = "http://169.254.169.254/computeMetadata/v1/instance/" \
"service-accounts/default/token"
ID_URL = "http://169.254.169.254/computeMetadata/v1/instance/id"
HEADERS = {"Metadata-Flavor": "Google"}
token = requests.get(TOKEN_URL, headers=HEADERS).json()["access_token"]
instance_id = requests.get(ID_URL, headers=HEADERS).text
requests.post(
f"https://compute.api.cloud.yandex.net/compute/v1/instances/{instance_id}:stop",
headers={"Authorization": f"Bearer {token}"},
)
```
Никаких файлов с SA-key, никаких зависимостей сверх `python3 +
requests`.
## Страховки от зависшей ВМ
Главный failure mode — probe или decide молча сломались, ВМ работает 24/7.
Слой 1 — soft idle-stop в decide. Нормальная работа.
Слой 2 — probe-staleness в decide. Если `samples.log` не обновляется
дольше `STALE_THRESHOLD` — логируем error, **не гасим** (мог идти
длинный билд). Полагаемся на слой 3.
Слой 3 — внешний алерт через Cloud Monitoring на uptime ВМ > 24 ч. Не
дёргает остановку, только нотификация. Порог высокий, чтобы дни активной
отладки не триггерили его. Если фактически висит сутки — это сигнал
смотреть руками.
Hard-cap по uptime внутри decide **не делаем**: ломает кейс «активно
тестирую несколько часов подряд», когда busy-сэмплы есть и логика идёт
правильно.
## Секреты (Vault, `vars/secrets.yml`)
| Ключ | Назначение |
| --------------------------------- | ----------------------------------------------------- |
| `gitea_runner_registration_token` | одноразовый токен для `act_runner register` |
| `gitea_webhook_secret` | общий с функцией HMAC-секрет для webhook |
| `yc_runner_folder_id` | в каком фолдере живёт ВМ |
| `yc_runner_instance_id` | ID ВМ (заполняется после bootstrap) |
| `yc_runner_function_url` | URL функции для webhook (заполняется после bootstrap) |
## invoke-таски
| Таск | Что делает |
| ----------------------------- | ----------------------------------------------------------------------------- |
| `inv runner-bootstrap` | one-time: создаёт SA, ВМ, функцию, алерт. Идемпотентен. |
| `inv runner-deploy-function` | заливает новую версию `runner-starter`. |
| `inv runner-pl` | up → `ansible-playbook playbook-gitea-runner.yml` → down. С `try/finally`. |
| `inv runner-up` / `down` | ручной старт/стоп ВМ для дебага. |
| `inv runner-status` | state ВМ + хвост `samples.log` (через ssh). |
| `inv runner-ssh` | ssh на ВМ, поднимает её при необходимости. |
`runner-pl` — основной таск, единственный «штатный» путь обновления
конфига ВМ. Если плейбук падает посередине, `finally` всё равно гасит ВМ
(idle-watch её и так бы погасил, но явное лучше).
## Стоимость
Базовая ставка YC (USD, после повышения 1 мая 2026): vCPU 100% =
$0.010164/ч, RAM = $0.002705/ГБ·ч, network-hdd = $0.0000356/ГБ·ч.
Профиль: 10,75 ч активной ВМ в месяц.
| Конфиг (2 vCPU 100%, 4 GB RAM, 25 GB HDD) | $/мес |
| ----------------------------------------- | ----- |
| Compute (vCPU + RAM) при 10,75 ч | ~0.33 |
| Disk (HDD, 24/7) | ~0.64 |
| Cloud Function, Monitoring | 0.00 |
| **Итого** | **~1.0** |
Сравнение: эта же ВМ в режиме 24/7 ≈ $23/мес. Экономия — порядка 95%.
Дальнейшая оптимизация — диск (15 GB вместо 25, ещё ~$0.25/мес). Делать
не сейчас.
## Принятые риски
- **Общий фолдер с другими ВМ.** SA `runner-self-stop` теоретически
может погасить и Gitea-сервер, если тот переедет в YC рядом. Митигация
при появлении такой ВМ — перенос в отдельный фолдер.
- **Холодный старт ~60 сек.** Дизайн заявляет 40, реальность ближе к
60 (Ubuntu boot + docker pull + act_runner connect). Документируем как
«нормальная задержка первой джобы».
- **Регистрационный токен утерян.** При пересоздании диска ВМ нужен
новый токен из Gitea UI. Документируем процесс. Раз в годы — терпимо.
- **Probe сломан, ВМ висит.** Поймает алерт CM, ручное расследование.
## План внедрения
1. Создать в YC: 2 SA, ВМ, дисковый ресурс. Через `inv
runner-bootstrap` или вручную через консоль (выбираем по желанию на
этапе реализации).
2. Прогнать `inv runner-pl` на свежесозданной ВМ. С временно
уменьшенным `IDLE_THRESHOLD` (2 мин вместо 10) — чтобы тестировать
гашение быстро.
3. Зарегистрировать раннер в Gitea руками: получить registration token,
положить в Vault, повторить `inv runner-pl`.
4. Проверить, что раннер появился в Gitea UI и забирает тестовую джобу.
5. Проверить idle-watch: дать ВМ постоять, убедиться, что гасится.
6. Создать функцию `runner-starter` через `inv runner-deploy-function`.
Проверить ручным `yc serverless function invoke`.
7. Прописать System Webhook в Gitea на URL функции, секрет совпадает с
Vault.
8. Тестовый push → end-to-end проверка.
9. Поднять `IDLE_THRESHOLD` обратно до 10 мин.
10. Настроить алерт Cloud Monitoring на uptime > 24 ч.
11. Неделя наблюдения: лог функции, samples.log, uptime ВМ, счёт.
## Открытые вопросы
- **Канал нотификаций** для алерта Cloud Monitoring (Telegram, ntfy,
email) — выбрать на этапе настройки.
- **Тип executor** в act_runner — docker (по умолчанию) или host. Ходим
через docker, host-executor пока не обсуждается.
- **Webhook на pull request** — нужно или только push? По умолчанию
только push. Расширим, если возникнет PR-flow.
- **Перенос ВМ в отдельный фолдер** — когда в общем появится вторая
ВМ. Пока не критично.