Docs: add docs and drafts
This commit is contained in:
@@ -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.
|
||||
- **Перенос ВМ в отдельный фолдер** — когда в общем появится вторая
|
||||
ВМ. Пока не критично.
|
||||
Reference in New Issue
Block a user