Антипаттерн: скрипт вместо документации
«У нас всё в Ansible, зачем ещё документация» — фраза, которую слышишь на каждом втором инфраструктурном созвоне. Звучит логично: playbook воспроизводим, идемпотентен, лежит в git — что ещё нужно. На практике этот подход работает ровно до первого вопроса «а почему это сделано именно так», на который отвечает только человек, писавший роль полгода назад и, возможно, уже уволившийся. Разберём, где ломается идея «код — это документация», и что стоит писать рядом со скриптами, чтобы не терять контекст решений.
Содержание
- Как это выглядит на практике
- Скрипт фиксирует ЧТО, но не ПОЧЕМУ
- Новому человеку в команде код автоматизации не заменяет контекст
- Скрипт устаревает без явного индикатора
- Что даёт документация рядом с автоматизацией
- Роль: postgres
- 2026-06 — переход с down/up на rolling update в деплое
- Где проходит разумная граница
Как это выглядит на практике
Типичный инфраструктурный репозиторий на конец августа 2026 года выглядит примерно так:
infra/
├── ansible/
│ ├── playbook.yml
│ ├── roles/
│ │ ├── nginx/
│ │ ├── postgres/
│ │ └── firewall/
│ └── inventory/
├── scripts/
│ ├── deploy.sh
│ ├── backup.sh
│ └── rotate_logs.sh
└── README.md # три строки: "запуск: ansible-playbook -i inventory playbook.yml"
Всё воспроизводимо: новый сервер поднимается командой ansible-playbook -i inventory/prod playbook.yml, конфигурация версионируется, откат — это git revert плюс повторный прогон. С точки зрения DevOps-гигиены придраться не к чему. Но откройте роль firewall и посмотрите на правило:
- name: Allow custom port for internal service
ufw:
rule: allow
port: "8422"
proto: tcp
src: 10.0.0.0/24
Порт 8422 — не стандартный ни для чего. Почему именно он, а не 8080 или дефолтный порт сервиса? Ограничение по src — это осознанное решение изолировать сеть, или кто-то просто скопировал блок из другой роли и забыл поменять маску? Плейбук не отвечает на эти вопросы — он просто выполняет действие. То же самое с backup.sh, где ротация настроена на 14 дней: это расчёт под объём диска, требование по complience или случайное число, которое когда-то показалось разумным? Скрипт молчит.
Здесь и проявляется суть антипаттерна: команда путает «код воспроизводим» с «код понятен». Это разные свойства, и второе автоматизацией не покрывается.
Скрипт фиксирует ЧТО, но не ПОЧЕМУ
Bash-скрипт или Ansible-роль — это императивная (или декларативная, для Ansible) запись конечного состояния и шагов к нему. Она по конструкции не содержит:
- альтернатив, которые рассматривались и были отклонены. Например, почему для очередей выбран Redis, а не RabbitMQ — playbook, разворачивающий Redis, никогда не расскажет, что RabbitMQ тестировали и отказались из-за overhead на маленьком инстансе;
- ограничений, под которые подгонялось решение. Swap в 2 ГБ на сервере с 4 ГБ RAM — это осознанный компромисс под конкретную нагрузку или дефолт, который никто не пересматривал с момента установки VPS;
- обстоятельств момента. Версия PostgreSQL зафиксирована на 15-й ветке в роли — потому что 16-я на момент настройки ломала совместимость с конкретным расширением, или просто никто не обновлял playbook два года;
- компромиссов между «правильно» и «работает сейчас». Временный костыль — cron-задача, которая раз в час перезапускает зависший процесс вместо починки утечки памяти — в коде выглядит как обычная штатная задача. Ничто в самом cron-файле не сигнализирует «это техдолг, а не архитектурное решение».
Разница принципиальная: скрипт отвечает на вопрос «что происходит при выполнении», документация — на вопрос «почему решение выглядит именно так». Читать первое вместо второго — то же самое, что понимать замысел здания по чертежу электропроводки: технически всё есть, но общей картины не видно.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНовому человеку в команде код автоматизации не заменяет контекст
Представьте, что в команду приходит новый инженер и ему дают доступ к репозиторию с Ansible-ролями вместо онбординг-документа — «разберёшься, всё же в коде». Первая неделя уйдёт не на понимание архитектуры, а на археологию: git blame по каждому файлу, попытки восстановить хронологию через сообщения коммитов вида fix и wip, вопросы в чат вида «а зачем тут этот costyl.sh».
Проблема усугубляется тем, что playbook описывает целевое состояние, а не путь принятия решений. Роль postgres покажет, что shared_buffers выставлен в 2GB — но не покажет, что до этого пробовали 4GB и словили OOM killer на проде, после чего значение сознательно занизили и оставили комментарий в тикете, который давно закрыт и не проиндексирован нигде, кроме памяти того, кто это делал. Новый человек с равной вероятностью решит, что 2GB — это временное значение, которое давно пора поднять, и «улучшит» конфиг, наступив на те же грабли.
Ситуация особенно болезненна в небольших командах и у одиночных инженеров, которые ведут инфраструктуру клиентов или собственные проекты: сегодня контекст в голове, но через полгода к тому же серверу возвращается тот же человек — и с тем же успехом мог бы быть кем-то посторонним. Память о нюансах выветривается быстрее, чем кажется в момент принятия решения. Подробнее о том, что стоит фиксировать конкретно про сервер, — в статье про документацию сервера.
Скрипт устаревает без явного индикатора
У кода автоматизации есть особенность, которая делает его особенно коварным источником единственной правды: он может годами не отражать реальность и никак об этом не сигнализировать.
Механизмы расхождения обычно одни и те же:
- ручные правки поверх автоматизации. Кто-то зашёл на прод по SSH, поправил конфиг nginx напрямую «на скорую руку», а playbook откатывать не стал — потому что «работает же». Со следующего прогона Ansible это либо перезатрёт правку (и снова что-то сломает), либо, если параметр не описан в роли явно, правка так и останется вне версионируемого состояния, невидимая для всех, кроме того, кто её сделал;
- playbook, который перестали запускать. Роль исправно лежит в репозитории, но последний реальный прогон на проде был восемь месяцев назад — с тех пор сервер жил своей жизнью через точечные правки. Git-история создаёт иллюзию, что состояние сервера описано в коде, хотя на деле описание разошлось с реальностью и никто это не проверял;
- скрипт, который «работает», но не делает то, что должен. Классический случай — бэкап-скрипт, у которого сломалась одна из команд в цепочке, а код продолжает исполняться и выходить с кодом 0, потому что ошибку никто не проверяет explicitly. Формально автоматизация есть и запускается по расписанию, по факту — не работает уже давно. Подробный разбор похожего случая — в статье про скрипт бэкапа, который молча падал четыре месяца.
У документации в этом смысле есть неочевидное преимущество: она статична и её несоответствие реальности проще заметить, потому что никто не ждёт, что README сам исполнится и подтвердит себя. А код автоматизации создаёт ложное чувство актуальности — раз он есть и синтаксически валиден, кажется, что он отражает текущее состояние. Разрыв между «playbook существует» и «playbook соответствует проду» — один из самых частых источников инцидентов при попытке восстановить сервер по чужой автоматизации; типовую цепочку такого сценария разбирали в статье про эскалацию до root через собственный же скрипт.
Что даёт документация рядом с автоматизацией
Речь не о замене кода документацией и не о параллельном ведении двух источников истины — это дублирование, которое неизбежно разъезжается ещё быстрее, чем сам код. Речь о том, чтобы явно фиксировать то, что код в принципе не может выразить.
Минимальный рабочий набор:
README на уровне репозитория или роли, отвечающий на три вопроса: зачем этот компонент существует, какие у него нетривиальные зависимости и ограничения, куда смотреть при инциденте. Не пересказ того, что делает playbook (это видно из кода), а контекст вокруг него:
Роль: postgres
Разворачивает PostgreSQL для основного приложения.
Почему так
- shared_buffers = 2GB, не 4GB: на 4GB словили OOM на проде
при пиковой нагрузке (см. инцидент от марта 2025, тикет в внутреннем трекере INFRA-142). Прежде чем поднимать обратно — убедитесь, что суммарная память процесса учтена с запасом.
- Версия закреплена на 15-й ветке: расширение pg_cron
из роли cron-jobs несовместимо с 16.x на момент написания. Проверить актуальность перед апгрейдом.
Что НЕ покрыто этой ролью
- Резервное копирование — отдельная роль backup, запускается
независимо по расписанию из main playbook.
**Комментарии в самом коде** — не построчный пересказ команд, а объяснение неочевидных решений прямо там, где их видно в момент чтения:
# Пауза в 5 секунд перед healthcheck — не эстетика,
# а обход гонки: контейнер приложения проходит миграции
# при старте и первые секунды отвечает 503 даже будучи
# "healthy" по докеровским меркам. Без паузы деплой
# считает сервис упавшим и откатывает релиз. sleep 5 curl -f http://localhost:8080/health || exit 1
**Журнал решений (ADR-подобный, необязательно формальный).** Для маленькой команды не нужен полноценный процесс Architecture Decision Records с шаблонами — достаточно файла `DECISIONS.md`, куда коротко пишется дата, что изменили и почему:
2026-06 — переход с down/up на rolling update в деплое
Раньше деплой делал docker compose down && up, что клало весь стек на 10-15 секунд ради обновления одного сервиса. Перешли на up -d --no-deps <service>. Возможная проблема: при изменении shared-сети между сервисами придётся всё равно поднимать весь стек — учитывайте при следующих правках compose-файла.
**Дата и результат последнего реального прогона.** Простая практика — playbook или деплой-скрипт при успешном выполнении дописывает метку в отдельный лог или файл-маркер на сервере:
echo "$(date -u +%FT%TZ) $(git rev-parse --short HEAD)" >> /var/log/last-provision.log
Так любой, кто заходит на сервер, за секунду видит: последний прогон был вчера или восемь месяцев назад — и соответственно доверяет (или не доверяет) состоянию репозитория как источнику истины.
Где проходит разумная граница
Полная документация каждой строчки Ansible-роли — не цель и почти всегда контрпродуктивна: такая документация протухает ещё быстрее кода, потому что синхронизировать два параллельных описания вручную реалистично только на короткой дистанции. Задача не в объёме, а в том, чтобы фиксировать именно то, что код структурно не может передать:
| Что происходит | Достаточно кода | Нужен текст рядом |
|---|---|---|
| Установка пакета nginx | Да | Нет |
| Нестандартный порт в firewall | Частично | Да — почему именно этот порт |
| Значение таймаута, подобранное под нагрузку | Частично | Да — на основе чего подобрано |
| Временный костыль вместо починки причины | Нет | Да — явная пометка «техдолг», иначе живёт годами как «архитектура» |
| Отклонённая альтернатива (другая СУБД, другой оркестратор) | Нет | Да — иначе решение будет пересматриваться заново каждые полгода |
| Стандартный шаг деплоя (миграции, рестарт) | Да | Нет |
Практическое правило, которое работает лучше любого чек-листа: если, читая код автоматизации, вы сами задаёте себе вопрос «а почему тут именно так» — это сигнал написать ответ рядом, пока он ещё у вас в голове. Через полгода эта же строчка будет вызывать тот же вопрос, но ответа под рукой уже не будет.
Стоит также разделять зоны ответственности. Автоматизация (Ansible, скрипты деплоя) отвечает на вопрос «как привести систему в нужное состояние» и должна быть максимально явной и воспроизводимой без пояснений — примеры разбирали в статье про типовую настройку сервера через Ansible-playbook. Документация отвечает на вопрос «почему это состояние такое, какое есть» и «что учитывать при изменении». Смешивать эти роли — то есть пытаться впихнуть контекст решений в YAML через избыточные комментарии на каждую строку или, наоборот, дублировать в README логику, которая и так читается из кода, — обычно хуже, чем аккуратно их развести.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Разве Git-история коммитов не заменяет документацию?
Частично, но плохо. Сообщения коммитов пишутся в момент, когда контекст ещё свежий и кажется очевидным — поэтому получаются короткие и малоинформативные («fix», «update config»). Кроме того, чтобы восстановить решение через git blame, нужно уже знать, какую строку искать, а новый человек в команде этого не знает по определению.
Стоит ли документировать вообще каждый bash-скрипт?
Нет. Стандартные, самоочевидные действия (установка пакета, копирование файла, рестарт сервиса) не нуждаются в пояснении — оно только зашумит текст. Пишите про нестандартные решения, ограничения и отклонённые альтернативы — то, что не восстановить чтением кода.
Что делать, если документации уже нет, а инфраструктура старая?
Не пытайтесь описать всё сразу постфактум — это дорого и обычно не доводится до конца. Начните фиксировать контекст с этого момента: при следующей правке любого playbook или скрипта добавляйте короткий комментарий или запись в DECISIONS.md о том, почему меняете именно так. База накопится сама за несколько месяцев активной работы.
Кто должен вести эту документацию — DevOps-инженер или вся команда?
Тот, кто принял решение и знает контекст в момент принятия — обычно это автор конкретного изменения в инфраструктуре, а не выделенный «документатор» постфактум. Постфактум контекст уже частично забыт, и запись получается менее точной.
Не проще ли просто держать всё в голове, если команда маленькая?
Проще — ровно до отпуска, болезни или ухода единственного человека, который эту голову носит. Для команды из одного-двух человек порог входа в письменную фиксацию контекста ниже, чем цена простоя, когда критичный сервер нужно чинить, а объяснить особенности некому.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →