MAATRIX / Блог / Регламент тестового окружения: как не дать стейджу превратиться в помойку

Регламент тестового окружения: как не дать стейджу превратиться в помойку

MAATRIX

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

Почему данные на стейдже устаревают и почему это не мелочь

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

  • Новые сегменты пользователей отсутствуют. В проде появился новый тип аккаунта или тариф, а дамп на стейдже старше — тесты не видят эту ветку логики, потому что подходящих строк в базе физически нет.
  • Объём расходится на порядки. Прод за год вырос с 50 тысяч заказов до 5 миллионов, а на стейдже как было 50 тысяч, так и осталось. Запрос, который на проде уже упирается в Seq Scan без индекса, на стейдже отрабатывает мгновенно — проблема с производительностью долетает до продакшена незамеченной.
  • Распределение значений не соответствует реальности. На проде NULL в старых полях, экзотические значения enum от выключенных фичей, дубликаты из-за старого бага — а на стейдже данные уже «причёсаны» ручными правками тестировщиков. Именно на неровностях реальных данных ловятся баги, которых не видно на аккуратной выборке.
  • Миграции применены неравномерно. На стейдже накатили миграцию, которая на проде ещё не применена (или наоборот), — структура таблиц разошлась, и тест проходит на схеме, которой в проде не существует.

Проверить, насколько далеко разъехались данные, можно за пару минут — сравнить возраст дампа и грубые метрики объёма:

# когда стенд последний раз обновлялся из прода
psql -h stage-db.internal -U app -d appdb -c \
  "SELECT max(created_at) FROM orders;"

# сверить порядок величины с продом
psql -h prod-db.internal -U readonly -d appdb -c \
  "SELECT count(*) FROM orders;"

Если разница между max(created_at) на стейдже и текущей датой измеряется месяцами, а не днями — стенд уже не тестовая копия прода, а архив с прошлого квартала.

Мусор от экспериментов: как стенд обрастает шлаком

У прода есть встроенный тормоз для экспериментов — цена ошибки. На стейдже этого тормоза нет, поэтому туда сваливают всё: контейнер для проверки гипотезы, ветку фичи, которую отменили месяц назад, cron-задачу, которая должна была прожить один день. Через полгода никто не помнит, что из этого ещё нужно, а что можно снести.

Типичные источники мусора:

  • Остановленные, но не удалённые контейнеры и тома. docker stop без docker rm оставляет контейнер в списке навсегда; тома с данными от удалённых контейнеров занимают место на диске.
  • Ветки фич, которые задеплоили и забыли. У каждого разработчика — свой поддомен feature-xyz.stage.example.com, фича давно смержена, а окружение продолжает крутиться и жрать ресурсы.
  • Тестовые пользователи и API-ключи без TTL. Учётки вида test_admin с паролем «попроще для удобства» накапливаются годами, часть — с доступом шире, чем нужно для текущих задач.
  • Логи и артефакты сборки, которые не ротируются, потому что регламент хранения логов писали только для прода.

Быстрая инвентаризация показывает масштаб проблемы нагляднее любых предположений:

# сколько места реально съедено остановленными контейнерами и висящими томами
docker system df -v

# список контейнеров, которые не запускались много недель
docker ps -a --format '{{.Names}}\t{{.Status}}' | grep -i exited

# висящие тома без привязанного контейнера
docker volume ls -qf dangling=true

Разовая чистка снимает симптом, но не причину — через месяц стенд снова обрастёт тем же мусором, если не встроить генеральную уборку в регламент (об этом — в разделе про пересборку с нуля). Похожий по духу ритуал плановой гигиены сервера, только для боевого контура, разобран в статье «Еженедельный регламент обслуживания: 40 минут в пятницу вместо аврала в понедельник» — стейджу нужен свой вариант, с другим набором проверок.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Расхождение конфигурации: когда тесты перестают быть показательными

