Кожна студія каже, що "будує production-grade". Фразу зачовгали настільки, що вона тепер не означає майже нічого. Промислова якість — це не настрій і не тариф на сторінці з цінами. Це чекліст із гострими краями, і більшість пунктів у ньому не мають жодного стосунку до продукту, за який клієнт насправді платить.
Ми веземо однаковий одностороінковий чекліст у кожен проєкт. Він нецікавий. Це не те, що засновник поклав у дорожню карту. Це нудний фундамент, який вирішує, чи виживе цікава робота над ним після зіткнення з реальними користувачами. Ця стаття і є той чекліст у формі абзаців, із міркуваннями, які ми зазвичай пропускаємо на стартовій зустрічі, бо ніхто не хоче їх чути.
Структуровані логи з ідентифікатором запиту, від входу до виходу
Перше, що ми додаємо до будь-якого успадкованого коду, — це структуроване логування з ідентифікатором запиту, який передається від точки входу аж до запиту в базу та будь-якого фонового завдання, породженого по дорозі. Не "у нас є логер". Не "у нас є акаунт Sentry". JSON-рядок на подію зі стабільною схемою та ідентифікатором запиту, який дозволяє відновити, що сталося з одним конкретним користувачем у конкретний момент.
Причина буденна. О 3 ночі, з розпливчастим повідомленням про баг від одного клієнта, у вас є хвилини — не години — щоб знайти потрібну голку в копиці сіна. Логи, по яких можна швидко шукати, разом з ідентифікатором кореляції перетворюють це з піврічного слідства на двохвилинний запит. Додати все це з першого дня — невелика робота. Зробити те саме заднім числом, після шести місяців неструктурованих print-ів, — це проєкт на кілька тижнів, який ніхто не профінансує.
Справжнє сховище секретів, а не чийсь .env
Кількість продакшн-систем, які ми бачили, що працюють на .env-файлі, який живе на ноутбуці одного розробника й перезаливається руками щоразу, коли змінюється, — щиро пригнічує. Це не сховище секретів. Це чутка про секрет.
Справжнє сховище секретів має контроль доступу, журнал аудиту, ротацію та спосіб відкликати скомпрометовані облікові дані без перерозгортання всіх сервісів, які їх використовують. Конкретний інструмент важить менше за властивості. Ми обираємо те, що підходить до середовища розгортання: хмарні менеджери, коли хмара вже задіяна, самостійно розгорнуте сховище, коли ні. Непорушна вимога: жоден секрет не живе в Git-репозиторії, жоден секрет не пересилають через Slack, а ротація — це рутинна операція, а не паніка.
Автентифікація, що витримує базовий OWASP
Більшість коду автентифікації, який ми успадковуємо, обробляє щасливий сценарій. Користувач із правильним паролем заходить. А от у нещасливих сценаріях і закопані тіла. Скидання сесій при зміні пароля. Обмеження частоти запитів на вхід, скидання пароля та будь-який ендпойнт, який повертає "так, такий email існує" з вимірюваною різницею в часі відповіді. CSRF-токени, прив'язані до операцій, які насправді змінюють стан, а не розкидані випадково по всіх формах. Послідовна модель прав замість if user.is_admin, розпорошеного по сорока в'юшках.
Нічого з цього не є новим дослідженням. Кожен пункт є на першій сторінці шпаргалки OWASP. Ми не претендуємо на інсайт — ми претендуємо на дисципліну. Чекліст існує саме для того, щоб дисципліна не залежала від того, чи інженер, який робив роботу, випадково згадав кожну категорію того дня, коли писав форму входу.
Схема зі справжніми обмеженнями
Схема бази даних без обмежень — це пропозиція, а не правило. Ми втратили лік випадкам, коли команда зрештою з'ясовує важким шляхом, що user_id у якійсь таблиці буває NULL, буває висячим посиланням, а одного разу — пам'ятно — був рядком. Код застосунку, який покладається на те, що ці дані поводяться адекватно, переповнений захисними перевірками, які існують лише тому, що схема не виконує своєї роботи.
Ми додаємо NOT NULL там, де значення дійсно потрібне. Зовнішні ключі там, де є зв'язок, із явною семантикою ON DELETE замість того, що Postgres ставить за замовчуванням. CHECK для діапазонів та переліків. UNIQUE індекси для того, що має бути унікальним. Ціна — певний дискомфорт під час міграції. Користь — ціла категорія багів "як ми взагалі дійшли до такого стану?" стає структурно неможливою.
Міграції, які проходять уперед і назад
Міграція, що йде лише вперед, — це двері в один бік. Ви її випустили, і з цієї миті ваша здатність відкотити застосунок обмежена здатністю відкотити базу — а вона, якщо міграція була деструктивною, нульова.
Ми пишемо міграції з визначеним шляхом відкату та тестуємо його. Для деструктивних змін — викинути колонку, звузити тип — ми використовуємо підхід "розширити й стиснути": спочатку випускаємо адитивну зміну, потім розгортаємо застосунок, який більше не потребує старої колонки, і лише після цього викидаємо її окремою наступною міграцією. Це довше за коротший шлях. Але це означає, що "відкотити реліз" залишається реальною опцією протягом усього вікна, у якому може виявитися регресія.
Релізи, оборотні менш ніж за дві хвилини
Найцінніша властивість конвеєра розгортання — це здатність його скасувати. Не здатність розгорнути. Розгорнути може будь-хто. Питання в тому, що відбувається, коли реліз вийшов, частота помилок стрибнула й хтось має зробити так, щоб новий код перестав працювати — просто зараз, до того як наступна демонстрація в кінці спринту перетвориться на вибачення.
Двохвилинний відкат — це не функція, яку прикручують зверху. Це властивість усього конвеєра: незмінні артефакти, атомарні перемикання між версіями, міграції бази, які не палять міст за собою, секрети та конфігурація, які не сплетені з релізом. Ми проєктуємо під це з першого дня, бо допасовувати таке заднім числом до конвеєра, який не був під це збудований, важче, ніж зробити правильно з першого разу.
Інструкції реагування, читабельні о 3 ночі
Останній пункт — той, який пропускають найчастіше, і той, який платить найбільший дивіденд першого разу, коли стається реальний інцидент. Інструкція реагування (runbook) — це короткий, прямий документ, який каже черговому інженеру, що робити, коли спрацьовує конкретний сигнал. Не "розібратися з проблемою". А список команд, дашборди для перевірки та три найімовірніші причини, упорядковані за частотою.
Тест для гарного runbook жорстокий: дайте його інженеру, який ніколи не бачив цю систему, о 3 ночі, без контексту, і подивіться, чи зможе він зняти алерт, не піднімаючи нікого іншого. Більшість таких інструкцій провалюють цей тест із першої спроби. Це нормально — цінність у другій спробі, після інциденту, коли команда переписує інструкцію проти того, що вони насправді хотіли б знати о 3 ночі. За кілька ітерацій runbook стає інституційною пам'яттю, яку команді інакше довелося б тримати цілком у людських головах.
Компроміс, уголос
Це непривабна робота. Вона забирає приблизно два тижні сфокусованих зусиль на старті проєкту й не випускає жодної функції, яку засновник міг би показати клієнту. Ми кажемо це чітко на кожній стартовій зустрічі, бо альтернатива — це коли роботу тихо пропускають, а потім переграють під тиском аварії через шість місяців, коли її ціна втричі вища, а люди, які її роблять, не сплять.
Аргумент не в тому, що це весело. Аргумент у тому, що рахунок прийде так чи так. Можна оплатити його зараз, у спокійних умовах, командою, яка має контекст зробити це правильно. Або оплатити пізніше, в каналі інциденту опівночі, з тими, хто випадково доступний. Ми наполегливо рекомендуємо перший варіант і будуємо свої проєкти так, щоб він був за замовчуванням.
І одна примітка про AI
Це також та планка, яку AI-згенерований код наразі сам по собі не бере. Модель видасть вам правдоподібний чорновик будь-якого пункту з цього списку. Цей чорновик буде відсотків на 70 правильним і на 100 тихо хибним щодо тих 30 відсотків, які мають значення: формулювання ON DELETE, поведінка обмежувача частоти при збої, шлях відкату, який мовчки припускає одноекземплярне розгортання. Закрити цей розрив — це саме та робота, за яку нам платять. Це, наразі, робота, яка ще не пишеться на відчуттях.


