Compare commits

..

3 Commits

Author SHA1 Message Date
av 313b1820be Update readme
Linting / YAML Lint (push) Has been cancelled
Linting / Ansible Lint (push) Has been cancelled
2026-05-24 14:55:23 +03:00
av 2f2c1b0754 Add ADR after migration to timeweb cloud 2026-05-24 14:51:03 +03:00
av e45e1db002 Add architecture decision record templates 2026-05-24 14:35:35 +03:00
6 changed files with 329 additions and 2 deletions
+81
View File
@@ -0,0 +1,81 @@
---
name: adr
description: >-
Создаёт и сопровождает Architecture Decision Records (ADR) в docs/adr/.
Используй, когда уже сделанное архитектурное или инфраструктурное
изменение нужно зафиксировать постфактум: выбор инструмента/подхода,
структурное решение, намеренный отказ от очевидного варианта, либо
прямая просьба «записать решение / завести ADR». А также когда старую
ADR нужно пометить заменённой или устаревшей. ADR пишут ПОСТФАКТУМ;
идеи, планы и обсуждения — это drafts, а не ADR.
---
# Работа с ADR
ADR живут в `docs/adr/`. Формат и соглашения — `docs/adr/README.md`,
шаблон — `docs/adr/template.md`. Читай README перед первой записью в
сессии: правила там — источник истины, эта инструкция лишь даёт порядок
действий.
Язык записей — **русский**, стиль — как в `docs/drafts/`: конкретно,
по делу, без воды. Калибровка под личный хобби-сервер: не раздувай
запись, не предлагай корпоративные процессы.
## Сначала реши, нужен ли ADR
Заводи, если изменение **уже сделано** и это: выбор технологии/
инструмента, структурное решение, решение с долгосрочными последствиями
или дорогим откатом, либо намеренный отказ от очевидного варианта.
Не заводи:
- для рутины (бамп версии образа, сервис по накатанной схеме) и того,
что видно из кода/git;
- для идей, планов и того, что ещё не реализовано — это `docs/drafts/`,
а не ADR.
Если сомневаешься — спроси пользователя, не плоди записи молча.
## Создание новой ADR
1. **Дата.** Когда изменение реально сделано. Обычно сегодня
(`date +%F`). Если оформляем задним числом — уточни дату у
пользователя, не подставляй сегодняшнюю вслепую.
2. **Идентификатор и имя файла:** `ADR-ГГГГ-ММ-ДД-kebab-slug.md`
(slug — латиницей). Если за эту дату уже есть ADR — slug просто
должен отличаться; проверь `ls docs/adr/`.
3. **Файл.** Возьми за основу `docs/adr/template.md`.
4. **Заполни** по шаблону:
- Заголовок `# Человеческий заголовок` (без даты/ID в тексте).
- Метаданные: только `- Дата: ГГГГ-ММ-ДД`. **Строку статуса не
добавляй** — у активной записи статуса нет.
- **Контекст** — какая проблема и ограничения вынудили это делать.
- **Рассмотренные варианты** — *опциональная* секция. **Спроси
пользователя, рассматривал ли он какие-то альтернативы.** Если да —
перечисли их с плюсами/минусами (особенно те, что отвергнуты); если
нет (решение было единственным очевидным) — удали секцию целиком.
- **Решение** — что сделано и, **главное, почему**: какое намерение и
причина за этим стоят.
- **Последствия** — `+`/`-` и что нужно сделать как следствие.
5. **Индекс.** Добавь строку в таблицу «Список записей» в
`docs/adr/README.md`, **сверху** (новые сверху). Статус — `—`.
Самое важное в ADR — сохранить **почему**: намерение и причинность, а не
просто «что сделали». Не придумывай причины и альтернативы за
пользователя — если их нет в контексте сессии, обязательно спроси: что
подтолкнуло к изменению и какие варианты рассматривались.
## Замена / устаревание решения
Старые ADR неизменяемы. Если решение пересмотрено:
1. Заведи новую ADR (шаги выше). В её «Контексте» — строка
«Заменяет ADR-ГГГГ-ММ-ДД-slug».
2. В старой ADR добавь строку статуса сразу под датой:
`- Статус: заменено на ADR-ГГГГ-ММ-ДД-slug` (или `- Статус: устарело`,
если замены нет). Тело не трогай.
3. Обнови статус старой записи в индексе `README.md`.
## После записи
Покажи пользователю путь к файлу и кратко содержание. Не коммить без
явной просьбы.
+11 -2
View File
@@ -5,6 +5,9 @@
> В этом проекте не самые оптимальные решения.
> Но они помогают мне поддерживать сервер для моих личных проектов уже много лет.
История и обоснования значимых решений — в [ADR-записях](docs/adr/)
(`docs/adr/`): *почему* приняты те или иные изменения, а не только что сделано.
## Требования
- [uv](https://docs.astral.sh/uv/)
@@ -40,11 +43,17 @@ uv run ansible-galaxy install --role-file requirements.yml
Деплой приложения через ansible:
```bash
uv run ansible-playbook ansible-playbook -i production.yml --diff playbook-gitea.yml
uv run ansible-playbook ansible-playbook -i timeweb.yml --diff playbook-gitea.yml
```
Или через таску invoke:
```bash
./inv pl -- gitea
```
## Удаление приложения <name>
```bash
uv run ansible-playbook -i production.yml --diff playbook-remove-user-and-app.yml --extra-vars user_name=<name>
uv run ansible-playbook -i timeweb.yml --diff playbook-remove-user-and-app.yml --extra-vars user_name=<name>
```
@@ -0,0 +1,54 @@
# Вести историю решений в виде ADR
- Дата: 0000-00-00
> Основополагающая запись о самом процессе ADR. Дата-сентинел
> `0000-00-00` (фактически создана 2026-05-24) — исключение: так запись
> всегда остаётся в самом низу списка и не путается с реальными
> изменениями.
## Контекст
Сервер развивается итеративно: меняются прокси, схема бэкапов, набор
сервисов, провайдер хостинга. Решения принимаются по одному, часто с
неочевидными компромиссами под ресурсы сервера и стоимость. Через
несколько месяцев мотивация забывается, и возникает риск «переоткрыть»
уже отвергнутый вариант или сломать то, что было сделано осознанно.
Журналы в `docs/drafts/` фиксируют хронологию и черновики, но не
обоснование выбора — по ним не видно, какие альтернативы отвергнуты и
почему.
## Рассмотренные варианты
- **ADR (отдельный файл на решение)** — стандартный формат, каждая
запись неизменяема, видно эволюцию через накопление и замену записей.
- **Один changelog-файл** — проще вести, но правки затирают историю
рассуждений, и формат расплывается со временем.
- **Ничего, держать в голове / в git-сообщениях** — нулевые затраты,
но обоснование теряется, а git-история не отвечает на вопрос «почему».
## Решение
Заводим каталог `docs/adr/` с записями в формате ADR (Nygard + блок
«Рассмотренные варианты»). Идентификатор записи — датовый,
`ADR-ГГГГ-ММ-ДД-slug`: дата (когда изменение реально сделано) сразу
видна в списке и позволяет оформлять записи задним числом. ADR пишем
постфактум, поэтому жизненный цикл сведён к двум статусам у потерявших
силу записей — `заменено на` и `устарело`; идеи и планы остаются в
`docs/drafts/`.
Формат и процесс описаны в [`README.md`](README.md), шаблон — в
[`template.md`](template.md). Для единообразия заполнение
автоматизировано скиллом `adr` (`.claude/skills/adr/`).
## Последствия
- `+` Сохраняется обоснование решений и отвергнутые альтернативы.
- `+` Датовый ID даёт хронологию «из коробки» и не мешает оформлять
записи задним числом.
- `+` Единый формат: записи делает человек или агент по одному шаблону.
- `-` Небольшая дисциплина: сделав значимое изменение, нужно не забыть
оформить ADR.
- Скилл `adr` берёт на себя имя файла, шаблон и обновление индекса в
`README.md`, снижая трение.
@@ -0,0 +1,79 @@
# Переезд сервера с Yandex Cloud на Timeweb VPS
- Дата: 2026-05-23
## Контекст
`rivendell-v2` жил на VM в Yandex Cloud. Одновременно копились три
проблемы:
- **Цена.** ≈ 2 887 ₽/мес за конфигурацию, которую другие провайдеры
дают дешевле и мощнее.
- **Потолок RAM.** 4 ГБ, ≈ 80 % заняты на штатной нагрузке. Любой
всплеск (миграции БД, индексация Outline, restic) — конкуренция за
память и риск OOM. Расти на этом тарифе дальше — только заметно
дороже.
- **Медленный диск.** Чтобы сдержать цену в YC, использовался дешёвый
HDD вместо SSD/NVMe — страдала отзывчивость (Gitea, Outline, тёплый
старт контейнеров, restic check/forget).
Это личный сервер — допустим мягкий даунтайм.
## Рассмотренные варианты
- **Остаться в YC, поднять тариф** (больше RAM/SSD на месте). Отвергнуто:
YC уже дороже альтернатив, апгрейд поднимает цену непропорционально
приросту — те же три проблемы решаются дороже, чем переездом.
- **Свой / домашний сервер** (железо под контролем, без ежемесячной
аренды). Отвергнуто: дома нет надёжного аптайма 24/7 (питание,
интернет-канал, железо), а сервисы должны быть всегда доступны.
- **Переезд на Timeweb Cloud VPS** — выбранный вариант.
## Решение
Переносим на Timeweb Cloud VPS: ≈ 1 980 ₽/мес, 8 ГБ RAM (×2), 4 ядра
(×2, гарантия CPU 100 % вместо 50 %), 80 ГБ NVMe вместо 120 ГБ HDD.
Один переезд закрывает все три причины сразу.
Рамки решения:
- Переезжает **только compute** (VM с приложениями). S3 (restic, бекапы),
Container Registry, Postbox SMTP и DNS-зона `vakhrushev.me` остаются
в Yandex и используются с новой машины.
- Стратегия — **cold cutover**: погасить сервисы на источнике, раскатать
ansible на новом сервере без запуска приложений (сохраняя uid/gid), перенести
данные `rsync`'ом, запустить, переключить DNS.
- Диск: фаза 1 — один 80 ГБ NVMe (всего 22 ГБ данных, влезает с
запасом). «Холодный» второй диск под крупные данные — отдельная
фаза 2, не на критическом пути.
- Источник не удаляется сразу после cutover: держим «холодным запасным»
пару недель ради отката.
Детальный план — [`../drafts/timeweb.md`](../drafts/timeweb.md),
фактическое выполнение —
[`../drafts/timeweb-migration-log.md`](../drafts/timeweb-migration-log.md).
## Последствия
- `+` 907 ₽/мес (≈ −31 %) при вдвое большем RAM и CPU и NVMe-диске —
закрыты все три исходные проблемы.
- `+` Запас по RAM убирает OOM-риск при всплесках нагрузки.
- `+` Диверсификация по облакам: раньше сервер и данные были в одном
аккаунте Yandex Cloud, теперь compute в Timeweb, а бэкапы (S3) — в
Yandex. Если заблокируют или потеряем доступ к одному провайдеру,
данные остаются доступны через другой.
- `-` Диск меньше (80 ГБ NVMe против 120 ГБ HDD), но сейчас занят
примерно наполовину — запас есть, фаза 2 с холодным диском не срочная.
- `-` Сохраняется зависимость от Yandex Cloud (S3, Container Registry,
Postbox SMTP, DNS) — переезд её не устраняет.
- `-` Timeweb активно блокирует Telegram (в отличие от YC) — интеграция
отвалилась. Затронуты `transcriber`, `remembos` и нотификации о
бэкапах. Ожидаемо; нотификации остались через почту, второй канал
рассматривается через Matrix.
- `-` Из-за тех же блокировок Timeweb перестали обновляться некоторые
RSS-фиды в `miniflux`.
- `-` Для доступа к `cr.yandex` вне YC появился долгоживущий OAuth-токен
Яндекса в vault (`yc_oauth_token`) с широким blast radius. При желании
сузить — IAM-ключ сервисного аккаунта отдельной итерацией.
- Инвентарь временно раздвоен (`production.yml` + `timeweb.yml`); после
стабилизации источник удаляется, `timeweb.yml``production.yml`.
+67
View File
@@ -0,0 +1,67 @@
# Architecture Decision Records (ADR)
Журнал значимых архитектурных и инфраструктурных решений по серверу.
Одна запись — одно решение. ADR пишем **постфактум**, когда изменение
уже сделано: идеи, планы и обсуждения живут в [`../drafts/`](../drafts),
а в ADR попадает только то, что реализовано. Записи **неизменяемы**:
передумали → не правим старую, а заводим новую и помечаем старую.
Чем ADR отличается от журналов в `../drafts/`: drafts — оперативная
хроника и черновики («что делаю / собираюсь сделать»), ADR — фиксация
выбора и его обоснования («почему сделал так, а не иначе»).
Главная ценность записи — сохранить **почему**: намерение и причинность.
Это важнее аккуратности оформления и полноты остальных секций.
## Когда заводить ADR
- Выбор технологии или инструмента (Caddy vs Nginx, restic vs borg).
- Структурные решения (схема бэкапов, организация плейбуков, сеть).
- Решения с долгосрочными последствиями или дорогим откатом.
- **Намеренный отказ** от очевидного подхода — чтобы потом не
переоткрывать «а почему мы не сделали X».
Не заводить для рутины (бамп версии образа, добавление сервиса по
накатанной схеме) и того, что и так видно из кода и git.
## Соглашения
- **Имя файла = идентификатор:** `ADR-ГГГГ-ММ-ДД-kebab-slug.md`.
Идентификатор — имя без `.md` (например `ADR-2026-05-24-adr-process`).
Slug — латиницей.
- **Дата** — когда изменение реально сделано (можно задним числом, а не
дата оформления записи).
- Несколько ADR за один день различаются по slug.
- **Заголовок в файле:** `# Человеческий заголовок` (без даты и ID — они
в имени файла и в строке «Дата»).
- Секция **«Рассмотренные варианты» — опциональна**: оставляй её, только
если альтернативы реально рассматривались.
- Шаблон новой записи — [`template.md`](template.md).
- Исключение: основополагающая мета-запись о самом процессе ADR
использует дату-сентинел `0000-00-00`, чтобы всегда оставаться в самом
низу списка. Реальные записи такую дату не используют.
## Статусы
Активная запись статуса **не имеет**. Статус появляется, только когда
запись теряет силу, и значений всего два:
- `заменено на ADR-ГГГГ-ММ-ДД-slug` — решение пересмотрено новой ADR.
- `устарело` — решение потеряло смысл и замены нет.
## Замена и устаревание
1. Заводим новую ADR; в её «Контексте» — строка
«Заменяет ADR-ГГГГ-ММ-ДД-slug».
2. В старой ADR добавляем строку `- Статус: заменено на ADR-…` сразу под
датой. Тело не трогаем — это часть истории.
3. Обновляем статус старой записи в индексе ниже.
## Список записей
Новые сверху.
| Дата | Запись | Статус |
| ---------- | ------------------------------------------------------------------------------------- | ------ |
| 2026-05-23 | [Переезд сервера с Yandex Cloud на Timeweb VPS](ADR-2026-05-23-migrate-to-timeweb.md) | — |
| 0000-00-00 | [Вести историю решений в виде ADR](ADR-0000-00-00-record-architecture-decisions.md) | — |
+37
View File
@@ -0,0 +1,37 @@
# Краткий заголовок решения
- Дата: ГГГГ-ММ-ДД
<!-- Строку статуса добавляют позже, только если запись потеряла силу:
- Статус: заменено на ADR-ГГГГ-ММ-ДД-slug
- Статус: устарело
У активной записи строки статуса нет. -->
## Контекст
Что вынудило сделать изменение: проблема, силы и ограничения (ресурсы
сервера, стоимость, время на поддержку, существующая архитектура).
Пиши так, чтобы через год было понятно «почему это вообще делалось»
без чтения переписки.
## Рассмотренные варианты
<!-- Опциональная секция. Оставь, только если варианты реально
рассматривались. Если решение было единственным очевидным —
удали её, а причину объясни в «Решении». -->
- **Вариант A** — суть, плюсы и минусы.
- **Вариант B** — суть, плюсы и минусы.
- **Вариант C** — если отвергнут сразу, коротко почему.
## Решение
Что именно сделано и — главное — **почему**: какое намерение и какая
причина за этим стоят. Если варианты рассматривались — почему выбран
этот, а не остальные.
## Последствия
- `+` что стало лучше, какие возможности открылись.
- `-` чем платим: новые ограничения, риски, регулярная нагрузка на
поддержку.
- Что нужно сделать как следствие (если есть).