Третий и самый коварный вид деградации — конфигурационный дрейф. Данные и мусор видно невооружённым глазом, а расхождение конфигурации обнаруживается только тогда, когда что-то ломается на проде, хотя на стейдже «всё работало». Типичные источники дрейфа:

  • Версии пакетов и образов. На стейдже Dockerfile тянет node:20, на проде закреплён node:20.11.1 после инцидента с патчем — либо наоборот, стейдж годами сидит на устаревшем образе, потому что «зачем трогать, если работает».
  • Переменные окружения и feature-флаги. На стейдже давно включены флаги, которые на проде ещё выключены (или наоборот), и разработчик тестирует код, который в продакшене выполняется по другой ветке логики.
  • Ресурсы и лимиты. Прод отдаёт контейнеру 4 CPU и 8 ГБ RAM, стейдж — 1 CPU и 2 ГБ «для экономии». Нагрузочный сценарий, упирающийся на стейдже в CPU, на проде выглядит совершенно иначе, и наоборот.
  • Внешние интеграции. На проде — боевой платёжный шлюз, на стейдже — заглушка с другой логикой ретраев и таймаутов. Тест «оплата прошла» ничего не говорит о поведении при реальном таймауте провайдера.
  • Ручные правки, которые не попали в код. Кто-то поправил nginx.conf прямо на сервере «чтобы побыстрее завести» — исправление живёт только на этом стейдже и пропадёт при следующей пересборке.

Реальный инцидент, где расхождение сработало в обратную сторону — тестовый .env затёр боевой конфиг и включил режим отладки на проде, — разобран в статье «Скопировали конфиг с тестового и получили режим отладки на проде». Механика та же: граница стирается, когда конфигурация нигде не зафиксирована как единый источник правды.

Грубую сверку можно сделать вручную, но это разовая мера, а не регламент:

diff <(ssh stage 'cat /etc/nginx/sites-enabled/app.conf') \
     <(ssh prod  'cat /etc/nginx/sites-enabled/app.conf')

Регламент: обновление данных из прода с анонимизацией

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

Процесс, который не зависит от человеческой памяти, выглядит как пайплайн из трёх шагов, запускаемый по cron или в CI по расписанию:

# пример шага в GitLab CI, запускается по расписанию (Schedules)
refresh_stage_db:
  stage: maintenance
  script:
    - pg_dump -h prod-db.internal -U readonly -d appdb -F c -f dump.pgdump
    - psql -h stage-db.internal -U app -d appdb -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
    - pg_restore -h stage-db.internal -U app -d appdb dump.pgdump
    - psql -h stage-db.internal -U app -d appdb -f anonymize.sql
  only:
    - schedules

Файл anonymize.sql — не разовый скрипт, а часть регламента, которая обновляется вместе со схемой базы: появилась в проде новая таблица с персональными данными — в скрипт добавляется соответствующий UPDATE. Набор техник маскирования и примеры для PostgreSQL и MySQL разобраны в статье «Тестовый сервер с копией боевой базы: точка входа, о которой забыли» — принцип «структура и объём сохраняются, значения — нет» применим к любому регулярному обновлению, а не только к разовому дампу.

Важные практические детали регламента:

  • Окно обновления фиксируется заранее и не пересекается с активным тестированием — снос базы посреди регрессии ломает работу, а не помогает ей.
  • Уведомление команды перед обновлением — сообщение в чат за час до запуска, чтобы никто не удивился пропавшим тестовым записям.
  • Логирование каждого обновления — дата, объём дампа, статус анонимизации, чтобы при разборе странного поведения на стейдже быстро понять, «а не съехали ли данные».
  • Отдельная роль для восстановления, у которой нет прав трогать прод, — ошибка в hostname не должна суметь испортить боевую базу.

Для команд, где прод меняется медленно, разумно синхронизировать не всю базу, а только таблицы, нужные для тестов, — это сокращает время обновления и объём данных, которые нужно анонимизировать.

Регламент: периодическая полная пересборка окружения с нуля

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

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

# для окружения на docker-compose — полный снос вместе с томами
docker compose down -v --remove-orphans
docker system prune -af --volumes

# развернуть заново строго из репозитория, без ручных доводок
git pull origin main
docker compose up -d --build

