Защитная инженерия: системы, устойчивые к катастрофам
Паттерны защитной инженерии против сбоев в продакшене: molly guard, dry-run, мягкое удаление и почему окна подтверждения не работают.

Молодой инженер в финтех-компании среднего размера запустил скрипт миграции базы данных в пятницу после обеда. Скрипт должен был чистить «осиротевшие» записи на staging-окружении. Вместо этого он подключился к продакшен-базе и удалил 4,2 миллиона записей о транзакциях клиентов. Бэкап? Ему было три дня. Следующие 72 часа компания провела в режиме полноценного реагирования на инцидент, вручную восстанавливая записи по логам платёжного процессора. Итоговые расходы превысили 400 000 долларов с учётом времени инженеров, компенсаций клиентам и отчётности для регуляторов.
В скрипте не было проверки окружения. Не было флага dry-run. Не было запроса подтверждения, который показал бы, к какой базе идёт подключение. Строка подключения бралась из переменной окружения, которая две недели назад случайно осталась указывать на продакшен после отладочной сессии на ноутбуке инженера. Каждый из этих промахов можно было предотвратить.
Почему диалоги «Вы уверены?» не работают
Самый распространённый механизм безопасности в софте — диалог подтверждения. И самый бесполезный. Исследования «усталости от подтверждений» показывают, что после нескольких встреч с запросом «Вы уверены?» пользователи почти всегда нажимают «ОК» не глядя. Окно становится невидимым: это просто ещё один клик на пути к тому, что вы уже решили сделать.
Дело не в том, что подтверждения — плохая идея. Проблема в том, что обычные подтверждения не несут никакой информации. «Вы действительно хотите продолжить?» ничего не говорит о том, что вы собираетесь сделать. Сравните: «Вы собираетесь удалить 4 217 893 строки из таблицы transactions в prod-us-east-1. Введите имя базы данных для подтверждения». Вторая версия заставляет реально прочитать, что происходит. Вот разница между «лежачим полицейским» и molly guard.
Что такое molly guard и почему это важно
Термин «molly guard» пошёл от физической пластиковой крышки, которую ставили на большую красную кнопку на мейнфреймах, чтобы не выключить машину случайно. По легенде, назвали её в честь маленькой дочки одного программиста, Молли, которая любила эту кнопку нажимать. В софте molly guard — любой механизм, который делает разрушительное действие сложным для случайного запуска, но оставляет его возможным, когда оно действительно нужно.
Linux-пакет molly-guard делает именно это. Он перехватывает команды shutdown, reboot и halt в SSH-сессиях и просит ввести имя хоста машины, которую вы собираетесь выключить. Проскочить на автопилоте не получится: нужно доказать, что вы понимаете, на какой машине находитесь.
$ sudo reboot
Good grief! You're on a remote SSH session to prod-web-03.
Please type the hostname to confirm: prod-web-03
# Compare with the useless version:
$ sudo reboot
Are you sure? (y/n): y
# *types y without reading, muscle memory*
Принцип простой: подтверждение должно требовать информацию, которая доказывает, что оператор понимает действие. Ввод «y» ничего не доказывает. Ввод имени хоста, таблицы или количества затрагиваемых записей показывает, что вы прочитали предупреждение.
Режимы dry-run: сначала покажи, потом стреляй
У каждой разрушительной операции должен быть режим dry-run. Не «должен» в смысле «хорошая практика», а «должен», потому что рано или поздно вы пожалеете, что его нет. Dry-run выполняет всю логику операции, записывает в лог, что произошло бы, и останавливается. Никаких побочных эффектов, полная видимость.
Реализовать этот паттерн несложно. Вот скрипт миграции со встроенным dry-run:
#!/bin/bash
set -euo pipefail
DRY_RUN=${DRY_RUN:-true} # Default to dry-run!
DB_HOST=$(get_db_host)
DB_NAME=$(get_db_name)
echo "Target: $DB_HOST / $DB_NAME"
echo "Mode: $([ "$DRY_RUN" = true ] && echo 'DRY RUN' || echo 'LIVE')"
ROWS=$(psql -h "$DB_HOST" -d "$DB_NAME" -t -c \
"SELECT count(*) FROM orphaned_records WHERE created_at < now() - interval '90 days'")
echo "Records to delete: $ROWS"
if [ "$DRY_RUN" = true ]; then
echo "Dry run complete. Set DRY_RUN=false to execute."
exit 0
fi
# Require explicit confirmation for live runs
read -p "Type '$DB_NAME' to confirm deletion: " CONFIRM
if [ "$CONFIRM" != "$DB_NAME" ]; then
echo "Aborted."
exit 1
fi
psql -h "$DB_HOST" -d "$DB_NAME" -c \
"DELETE FROM orphaned_records WHERE created_at < now() - interval '90 days'"
echo "Deleted $ROWS records."
Обратите внимание на три вещи: по умолчанию включён dry-run (отказаться от разрушения нужно явно, а не наоборот), перед любым действием скрипт показывает целевую базу и количество строк, а в боевом режиме нужно ввести имя базы данных. Три уровня защиты. Любой из них предотвратил бы инцидент, который я описал в начале.
Страховочные сетки для миграций баз данных
Миграции баз данных — одна из самых рискованных операций в любом деплой-пайплайне. Их часто нельзя откатить, они работают с общим состоянием, и одна неудачная может положить всё приложение. При этом большинство команд относятся к ним как к обычному шагу деплоя.
Вот иерархия защитных механизмов, от базовых до почти неуязвимых:
- Проверки окружения — Скрипт миграции убеждается, что работает с нужным окружением, перед выполнением. Звучит очевидно. Вы удивитесь, как часто этой проверки нет.
- Снапшоты перед миграцией — Автоматически создавать снапшот базы перед любой миграцией. Если что-то пошло не так, можно откатиться за минуты, а не за часы.
- Ограничители по количеству строк — Если миграция затронет больше N строк, требовать явного подтверждения. Миграция, которая неожиданно трогает миллионы строк, почти всегда является багом.
- Таймауты на выражения — Ставить жёсткие таймауты на запросы миграции. Миграция, которая идёт 45 минут, блокирует таблицы и тормозит систему. Лучше упасть быстро.
- Только обратно совместимые изменения — Требовать, чтобы каждая миграция была совместима с текущим задеплоенным кодом. Никаких переименований колонок, никаких NOT NULL без значений по умолчанию, никаких удалений колонок, на которые ещё есть ссылки.
# Rails example using strong_migrations gem
class AddPhoneToUsers < ActiveRecord::Migration[7.1]
def change
# This will raise an error with a safe alternative:
# add_column :users, :phone, :string, null: false
#
# Instead, do it in two steps:
add_column :users, :phone, :string
# Then in a separate migration after backfill:
# change_column_null :users, :phone, false
end
end
Инструменты вроде strong_migrations для Rails, squawk для PostgreSQL и skeema для MySQL ловят опасные паттерны до того, как они попадут в продакшен. Если вы ими не пользуетесь, вы полагаетесь на код-ревью, а ревьюеры пропускают тонкие проблемы миграций.
Мягкое удаление и окна для отмены
Жёсткое удаление — это необратимое решение, принятое в момент уверенности. Беда в том, что уверенность часто оказывается ошибочной. Мягкое удаление — когда запись помечается как удалённая, но физически не стирается — даёт окно для восстановления после ошибок.
-- Hard delete: gone forever
DELETE FROM projects WHERE id = 4872;
-- Soft delete: recoverable
UPDATE projects
SET deleted_at = now(),
deleted_by = 'user:priya'
WHERE id = 4872;
-- Recovery is trivial
UPDATE projects
SET deleted_at = NULL,
deleted_by = NULL
WHERE id = 4872;
Компромисс реальный: мягкое удаление усложняет запросы (условие WHERE deleted_at IS NULL нужно писать везде), увеличивает объём хранилища и может запутывать в состоянии данных. Но для пользовательских данных, где случайное удаление возможно, оно того стоит. GitHub не удаляет ваш репозиторий сразу, а хранит его 90 дней. Slack сохраняет удалённые сообщения ради соответствия требованиям. Корзина в Gmail очищается через 30 дней. Это не случайность, а осознанные инженерные решения.
Тот же принцип работает и для инфраструктуры. Вместо того чтобы терминировать EC2-инстанс, сначала остановите его. Вместо удаления базы переименуйте её в mydb_deleted_20260315 и поставьте напоминание в календаре, чтобы удалить её через две недели. Стоимость хранения остановленного инстанса или переименованной базы на пару дней ничтожна по сравнению с ценой восстановления из бэкапа.
Безопасность деплоя: канарейки, предохранители и откаты
Деплой — ещё одна категория разрушительных действий, к которой большинство команд относятся без должной осторожности. Плохой деплой может уронить продакшен не хуже удалённой базы, и случается он гораздо чаще.
Минимальный рабочий набор защиты деплоя включает:
- Канареечные деплои — Направлять 1–5% трафика на новую версию. Если частота ошибок растёт, откатываться автоматически, пока радиус поражения не увеличился.
- Автоматические триггеры отката — Задать пороги по частоте ошибок, задержкам и результатам health-check, при которых откат происходит сам. Не надейтесь, что человек заметит это в два ночи.
- Заморозка деплоев во время инцидентов — Если идёт активный инцидент, блокировать все деплои. Меньше всего нужно, чтобы во время тушения пожара кто-то выкатывал несвязанные изменения.
- Откат в один клик — Откат должен быть проще, чем накат. Если откат означает SSH на сервера и ручные команды, то процесса отката у вас нет.
# Kubernetes progressive delivery with Argo Rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
strategy:
canary:
steps:
- setWeight: 5
- pause: { duration: 5m }
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 25
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
# Auto-rollback if error rate exceeds 1%
abortScaleDownDelaySeconds: 30
Суть в прогрессивном принятии риска. Вы не переходите от 0 до 100% за один шаг. Вы делаете маленькие шаги, проверяете каждый и сохраняете возможность отступить на каждом этапе. Это медленнее, чем выкатить всё на все инстансы разом, но в первый раз, когда система поймает плохой деплой до того, как он заденет всех пользователей, вы будете благодарны каждой лишней минуте.
Архитектурная профилактика: делаем неправильное действие невозможным
Лучшие механизмы безопасности не требуют от вас осторожности. Они делают неправильное действие структурно невозможным. Это разница между ограждением и знаком «осторожно».
- Неизменяемая инфраструктура — Если к продакшен-серверам нельзя подключиться по SSH, нельзя случайно выполнить на них команду. Если деплои всегда создают новые инстансы из известного образа, дрейфа конфигурации быть не может.
- Доступ по принципу наименьших привилегий — У инженеров не должно быть продакшен-учётных данных к базам на ноутбуках. Точка. Используйте инструменты just-in-time доступа, которые выдают временные креды с аудитом.
- Отдельные учётные данные для каждого окружения — Если staging и продакшен используют разные хранилища секретов, физически невозможно случайно подключиться к продакшену инструментами для staging.
- Защита от удаления — AWS позволяет включить защиту от завершения для EC2 и защиту от удаления для RDS. Включайте её для всего, что важно. Это изменение конфигурации на пять секунд, которое полностью исключает целый класс катастрофических ошибок.
Если кто-то может случайно уничтожить продакшен одной командой, проблема не в человеке, а в системе, которая позволила одной команде уничтожить продакшен.
Я видел, как команды реагируют на инциденты в продакшене, добавляя больше документации, чек-листов и обучения. Это помогает, но всё это опирается на то, что люди безошибочны. Люди не безошибочны. Лучшая реакция — изменить систему так, чтобы ошибка вообще не могла произойти, а если всё же произошла, радиус поражения был ограничен и восстановление шло быстро.
Культура инженерии, в центре которой безопасность
Инструменты и архитектура важны, но то, внедрят ли их на самом деле, решает культура. Команды, которые воспринимают механизмы безопасности как накладные расходы или бюрократию, будут пропускать их под давлением дедлайнов, а давление дедлайнов — это навсегда.
Самый эффективный паттерн, который я видел, — рассматривать механизмы безопасности как обязательное инженерное требование первого класса, а не как приятное дополнение. Каждая разрушительная операция проходит дизайн-ревью, на котором задаются конкретные вопросы: что будет, если это запустится не на той цели? Что будет, если запустить дважды? Что будет, если запустить с устаревшими данными? Как это откатить?
Разборы инцидентов без поиска виноватых уже давно стали базовой практикой. Но, по-моему, важнее менее распространённая практика — пре-мортем. Перед запуском рискованного изменения соберите команду и спросите: «Представьте, что всё пошло ужасно не так. Что произошло?» Люди удивительно хорошо находят способы отказа, если подать задачу как упражнение на воображение, а не как прогноз. Найденные ими сценарии отказа и становятся механизмами защиты, которые вы строите.
Тот молодой инженер, который удалил продакшен-базу? Он до сих пор в компании. Сейчас он один из самых активных сторонников защитной инженерии в команде. Инцидент был не его виной, а сбоем системы. И теперь систему сломать гораздо сложнее.


