Переезд: деплой-конвейер разворачивается наизнанку — раннеры на Beget катят на Selectel #3029

Open
opened 2026-08-21 12:55:13 +00:00 by lekss361 · 6 comments
Owner

Эпик: #2989

Что меняется

Сейчас Forgejo, раннеры и прод живут на одной машине, и деплой по сути локальный. После переезда раннеры остаются на Beget, а цель деплоя переезжает на Selectel — конвейер начинает ходить по SSH через интернет.

Что перенастроить

  • Deploy-ключ — новая пара, публичная часть на Selectel, приватная в секретах Forgejo Actions. Старый ключ отозвать, а не оставить «на всякий случай».
  • Хост назначения в обоих workflow (deploy.yml, deploy-tradein.yml) — сейчас это фактически localhost-семантика.
  • known_hosts — раннер должен знать фингерпринт нового хоста, иначе первый деплой встанет на интерактивном подтверждении или, хуже, будет обходить проверку.
  • Секреты Actions — пересмотреть весь набор: что было безопасно передавать внутри одной машины, теперь уходит по сети.
  • Сеть: SSH на Selectel не должен быть открыт всему интернету — ограничить по адресу раннера.

Ловушка, уже случавшаяся

deploy #1951 пересоздал tradein-scraper посреди полного прохода ЦИАН — прогон отменился (cancelled, startup-reap). На междоузловом деплое окно такой гонки шире. Предусмотреть, чтобы деплой не рвал длинные скрап-прогоны: либо дренаж, либо отказ пересоздавать контейнер при активном run.

Приёмка

  • Новый deploy-ключ выпущен, старый отозван
  • Оба workflow катят на новый хост, known_hosts заполнен без обхода проверки
  • SSH на Selectel ограничен по источнику
  • Пробный деплой прошёл end-to-end с Beget на Selectel
  • Длинный скрап-прогон не рвётся деплоем
Эпик: #2989 ## Что меняется Сейчас Forgejo, раннеры и прод живут на одной машине, и деплой по сути локальный. После переезда **раннеры остаются на Beget, а цель деплоя переезжает на Selectel** — конвейер начинает ходить по SSH через интернет. ## Что перенастроить - **Deploy-ключ** — новая пара, публичная часть на Selectel, приватная в секретах Forgejo Actions. Старый ключ отозвать, а не оставить «на всякий случай». - **Хост назначения** в обоих workflow (`deploy.yml`, `deploy-tradein.yml`) — сейчас это фактически localhost-семантика. - **`known_hosts`** — раннер должен знать фингерпринт нового хоста, иначе первый деплой встанет на интерактивном подтверждении или, хуже, будет обходить проверку. - **Секреты Actions** — пересмотреть весь набор: что было безопасно передавать внутри одной машины, теперь уходит по сети. - **Сеть**: SSH на Selectel не должен быть открыт всему интернету — ограничить по адресу раннера. ## Ловушка, уже случавшаяся `deploy #1951` пересоздал `tradein-scraper` **посреди полного прохода ЦИАН** — прогон отменился (`cancelled`, `startup-reap`). На междоузловом деплое окно такой гонки шире. Предусмотреть, чтобы деплой не рвал длинные скрап-прогоны: либо дренаж, либо отказ пересоздавать контейнер при активном run. ## Приёмка - [ ] Новый deploy-ключ выпущен, старый отозван - [ ] Оба workflow катят на новый хост, `known_hosts` заполнен без обхода проверки - [ ] SSH на Selectel ограничен по источнику - [ ] Пробный деплой прошёл end-to-end с Beget на Selectel - [ ] Длинный скрап-прогон не рвётся деплоем
lekss361 added the
chore
priority/p1
scope/devops
tradein
labels 2026-08-21 12:57:08 +00:00
Author
Owner

Разобрал последний пункт приёмки — «длинный скрап-прогон не рвётся деплоем». Он уже реализован, но не закрывает проблему, и это видно по проду. Пишу, чтобы никто не переписал заново то, что есть, и не считал пункт сделанным.

Что уже стоит в deploy-tradein.yml (после #1951)

Три меры, все в shell деплой-скрипта:

  1. Атомарный recreatebrowser backend frontend tgbot [scraper] поднимаются одним docker compose up -d, а не двумя последовательными командами. Раньше scraper пересоздавался второй командой, уже после остальных — это и было окно гонки.
  2. Graceful drain — если scraper будет пересоздан, ждём до 5 минут (poll 10 с), пока COUNT(*) FROM scrape_runs WHERE status='running' не станет 0. Сделано аккуратно: неудачное чтение psql не считается за «слито» (иначе дренаж превращался бы в no-op ровно в момент проблем с БД).
  3. Startup-reap — сразу после recreate помечаются cancelled те running-строки, чей heartbeat не рос с отметки, взятой по часам самой БД перед stop. Не zombie, а именно cancelled — чтобы «убит деплоем» отличалось от «непонятно завис».

Плюс SCRAPER_RECREATE сравнивает digest подтянутого образа с тем, на котором бежит контейнер: если образ не изменился, drain пропускается и in-flight прогоны не трогаются вовсе (#2679).

Почему пункт всё равно не закрыт

Замер на проде, 30 дней:

status n
done 2312
banned 96
failed 35
cancelled 10
zombie 9

Все десять cancelled — длинные прогоны, и убиты они спустя от 26 минут до 3 часов работы:

source дата прожил до отмены
avito_full_load_exhaustive 23.08 26 мин
cian_full_load 20.08 41 мин
yandex_city_sweep 15.08 65 мин
yandex_city_sweep 12.08 2 ч 33 мин
avito_full_load_exhaustive 09.08 2 ч 58 мин
cian_full_load 31.07 2 ч 10 мин

То есть примерно один убитый прогон каждые три дня, и это ровно самые дорогие прогоны — те, что собирают город целиком.

Пятиминутный дренаж им помочь не может по построению: cian_full_load идёт ~400 минут. Никакой деплой не будет ждать столько, и не должен. Дренаж спасает короткие прогоны, а убивают деплои — длинные.

Что из этого следует для переезда

Междоузловой деплой окно гонки не создаёт, а расширяет — механизм остаётся тем же, добавляется только сетевая задержка на каждом шаге. Так что переписывать под Selectel тут нечего; менять надо саму стратегию, и это уже не про переезд.

Варианты, по возрастанию цены:

  1. Не пересоздавать scraper, если бежит длинный прогон, а отложить — деплой катит остальные сервисы, scraper остаётся на старом образе до следующего окна. Уже есть половина механизма (SCRAPER_RECREATE + сравнение digest'ов), не хватает ветки «образ новый, но прогон длинный → отложить, а не ждать 5 минут и убить».
  2. Окно деплоя — не катить в часы, когда по расписанию идут full_load. Дёшево, но хрупко: расписание плавает.
  3. Чекпоинты, с которых прогон продолжается — правильно, но это уже правка Python в скрапперах, не devops.

Вариант 1 выглядит лучшим соотношением: он ничего не ломает (сейчас в этой ситуации прогон и так теряется, отложенный recreate строго не хуже), не требует правок в Python и переиспользует уже написанное.

Развилка за владельцем: согласиться, что scraper может на несколько часов отставать от backend по образу. Оба бегут ОДИН образ (#2679), так что расхождение реально — например, миграция уже применена, а scraper на старом коде. Это ровно тот компромисс, который #2679 закрывал в другую сторону, поэтому решать не мне.

Остальные пункты приёмки

Без владельца не закрываемы: выпуск и отзыв deploy-ключа, секреты Actions, ограничение SSH по адресу раннера — всё это работа с секретами и файрволом прода. Готов сделать по команде, но не молча.

Разобрал последний пункт приёмки — «длинный скрап-прогон не рвётся деплоем». **Он уже реализован**, но не закрывает проблему, и это видно по проду. Пишу, чтобы никто не переписал заново то, что есть, и не считал пункт сделанным. ## Что уже стоит в `deploy-tradein.yml` (после #1951) Три меры, все в shell деплой-скрипта: 1. **Атомарный recreate** — `browser backend frontend tgbot [scraper]` поднимаются одним `docker compose up -d`, а не двумя последовательными командами. Раньше scraper пересоздавался второй командой, уже после остальных — это и было окно гонки. 2. **Graceful drain** — если scraper будет пересоздан, ждём до 5 минут (poll 10 с), пока `COUNT(*) FROM scrape_runs WHERE status='running'` не станет 0. Сделано аккуратно: неудачное чтение psql **не** считается за «слито» (иначе дренаж превращался бы в no-op ровно в момент проблем с БД). 3. **Startup-reap** — сразу после recreate помечаются `cancelled` те `running`-строки, чей heartbeat не рос с отметки, взятой по часам **самой БД** перед stop. Не `zombie`, а именно `cancelled` — чтобы «убит деплоем» отличалось от «непонятно завис». Плюс `SCRAPER_RECREATE` сравнивает digest подтянутого образа с тем, на котором бежит контейнер: если образ не изменился, drain пропускается и in-flight прогоны не трогаются вовсе (#2679). ## Почему пункт всё равно не закрыт Замер на проде, 30 дней: | status | n | |---|---| | done | 2312 | | banned | 96 | | failed | 35 | | **cancelled** | **10** | | zombie | 9 | Все десять `cancelled` — длинные прогоны, и убиты они спустя от 26 минут до 3 часов работы: | source | дата | прожил до отмены | |---|---|---| | avito_full_load_exhaustive | 23.08 | 26 мин | | cian_full_load | 20.08 | 41 мин | | yandex_city_sweep | 15.08 | 65 мин | | yandex_city_sweep | 12.08 | **2 ч 33 мин** | | avito_full_load_exhaustive | 09.08 | **2 ч 58 мин** | | cian_full_load | 31.07 | 2 ч 10 мин | То есть примерно **один убитый прогон каждые три дня**, и это ровно самые дорогие прогоны — те, что собирают город целиком. Пятиминутный дренаж им помочь не может **по построению**: `cian_full_load` идёт ~400 минут. Никакой деплой не будет ждать столько, и не должен. Дренаж спасает короткие прогоны, а убивают деплои — длинные. ## Что из этого следует для переезда Междоузловой деплой окно гонки **не создаёт, а расширяет** — механизм остаётся тем же, добавляется только сетевая задержка на каждом шаге. Так что переписывать под Selectel тут нечего; менять надо саму стратегию, и это уже не про переезд. Варианты, по возрастанию цены: 1. **Не пересоздавать scraper, если бежит длинный прогон, а отложить** — деплой катит остальные сервисы, scraper остаётся на старом образе до следующего окна. Уже есть половина механизма (`SCRAPER_RECREATE` + сравнение digest'ов), не хватает ветки «образ новый, но прогон длинный → отложить, а не ждать 5 минут и убить». 2. **Окно деплоя** — не катить в часы, когда по расписанию идут full_load. Дёшево, но хрупко: расписание плавает. 3. **Чекпоинты, с которых прогон продолжается** — правильно, но это уже правка Python в скрапперах, не devops. Вариант 1 выглядит лучшим соотношением: он ничего не ломает (сейчас в этой ситуации прогон **и так** теряется, отложенный recreate строго не хуже), не требует правок в Python и переиспользует уже написанное. **Развилка за владельцем:** согласиться, что scraper может на несколько часов отставать от backend по образу. Оба бегут ОДИН образ (#2679), так что расхождение реально — например, миграция уже применена, а scraper на старом коде. Это ровно тот компромисс, который #2679 закрывал в другую сторону, поэтому решать не мне. ## Остальные пункты приёмки Без владельца не закрываемы: выпуск и отзыв deploy-ключа, секреты Actions, ограничение SSH по адресу раннера — всё это работа с секретами и файрволом прода. Готов сделать по команде, но не молча.
Author
Owner

Решение владельца (2026-08-24) по последнему пункту приёмки «длинный скрап-прогон не рвётся деплоем»: вариант 3 — чекпоинты, с которых прогон продолжается. Отмечено, что это правка Python в скрапперах, а не devops.

Поэтому пункт вынесен из этой задачи в #3074 (scope/backend / scrapers) — здесь он больше не считается открытым, а #3029 остаётся про перенастройку деплой-конвейера под переезд.

Что осталось в #3029 и по-прежнему требует владельца — работа с секретами и файрволом прода, молча её делать нельзя:

  • выпустить новый deploy-ключ, отозвать старый
  • пересмотреть набор секретов Actions (то, что раньше не покидало машину, теперь уходит по сети)
  • ограничить SSH на Selectel по адресу раннера
  • заполнить known_hosts фингерпринтом нового хоста
  • пробный деплой end-to-end Beget → Selectel

Отдельная развилка, которую решение по чекпоинтам не закрывает: чекпоинты появятся не мгновенно, а деплой продолжает убивать примерно один длинный прогон в три дня. Вариант 1 (не пересоздавать scraper при активном длинном прогоне, а отложить recreate) доступен прямо сейчас, не требует Python и строго не хуже сегодняшнего поведения. Цена — scraper может на несколько часов отставать от backend по образу, а образ у них один (#2679). Ставим эту заплатку на время до #3074 или живём как есть — решение за владельцем.

Refs #3074

Решение владельца (2026-08-24) по последнему пункту приёмки «длинный скрап-прогон не рвётся деплоем»: **вариант 3 — чекпоинты, с которых прогон продолжается**. Отмечено, что это правка Python в скрапперах, а не devops. Поэтому пункт **вынесен из этой задачи** в #3074 (`scope/backend` / `scrapers`) — здесь он больше не считается открытым, а #3029 остаётся про перенастройку деплой-конвейера под переезд. Что осталось в #3029 и по-прежнему требует владельца — работа с секретами и файрволом прода, молча её делать нельзя: - [ ] выпустить новый deploy-ключ, отозвать старый - [ ] пересмотреть набор секретов Actions (то, что раньше не покидало машину, теперь уходит по сети) - [ ] ограничить SSH на Selectel по адресу раннера - [ ] заполнить `known_hosts` фингерпринтом нового хоста - [ ] пробный деплой end-to-end Beget → Selectel Отдельная развилка, которую решение по чекпоинтам не закрывает: чекпоинты появятся не мгновенно, а деплой продолжает убивать примерно один длинный прогон в три дня. Вариант 1 (не пересоздавать scraper при активном длинном прогоне, а отложить recreate) доступен прямо сейчас, не требует Python и строго не хуже сегодняшнего поведения. Цена — scraper может на несколько часов отставать от backend по образу, а образ у них один (#2679). Ставим эту заплатку на время до #3074 или живём как есть — решение за владельцем. Refs #3074
Author
Owner

Решение владельца (2026-08-24) по перимет­ру доступа: сейчас — только добавление, удаление после переезда.

То есть:

  • новый deploy-ключ выпускается и добавляется, старый пока остаётся рабочим — отзыв переносится на после подтверждённого переезда;
  • SSH на Selectel ограничивается по адресу раннера добавлением разрешающего правила, текущий доступ не срезается;
  • набор секретов Actions пересматривается, но ничего не удаляется.

Мотив понятен и правильный: пока конвейер не проехал end-to-end на новый хост, любое удаление — это способ остаться без доступа ровно в момент, когда он нужен. Отзыв старого ключа сам по себе тоже риск: он же обслуживает сегодняшний деплой.

Приёмка переразмечена под это:

  • Новый deploy-ключ выпущен и добавлен на Selectel (старый остаётся живым)
  • Оба workflow катят на новый хост, known_hosts заполнен без обхода проверки
  • SSH на Selectel: добавлено правило под адрес раннера, существующий доступ сохранён
  • Набор секретов Actions пересмотрен — по каждому зафиксировано, покидает ли он теперь машину
  • Пробный деплой прошёл end-to-end с Beget на Selectel
  • Длинный скрап-прогон не рвётся деплоем → вынесено в #3074

Отложено до после переезда (отдельным чек-листом, не терять):

  • Старый deploy-ключ отозван
  • Временные разрешающие правила SSH сужены до целевых
  • Секреты, ставшие ненужными, удалены

Второй блок — это то, что легче всего забыть: «добавили и работает» не создаёт давления довести до конца, а до тех пор перимет­р шире целевого. Стоит завести отдельную задачу на пост-переездную уборку, чтобы она не растворилась в закрытом #3029.

Решение владельца (2026-08-24) по перимет­ру доступа: **сейчас — только добавление, удаление после переезда.** То есть: - новый deploy-ключ **выпускается и добавляется**, старый пока остаётся рабочим — отзыв переносится на после подтверждённого переезда; - SSH на Selectel ограничивается по адресу раннера **добавлением разрешающего правила**, текущий доступ не срезается; - набор секретов Actions пересматривается, но ничего не удаляется. Мотив понятен и правильный: пока конвейер не проехал end-to-end на новый хост, любое удаление — это способ остаться без доступа ровно в момент, когда он нужен. Отзыв старого ключа сам по себе тоже риск: он же обслуживает сегодняшний деплой. Приёмка переразмечена под это: - [ ] Новый deploy-ключ выпущен и добавлен на Selectel (старый остаётся живым) - [ ] Оба workflow катят на новый хост, `known_hosts` заполнен без обхода проверки - [ ] SSH на Selectel: добавлено правило под адрес раннера, существующий доступ сохранён - [ ] Набор секретов Actions пересмотрен — по каждому зафиксировано, покидает ли он теперь машину - [ ] Пробный деплой прошёл end-to-end с Beget на Selectel - [ ] ~~Длинный скрап-прогон не рвётся деплоем~~ → вынесено в #3074 **Отложено до после переезда (отдельным чек-листом, не терять):** - [ ] Старый deploy-ключ отозван - [ ] Временные разрешающие правила SSH сужены до целевых - [ ] Секреты, ставшие ненужными, удалены Второй блок — это то, что легче всего забыть: «добавили и работает» не создаёт давления довести до конца, а до тех пор перимет­р шире целевого. Стоит завести отдельную задачу на пост-переездную уборку, чтобы она не растворилась в закрытом #3029.
Author
Owner

Пункт «длинный скрап-прогон не рвётся деплоем» — механизм уже есть, но окна не хватает

Проверил перед тем, как писать код: защита реализована ещё в 9850bbde (04.07, PR #2388) — атомарный recreate одним up -d, graceful drain и startup-reap. Дубликат писать не нужно, но пункт приёмки закрывать рано: замер показывает, что окно дренажа мало.

Что показывают данные

Дренаж ждёт scrape_runs.status='running' → 0 до 5 минут. Реальные длительности прогонов, которые он должен защищать (отменённые ПОСЛЕ 04.07, то есть уже при работающем дренаже):

прогон источник шёл до отмены
08-23 14:24 avito_full_load_exhaustive 26 мин
08-20 20:27 cian_full_load 41 мин
08-15 16:31 yandex_city_sweep 65 мин
08-12 16:30 yandex_city_sweep 153 мин
08-09 14:02 avito_full_load_exhaustive 178 мин
08-06 16:17 yandex_city_sweep 29 мин
08-05 20:53 cian_full_load 90 мин
07-31 20:25 cian_full_load 130 мин

Десять отмен с 26.07 по 23.08 — примерно две-три в неделю. Пятиминутный дренаж против прогонов на 26–178 минут почти никогда не дожидается конца: он гасит именно те сборы, ради которых заводился.

Отметки честные, механизм пишет прямо: error_text = "deploy #1951: tradein-scraper recreated mid-run (startup-reap, checkpoint 2026-08-23 14:49:18+00)". То есть данные для вывода лежат в таблице с 04.07 — их просто никто не сводил.

Цена одного случая

Прогон 08-09 успел собрать 5496 объявлений за 178 минут и был убит деплоем. У отменённых 23.08 и 20.08 в total_seen ноль — счётчики до отмены не дошли, так что и объём потерянного по ним не восстановить.

Развилка — за владельцем

Issue формулирует альтернативу как «либо дренаж, либо отказ пересоздавать контейнер при активном run». Дренаж выбран и построен; вопрос лишь в его пороге:

  1. Поднять потолок дренажа (5 мин → например 60–180). Прямо решает проблему, но деплой начинает ждать часами — при частых мержах это де-факто блокировка выката.
  2. Не пересоздавать scraper при активном прогоне, отложив до следующего деплоя. Выкат не стоит, но планировщик остаётся на старом коде на неопределённый срок — ровно инцидент #2679, из-за которого этот компромисс уже один раз убирали.
  3. Сделать обрыв дешёвым#3074 (чекпоинты: прогон продолжается с места обрыва). Тогда порог дренажа перестаёт быть важным вовсе.

По данным выше третий вариант выглядит единственным, который не покупает одно за счёт другого: он снимает не симптом, а стоимость обрыва. Но это выбор архитектуры — не мой, поэтому кода не пишу и оставляю решение.

Предлагаю пункт приёмки переформулировать: «механизм есть» — закрыто, «прогон переживает деплой» — нет, зависит от решения выше.

## Пункт «длинный скрап-прогон не рвётся деплоем» — механизм уже есть, но окна не хватает Проверил перед тем, как писать код: защита реализована ещё в `9850bbde` (04.07, PR #2388) — атомарный recreate одним `up -d`, graceful drain и startup-reap. Дубликат писать не нужно, **но пункт приёмки закрывать рано**: замер показывает, что окно дренажа мало. ### Что показывают данные Дренаж ждёт `scrape_runs.status='running'` → 0 **до 5 минут**. Реальные длительности прогонов, которые он должен защищать (отменённые ПОСЛЕ 04.07, то есть уже при работающем дренаже): | прогон | источник | шёл до отмены | |---|---|---| | 08-23 14:24 | avito_full_load_exhaustive | 26 мин | | 08-20 20:27 | cian_full_load | 41 мин | | 08-15 16:31 | yandex_city_sweep | 65 мин | | 08-12 16:30 | yandex_city_sweep | **153 мин** | | 08-09 14:02 | avito_full_load_exhaustive | **178 мин** | | 08-06 16:17 | yandex_city_sweep | 29 мин | | 08-05 20:53 | cian_full_load | 90 мин | | 07-31 20:25 | cian_full_load | 130 мин | Десять отмен с 26.07 по 23.08 — примерно две-три в неделю. Пятиминутный дренаж против прогонов на 26–178 минут почти никогда не дожидается конца: он гасит именно те сборы, ради которых заводился. Отметки честные, механизм пишет прямо: `error_text = "deploy #1951: tradein-scraper recreated mid-run (startup-reap, checkpoint 2026-08-23 14:49:18+00)"`. То есть данные для вывода лежат в таблице с 04.07 — их просто никто не сводил. ### Цена одного случая Прогон 08-09 успел собрать **5496 объявлений за 178 минут** и был убит деплоем. У отменённых 23.08 и 20.08 в `total_seen` ноль — счётчики до отмены не дошли, так что и объём потерянного по ним не восстановить. ### Развилка — за владельцем Issue формулирует альтернативу как «либо дренаж, либо отказ пересоздавать контейнер при активном run». Дренаж выбран и построен; вопрос лишь в его пороге: 1. **Поднять потолок дренажа** (5 мин → например 60–180). Прямо решает проблему, но деплой начинает ждать часами — при частых мержах это де-факто блокировка выката. 2. **Не пересоздавать scraper при активном прогоне**, отложив до следующего деплоя. Выкат не стоит, но планировщик остаётся на старом коде на неопределённый срок — ровно инцидент #2679, из-за которого этот компромисс уже один раз убирали. 3. **Сделать обрыв дешёвым** — [#3074](https://git.gendsgn.ru/lekss361/gendesign/issues/3074) (чекпоинты: прогон продолжается с места обрыва). Тогда порог дренажа перестаёт быть важным вовсе. По данным выше третий вариант выглядит единственным, который не покупает одно за счёт другого: он снимает не симптом, а стоимость обрыва. Но это выбор архитектуры — не мой, поэтому кода не пишу и оставляю решение. **Предлагаю** пункт приёмки переформулировать: «механизм есть» — закрыто, «прогон переживает деплой» — нет, зависит от решения выше.
Author
Owner

Деплой на прод был сломан заменой адреса — починил, конвейер снова доезжает

Нашлось при проверке после восстановления сервера (#3119). Симптом был тихим: CI зелёный, PR мержатся, а код на прод не приезжал.

Что обнаружилось

После мержа #3120 в 08:45:

Хост HEAD в /opt/gendesign
Beget 094dbd0f — свежий
Poincare (прод) 8a4215fe от 26.08 15:23 — отставал на сутки

Прогоны того же коммита:

deploy-infra.yml   #8881  ✅   → Beget обновился
deploy.yml         #8883  ❌
deploy-metrics.yml #8882  ❌

В логе deploy последняя строка:

2026/08/27 08:47:10 dial tcp ***:***: i/o timeout

Хост замаскирован, потому что приходит из секрета. Это DEPLOY_HOST — в нём лежал 188.246.224.93, адрес, которого с 00:37 не существует. Раннер честно стучался в пустоту 49 секунд и падал.

Обрати внимание на форму отказа: зелёный CI и «PR смержен» ничего не говорят о том, доехал ли деплой. Инфраструктурный канал при этом работал и создавал впечатление, что всё в порядке.

Что сделал

DEPLOY_HOST188.124.37.140. Секрет перезаписан через API (значения секретов API не отдаёт, только пишет).

DEPLOY_KNOWN_HOSTS перегенерирован. Про него легко забыть: строка known_hosts привязана к адресу, поэтому со старым адресом шаг «Resolve deployed base SHA» (обычный openssh с StrictHostKeyChecking=yes) упал бы уже после того, как основной SSH-шаг прошёл. Снял заново через ssh-keyscan -p 22 188.124.37.140, все три типа ключей.

DEPLOY_SSH_FINGERPRINT трогать не нужно — и это проверено, а не предположено. Отпечаток ed25519 нового адреса совпал со старым побайтово:

SHA256:Og/DwfWg3DjLFk9D9AOcK2Wue1B2MP/RI2d9n+sHOw8

Машина та же, ключи хоста Rescue не переписал — менялся только адрес. Если бы отпечаток разошёлся, это был бы повод остановиться и разобраться, а не подставлять новый.

Проверка

Перезапустил deploy.yml вручную. Poincare поднялся до c6c934fb — текущий main. deploy-metrics.yml после правки тоже зелёный (#8889).

main:      c6c934fb
Poincare:  c6c934fb  ✅

Чего это НЕ закрывает

Пункты приёмки этой задачи остаются за владельцем — я починил сломанное, а не выполнил перенастройку:

  • Новый deploy-ключ выпущен и добавлен (старый живёт) — ключ прежний, я его не менял
  • SSH на Selectel ограничен по адресу раннера
  • Набор секретов Actions пересмотрен: по каждому зафиксировано, покидает ли он теперь машину
  • Оба workflow катят на новый хост, known_hosts заполнен без обхода проверкикатят, проверка подлинности на месте
  • Пробный деплой прошёл end-to-end с Beget на Selectel — прошёл, дважды

Вывод, который стоит записать отдельно

Пайплайн не умеет замечать, что деплой не доехал: deploy-status красный, но об этом никто не узнаёт — ровно та же дыра в оповещении, что и в #3119. Сутки расхождения прода и main обнаружились только потому, что я вручную сверил два git log. Стоит либо завести проверку «HEAD на проде == HEAD main», либо довести до конца алерты (#3078).

## Деплой на прод был сломан заменой адреса — починил, конвейер снова доезжает Нашлось при проверке после восстановления сервера (#3119). Симптом был тихим: CI зелёный, PR мержатся, а **код на прод не приезжал**. ### Что обнаружилось После мержа #3120 в 08:45: | Хост | HEAD в `/opt/gendesign` | |---|---| | **Beget** | `094dbd0f` — свежий | | **Poincare (прод)** | `8a4215fe` от 26.08 15:23 — **отставал на сутки** | Прогоны того же коммита: ``` deploy-infra.yml #8881 ✅ → Beget обновился deploy.yml #8883 ❌ deploy-metrics.yml #8882 ❌ ``` В логе `deploy` последняя строка: ``` 2026/08/27 08:47:10 dial tcp ***:***: i/o timeout ``` Хост замаскирован, потому что приходит из секрета. Это `DEPLOY_HOST` — в нём лежал `188.246.224.93`, адрес, которого с 00:37 не существует. Раннер честно стучался в пустоту 49 секунд и падал. Обрати внимание на форму отказа: **зелёный CI и «PR смержен» ничего не говорят о том, доехал ли деплой**. Инфраструктурный канал при этом работал и создавал впечатление, что всё в порядке. ### Что сделал **`DEPLOY_HOST` → `188.124.37.140`.** Секрет перезаписан через API (значения секретов API не отдаёт, только пишет). **`DEPLOY_KNOWN_HOSTS` перегенерирован.** Про него легко забыть: строка `known_hosts` привязана к **адресу**, поэтому со старым адресом шаг «Resolve deployed base SHA» (обычный openssh с `StrictHostKeyChecking=yes`) упал бы уже после того, как основной SSH-шаг прошёл. Снял заново через `ssh-keyscan -p 22 188.124.37.140`, все три типа ключей. **`DEPLOY_SSH_FINGERPRINT` трогать не нужно — и это проверено, а не предположено.** Отпечаток ed25519 нового адреса совпал со старым побайтово: ``` SHA256:Og/DwfWg3DjLFk9D9AOcK2Wue1B2MP/RI2d9n+sHOw8 ``` Машина та же, ключи хоста Rescue не переписал — менялся только адрес. Если бы отпечаток разошёлся, это был бы повод остановиться и разобраться, а не подставлять новый. ### Проверка Перезапустил `deploy.yml` вручную. Poincare поднялся до `c6c934fb` — текущий `main`. `deploy-metrics.yml` после правки тоже зелёный (#8889). ``` main: c6c934fb Poincare: c6c934fb ✅ ``` ### Чего это НЕ закрывает Пункты приёмки этой задачи остаются за владельцем — я починил сломанное, а не выполнил перенастройку: - [ ] Новый deploy-ключ выпущен и добавлен (старый живёт) — **ключ прежний**, я его не менял - [ ] SSH на Selectel ограничен по адресу раннера - [ ] Набор секретов Actions пересмотрен: по каждому зафиксировано, покидает ли он теперь машину - [x] ~~Оба workflow катят на новый хост, `known_hosts` заполнен без обхода проверки~~ — **катят**, проверка подлинности на месте - [x] ~~Пробный деплой прошёл end-to-end с Beget на Selectel~~ — прошёл, дважды ### Вывод, который стоит записать отдельно Пайплайн не умеет замечать, что деплой не доехал: `deploy-status` красный, но об этом никто не узнаёт — ровно та же дыра в оповещении, что и в #3119. Сутки расхождения прода и `main` обнаружились только потому, что я вручную сверил два `git log`. Стоит либо завести проверку «HEAD на проде == HEAD main», либо довести до конца алерты (#3078).
Collaborator

Перепроверка 27.08.2026 (после пересборки базы 25-26.08 и переезда продукта на Poincare). Тело тикета писалось 20-24.08 по старой инфраструктуре.

Половина тикета фактически состоялась: продукт развёрнут на Selectel (контейнеры от 27.08). Остаётся то, что и было целью — периметр: 22/tcp ALLOW Anywhere и гард по SSH-fingerprint в deploy.yml:557-562 работает fail-open.

**Перепроверка 27.08.2026** (после пересборки базы 25-26.08 и переезда продукта на Poincare). Тело тикета писалось 20-24.08 по старой инфраструктуре. Половина тикета фактически состоялась: продукт развёрнут на Selectel (контейнеры от 27.08). Остаётся то, что и было целью — периметр: `22/tcp ALLOW Anywhere` и гард по SSH-fingerprint в `deploy.yml:557-562` работает fail-open.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3029
No description provided.