Если инфраструктура описана через Terraform или Ansible, пересборка проверяет ровно то, что и должна: соответствует ли реальность коду.

terraform destroy -target=module.stage
terraform apply -target=module.stage

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

Практический чек-лист перед пересборкой:

  1. Убедиться, что все нужные для тестов правки попали в git, а не живут только на сервере.
  2. Предупредить команду — окружение будет недоступно от нескольких минут до часа.
  3. После пересборки прогнать базовый набор smoke-тестов и сразу обновить данные из прода по регламенту выше, чтобы не тестировать на пустой базе.

Стенд, простаивающий большую часть суток, не обязан быть постоянно запущенным дорогим сервером — можно поднимать его по расписанию или по требованию, и это заодно упрощает пересборку, поскольку окружение и так пересоздаётся при каждом запуске. Как выбрать сервер под тестовую среду и не переплачивать за круглосуточный простой, разобрано в статье «VPS для тестовой среды: что выбрать и как настроить».

Регламент: синхронизация конфигурации с продакшеном

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

Практические механизмы синхронизации:

  • Конфигурация в git, а не на сервере. nginx.conf, docker-compose.yml, systemd-юниты версионируются в репозитории; правка «прямо на сервере, чтобы побыстрее» запрещена регламентом, а не просто нежелательна.
  • Один шаблон, разные переменные. Вместо двух независимых файлов — один шаблон (Jinja2 в Ansible, envsubst, Helm values) и отдельный набор переменных на окружение. Структура физически не может разойтись — расходятся только явно выделенные значения.
  • Версии образов и пакетов закреплены явно. Не node:20, а node:20.11.1; не latest, а конкретный тег. Обновление версии — один pull request, применяемый к обоим окружениям по очереди.
  • Регулярный diff конфигурации, а не разовая сверка. Раз в неделю-две — автоматическое сравнение конфигурации прода и стейджа с уведомлением, если разница выросла:
# сравнить набор переменных окружения между стендами
ssh prod  'docker exec app env | sort' > /tmp/prod.env
ssh stage 'docker exec app env | sort' > /tmp/stage.env
diff /tmp/prod.env /tmp/stage.env
  • Ресурсные лимиты пропорциональны, а не произвольны. Если стейдж физически не может себе позволить те же CPU и RAM, что прод, — задать лимиты как долю от боевых (например, четверть), а не подбирать «на глазок», чтобы нагрузочные и пограничные тесты хотя бы масштабировались предсказуемо.

Для команд с несколькими окружениями (dev, staging, несколько preview-веток) удобно держать все профили в одном docker-compose.yml через profiles, а не плодить копии файла, которые неизбежно расходятся между собой, — подход разобран в статье «Docker Compose profiles: несколько окружений». Тот же принцип единого источника применим и к синхронизации между стейджем и продом: чем меньше независимых копий конфигурации существует, тем меньше шансов, что они разойдутся незаметно.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

С чего начать, если стейдж уже давно превратился в свалку?

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

Кто должен отвечать за регламент — DevOps, тестировщики или разработчики?

Формальный владелец — тот, кто отвечает за инфраструктуру, но обновление данных и пересборку стоит автоматизировать через CI, чтобы результат не зависел от памяти конкретного человека.

Обязательно ли анонимизировать данные, если стейдж доступен только внутри VPN компании?

Да. Периметр защищает от внешнего сканирования, но не от компрометации рабочей станции сотрудника или кражи ключей подрядчика. Анонимизация — это защита данных, а не сети.

Что делать, если пересборка занимает слишком много времени и мешает работе команды?

Это сигнал, что инфраструктура недостаточно описана как код и часть шагов выполняется вручную. Ускорение пересборки окупается: чем она быстрее и дешевле, тем чаще её можно проводить.

Нужен ли отдельный регламент для preview-окружений на каждую feature-ветку?

Да, но проще: TTL на существование (не дольше открытого pull request), автоудаление при мерже и наследование конфигурации основного стейджа — иначе именно preview быстрее всего превращаются в забытый мусор.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →