Бесплатный интерактивный курс · aicoding.space

12 факторов на момент решения: пакет skills-12factor

Практический курс по @dzhechkov/skills-12factor — набору из двенадцати навыков, каждый из которых включается в тот момент, когда AI-агент принимает одно конкретное архитектурное решение. Вместе с Артёмом ты установишь пакет одной командой, прогонишь унаследованный сервис заказов по всем двенадцати факторам, увидишь, где он их нарушает, и доведёшь его до состояния, в котором Kubernetes сможет его масштабировать, перезапускать и откатывать без потерь.

Содержание курса

1. Что за пакет и зачем ему двенадцать навыков

Навык на момент решения, установка одной командой, откуда взялся текст и насколько ему доверять.

Ключевая мысль: навык на момент решения

Артём открывает репозиторий сервиса заказов и через пять минут закрывает его обратно. Пароль от базы лежит в config.yml, сессии пользователей живут в памяти процесса, логи пишутся в /var/log/orders.log с ротацией по пятницам. Он знает, что «так нельзя», но не помнит точно, ПОЧЕМУ нельзя и что именно делать вместо этого.

Здесь и появляется пакет. @dzhechkov/skills-12factor — это двенадцать навыков для AI-агента, по одному на каждый фактор методологии Twelve-Factor App. Но важна не цифра, а форма: каждый навык — это навык на момент решения. Он не рассказывает про методологию целиком, а включается тогда, когда агент упирается в одну конкретную развилку: «куда положить ключ к платёжному API?», «процесс должен сам демонизироваться?», «логи в файл или в stdout?».

Что лежит внутри каждого из двенадцати навыков (одинаковый домашний формат):

  1. Decision — какое именно решение навык помогает принять и где его граница (что он НЕ решает — это отдано соседям).
  2. Protocol — шаги и таблица лакмусовых проверок: вопрос → проходной ответ → сигнал нарушения.
  3. Anti-patterns — что ломается и почему.
  4. Related decisions — к какому соседнему навыку уходит смежный вопрос.
  5. Источник, Self-check, Examples — ссылка на фактор, чеклист автора и примеры вопросов на русском и английском.

Ставится всё одной командой, и Артём ставит:

dz install @dzhechkov/skills-12factor --target claude-code

После этого все двенадцать навыков лежат в .claude/skills/. Имена навыков запоминать НЕ нужно: ты описываешь задачу обычными словами, а агент сам выбирает нужный навык. По данным README пакета, эта маршрутизация проверена отдельным гейтом: 103 положительных запроса и 82 «трудных» отрицательных, активация 100%, перехватов соседнего навыка 0% — это утверждение самого пакета, повторяю его с указанием источника, а не как собственное измерение.

Пакет опубликован: страница @dzhechkov/skills-12factor на npmjs.com, исходники — в публичном репозитории dz-harness на GitHub. Первоисточник — 12factor.net.

Теперь честно про доверие. Все двенадцать навыков помечены trust_tier: 0 — «машинно извлечено из первоисточника, человеком не проверено». Текст сгенерирован пакетом skills-book-digitizer, и это первый публичный пример оцифрованной книги в виде набора навыков. Два механизма держат его честным: гейт на дословность (ни одного нецитированного совпадения с источником длиной от 8 слов подряд — пакет проходит с нулём) и лицензия CC BY 4.0 с указанием авторов в файле NOTICE. Артём читает это и делает правильный вывод: пакет — сильный помощник, но каждое его «нарушение» стоит перепроверить по ссылке на фактор, прежде чем менять код.

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

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

2. Фактор I. Одна кодовая база — много деплоев

Граница между репозиторием, приложением и деплоем: что норма, что распределённая система, что нарушение.

Ключевая мысль: одна кодовая база — много деплоев

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

Навык 12factor-codebase-repo-mapping отвечает ровно за одну границу — между кодовой базой, приложением и деплоем. Правило простое и жёсткое: одна кодовая база — много деплоев. Кодовая база — один репозиторий (или группа репозиториев с общим корневым коммитом). Приложение — ровно одно на кодовую базу. Деплой — любой работающий экземпляр: прод, staging, ноутбук каждого разработчика.

Таблица лакмусовых проверок из навыка, своими словами:

  • Одна кодовая база → одно приложение → N деплоев на разных ревизиях — норма. То, что на ноутбуке Артёма есть коммиты, которых нет на проде, не делает их разными приложениями.
  • Несколько кодовых баз питают то, что вы зовёте «одно приложение», — это распределённая система. Назовите её честно и примените двенадцать факторов к каждому компоненту.
  • Одна кодовая база питает два приложения — нарушение. Общий код выносится в библиотеку и подключается через менеджер зависимостей.

Так что случай Артёма — третий: нарушение. И заметь, куда навык отправляет лечение: «вынести библиотеку» — это уже территория соседнего навыка про зависимости (фактор II). Каждый навык в пакете знает свою границу и называет соседа — это и есть раздел Related decisions.

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

💬 Спроси агента. Фраза «у нас монорепо, и оттуда деплоятся два разных сервиса — это по 12-факторам ок?» записана прямо в примерах навыка. Артём задал её — и получил не «зависит», а вердикт с таблицей: нарушение, два варианта лечения, ссылка на фактор I.

3. Фактор II. Манифест плюс изоляция зависимостей

Два механизма вместе, никогда по одному: список в репо и песочница на запуске, плюс проверка на голой машине.

Ключевая мысль: манифест плюс изоляция зависимостей

Сюрприз пришёл с CI. Артём вынес общий код в библиотеку, как советовал предыдущий навык, запустил сборку — и получил ModuleNotFoundError. Локально всё работает. У него есть requirements.txt, он честный человек. Что не так?

Навык 12factor-explicit-dependencies отвечает: у тебя есть половина решения. Фактор требует два механизма вместе — манифест плюс изоляция зависимостей, и только один из них не засчитывается:

  1. Манифест — полный список каждой библиотеки, закоммиченный в репозиторий (package.json, Gemfile, requirements для pip).
  2. Изоляция — инструмент, который на запуске не даёт системным пакетам просочиться внутрь (bundle exec, virtualenv, статическая линковка для C).

У Артёма был манифест, но не было изоляции: библиотека для работы с изображениями стояла глобально на его ноутбуке и потому «импортировалась нормально». На чистом CI её не было. Манифест без песочницы — это список, который никто не проверяет на полноту.

Два правила навыка, которые люди забывают чаще всего:

  • Одинаковый набор в dev и prod. Никаких «на проде и так есть X».
  • Внешние бинарники — тоже зависимости. Если приложение зовёт ImageMagick или curl через subprocess, известную копию нужно везти с собой, а не надеяться, что она есть на каждом хосте нужной версии.

И главная лакмусовая проверка — тест на голой машине: новый разработчик клонирует репозиторий на машину, где есть только рантайм языка и пакетный менеджер, выполняет одну сборочную команду — и всё работает. Если нужны ручные шаги «сначала поставь X и Y» — фактор нарушен. Артём вспоминает, что новичок в его команде поднимал сервис два дня. Теперь он знает, что это был не новичок, а фактор II.

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

💬 Спроси агента. «Работает локально, но на CI падает с ModuleNotFoundError» — это пусковая фраза навыка. Артём получил диагноз в одну строку: библиотека была ambient на dev-машине; добавить в манифест, запускать под изоляцией, перепроверить на чистом образе.

4. Фактор III. Конфиг в переменных окружения

Что считать конфигом, куда его класть, почему именованные профили — ловушка и как звучит проверка «репо можно открыть?».

Ключевая мысль: конфиг в переменных окружения

Вот и он — config.yml с паролем от базы, который Артём заметил в первые пять минут. Рядом ключ к платёжному API и адрес S3-бакета. Файл в .gitignore, «так что всё нормально». Реши за Артёма до того, как читать дальше: оставить в gitignore-файле, вынести в отдельный production.env, или что-то третье?

Навык 12factor-config-in-environment начинает не с ответа, а с определения. Конфиг — это всё, что различается от деплоя к деплою. Пароль базы, ключ платёжки, имя хоста этого деплоя — конфиг. Таблица маршрутов, внутренняя проводка зависимостей — одинаковы на всех деплоях, значит, это код, и он остаётся в репозитории. Артём три года считал «конфигом» всё, что лежит в yml-файле; теперь у него есть критерий.

Дальше — ответ: конфиг в переменных окружения. Не в константах кода, не в файле в репозитории — и, что неожиданно, даже не в файле под .gitignore. Навык называет три причины, почему gitignore-файл не спасает: он утекает случайно (один git add -A), плодит форматы и места хранения, привязывает к одному языку и фреймворку. Переменные окружения нейтральны к языку и переключаются на каждом деплое без правки кода — один и тот же собранный артефакт едет на staging и в прод.

Второе решение навыка — про форму хранения:

  • Плохо: именованные наборы development / staging / production. Каждый новый деплой требует нового профиля, а личные варианты вроде qa или joes-staging множатся комбинаторно.
  • Хорошо: каждая переменная — отдельная ручка. Новый деплой — это просто свои значения, а не новый профиль.

И лакмусовая проверка, которую Артём теперь задаёт себе перед каждым коммитом: можно ли прямо сейчас сделать репозиторий публичным, не раскрыв ни одного секрета? Если нет — что-то секретное всё ещё лежит в дереве.

Важная честная оговорка из навыка: переменные окружения дают переключение по деплоям и низкий риск случайного коммита, но НЕ дают ротации, шифрования на диске и аудита. Когда это нужно — поверх ставится менеджер секретов; сам фактор об этом молчит.

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

💬 Спроси агента. «У меня фича-флаги и ключ к платёжному API — положить в конфиг-файл в репозитории или куда?» — так звучит сценарий 3 из README пакета. Навык отделяет конфиг деплоя (в окружение) от внутреннего конфига приложения (в код) и даёт ссылку на фактор III.

5. Фактор IV. База, очередь, кэш — подключаемые ресурсы

Каждая сетевая зависимость доступна только через локатор из конфига — и потому меняется без правки кода.

Ключевая мысль: подключаемый ресурс через локатор

Пароль от базы Артём уже вынес в переменную окружения. Но хост базы db-01.internal по-прежнему стоит в коде строкой — «он же не секрет». А через неделю ему предстоит переезд с этой базы на управляемый сервис облака. Сколько правок кода это потребует? Правильный ответ по фактору IV — ноль.

Навык 12factor-backing-services-as-resources смотрит на одну вещь: насколько крепко код знает свои сетевые зависимости — базы, очереди, кэши, SMTP, хранилища файлов, внешние API. Ответ фактора: каждая такая зависимость — подключаемый ресурс через локатор. Локатор — это URL, строка подключения или пара учётных данных, и живёт он в конфиге. Код дотягивается до ресурса ТОЛЬКО через этот локатор, поэтому ресурс можно отсоединить, заменить и присоединить заново без единого коммита.

Заметь границу между двумя соседними навыками — её легко перепутать:

  • Фактор III (предыдущая секция) решает, ГДЕ физически лежит строка подключения — в переменной окружения.
  • Фактор IV (эта секция) решает, КАК код относится к тому, что за ней стоит: как к ресурсу, который можно поменять.

Три лакмусовых проверки навыка, каждая в одну фразу:

  1. Подмена поставщика. Локальный MySQL → управляемая база в облаке, локальный SMTP → Postmark: меняется только локатор в конфиге? Если нужна правка кода — ресурс не подключаемый, а вваренный.
  2. Счёт ресурсов. Два шарда MySQL — это ДВА ресурса и два локатора, а не «одна база».
  3. Живое восстановление. База умерла на железе: оператор восстанавливает из бэкапа, отсоединяет сломанную, присоединяет новую — как действие над конфигом, без релиза и без инженера с правами на код.

Честная оговорка навыка, которую стоит запомнить: подмена работает только между протокольно совместимыми ресурсами. Если замена меняет семантику или API, правка кода законна, и лакмус справедливо не проходит. Библиотека внутри процесса, к которой не ходят по сети, ресурсом не является — эта рамка ей ничего не добавляет.

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

💬 Спроси агента. «Перенести на managed RDS без правки кода» — одна из пусковых фраз навыка. Артём получил список из четырёх хостов, зашитых в код (включая «несекретный» db-01.internal), и один рецепт: локатор в конфиг, код — только через него.

6. Фактор V. Три стадии: build, release, run

Один поток в одну сторону, неизменяемый релиз с уникальным id, откат — переключение указателя, а не повторный пайплайн.

Ключевая мысль: три стадии build, release, run

На проде сервиса заказов лежит deploy.sh: он делает git pull, ставит зависимости, собирает статику, читает конфиг и запускает процесс — всё одним скриптом. Работает три года. Артём готов его оставить, пока агент не задаёт вопрос: «а что случится, если этот скрипт запустится сам в три часа ночи после перезагрузки сервера?»

Навык 12factor-build-release-run-separation раскладывает превращение кода в работающий деплой на три стадии: build, release, run — строго упорядоченные и односторонние:

  1. Build — взять код на конкретном закоммиченном коммите, подтянуть зависимости, скомпилировать бинарники и статику в один артефакт.
  2. Release — соединить этот артефакт с текущим конфигом деплоя и выдать релиз с уникальным неизменяемым id (метка времени или счётчик вроде v100).
  3. Run — просто запустить процессы против выбранного релиза. Ничего больше.

Код и конфиг встречаются ТОЛЬКО на стадии release. Поток идёт в одну сторону: правку, сделанную на живом сервере, некуда протолкнуть обратно в build — она пропадёт при следующем деплое. Именно поэтому «поправить на проде» — не лень, а нарушение фактора.

Вторая идея навыка — релизы как журнал, в который только дописывают. Релиз, однажды выпущенный, заморожен. Любое изменение — код или конфиг — рождает новый релиз, старый не правят. Раз id никогда не переиспользуется, откат тривиален: перевести указатель на прошлый известный релиз. Инструменты вроде Capistrano держат релизы в каталоге и переключают симлинк — откат за секунду.

Третья идея — сложность влево, run налегке. Build запускает разработчик, который смотрит на экран, — там можно позволить себе всё. Run может сработать сам: перезагрузка, менеджер процессов перезапустил упавший воркер, три часа ночи, никто не смотрит. Поэтому в run нет установки зависимостей, нет сборки статики, нет разрешения конфига — только запуск. Артём смотрит на свой deploy.sh и видит, что в нём все три стадии слиплись в одну — и вся тяжесть лежит в run.

Компромисс: три стадии и журнал релизов требуют пайплайна и места под старые артефакты (без политики хранения они копятся), зато откат становится переключением указателя, а ночной перезапуск — скучной и безопасной операцией.

💬 Спроси агента. «Почему приложение падает по ночам после рестарта?» — пример из навыка. Ответ: стадия run слишком тяжёлая; установку зависимостей, сборку статики и разрешение конфига унести в build, run оставить только запуск процессов.

7. Фактор VI. Процесс без состояния

Локальная память и диск — черновик одной транзакции; всё, что должно пережить запрос или рестарт, уходит в хранилище.

Ключевая мысль: процесс без состояния

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

Навык 12factor-stateless-processes отвечает: нет, и причина не в количестве экземпляров. Каждый процесс без состояния и ничего не разделяет с соседями: его локальная память и локальный диск могут исчезнуть в любую секунду — деплой, смена конфига, рестарт, переезд платформы на другое железо. Всё, что должно пережить запрос, фоновую задачу или рестарт, живёт в хранилище снаружи: база или key-value с истечением срока. Один экземпляр не спасает — его тоже перезапустят.

Что МОЖНО делать локально: черновик одной транзакции. Скачать большой файл, преобразовать, записать результат в хранилище, забыть. Что НЕЛЬЗЯ: прочитать закэшированное значение в СЛЕДУЮЩЕМ запросе. Следующий запрос, скорее всего, придёт в другой процесс, а одинокий процесс потеряет всё при рестарте.

Три правила навыка, которые Артём выписал себе на стикер:

  • Сессии — наружу, в Redis или Memcached с истечением срока. Тогда любой процесс обслужит любого вернувшегося пользователя.
  • Sticky sessions на балансировщике — нарушение. Если приложение работает, только когда пользователя вернули в тот же процесс, состояние спрятано внутри — вынести, а не прибивать маршрут.
  • Статику собирать на стадии build, не лениво на первый запрос с записью на диск. Диск процесса не общий и не вечный; под масштабированием копии разъедутся.

Лакмус, который навык предлагает задавать каждому байту, который процесс пишет: «Рестарт это потеряет?» Если да — во внешнее хранилище. Артём проверил: сессии в памяти — потеряет; загруженные пользователями файлы в /tmp/uploads — потеряет; скомпилированный CSS на диске — потеряет и пересоберёт на каждом новом поде. Три нарушения фактора VI на один сервис.

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

💬 Спроси агента. «Почему после деплоя слетает сессия?» — пусковая фраза навыка. Диагноз: сессия живёт в памяти процесса или за session affinity; лечение — Redis с истечением срока, чтобы любой процесс обслужил запрос.

8. Фактор VII. Приложение само слушает порт

Веб-сервер — обычная объявленная зависимость; лакмус — curl на localhost, когда среде отдали только приложение и его зависимости.

Ключевая мысль: приложение само слушает порт

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

Навык 12factor-port-binding даёт один ответ: приложение само слушает порт. Оно не «кладётся» в заранее установленный контейнер-сервер (PHP внутри Apache, Java внутри Tomcat), который среда обязана предоставить. Вместо этого веб-сервер приезжает как обычная объявленная зависимость — Tornado для Python, Thin для Ruby, Jetty для JVM — и работает в пользовательском пространстве. Процесс сам занимает порт и отвечает на http://localhost:<port>/. Публичное имя хоста и TLS — забота отдельного слоя маршрутизации перед приложением, не самого приложения.

Протокол навыка сводится к четырём шагам:

  1. Объявить веб-сервер среди зависимостей приложения (это связка с фактором II).
  2. Занять порт самому: процесс слушает и отвечает, снаружи ничего не нужно.
  3. Лакмус на localhost: отдать среде только приложение плюс его объявленные зависимости — оно всё ещё отвечает на http://localhost:<port>/? Значит, фактор соблюдён.
  4. Отдать имя хоста и TLS маршрутизатору перед приложением.

И неожиданное следствие, ради которого этот фактор стоит понять до конца: контракт «занял порт и слушаешь» не привязан к HTTP. Redis по своему протоколу, ejabberd по XMPP — тот же контракт. А раз сервис достижим просто по порту, одно приложение может быть подключаемым ресурсом для другого: потребитель получает его URL через конфиг — и это уже фактор IV.

Артём заменяет Apache на Gunicorn в requirements.txt, ставит bind 0.0.0.0:8080 и проверяет ровно так, как велит навык: пустой контейнер, приложение с зависимостями, curl http://localhost:8080/ — ответ 200. Показать код — вот он: одна строка в манифесте и одна команда проверки.

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

💬 Спроси агента. «Нужен ли внешний Apache/Tomcat, или приложение само должно слушать порт?» — пусковая фраза навыка. Ответ: встроить веб-сервер как объявленную зависимость и занять порт самому; контейнер, который среда «вставляет» снаружи, не нужен.

9. Фактор VIII. Типы процессов и формация

Процесс — единица масштаба: web и worker как именованные типы, счёт каждого — формация, жизненный цикл отдан менеджеру.

Ключевая мысль: типы процессов и формация

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

Навык 12factor-concurrency-process-model предлагает другую единицу измерения. Единица масштаба — процесс операционной системы, как у классического unix-демона. Разные виды работы получают разные именованные типы процессов: web для HTTP, worker для фоновых задач. Полный набор типов и количество копий каждого — это формация процессов: «3 web + 2 worker». Нужно больше мощности или появился новый вид работы — меняешь счёт нужного типа на большем числе машин, а не переписываешь приложение.

Две оси масштабирования, и навык расставляет их по старшинству:

  • Горизонтально — больше процессов — главная ось. Только процессы без состояния (фактор VI) масштабируются простым изменением счёта.
  • Вертикально — потоки и событийные рантаймы внутри процесса (EventMachine, Twisted, Node.js) — второстепенная оптимизация, упирающаяся в потолок одной машины.

Второе решение навыка, и оно жёстко связано с первым: процесс не управляет своим жизненным циклом. Никакой демонизации, никаких PID-файлов. Работает на переднем плане, пишет в stdout, а старт, перезапуск после падения и остановку по SIGTERM берёт на себя внешний менеджер — systemd на одном хосте, платформа в проде, Foreman с Procfile локально. Приложение, которое само себя перезапускает, дерётся с супервизором, а не помогает ему.

Лакмус навыка, который Артём произносит вслух на встрече: «десятикратный трафик и смешанная работа обрабатываются просто запуском большего числа процессов нужных типов, без редизайна?» Если да — масштабирует модель процессов. Если нет — сервис ещё не разделён на типы. Артём выносит генерацию PDF в тип worker, а вопрос «сколько web и сколько worker» становится настройкой формации, а не архитектурным спором.

Компромисс: разделение на типы и внешний менеджер добавляют операционных сущностей (Procfile, супервизор, очередь между web и worker), зато мощность становится числом, которое крутят, а не кодом, который переписывают.

💬 Спроси агента. «Потоки внутри процесса или больше процессов?» — пусковая фраза навыка. Ответ: горизонтально, больше процессов нужного типа; потоки — оптимизация внутри процесса, а не стратегия роста.

10. Фактор IX. Быстрый старт и мягкая остановка

Секунды до готовности, дренаж по SIGTERM, задача обратно в очередь — и корректность даже при внезапной смерти.

Ключевая мысль: быстрый старт и мягкая остановка

Теперь ты в роли Артёма, и решения принимаешь сам. Kubernetes скоро начнёт убивать и поднимать поды сервиса заказов, когда ему вздумается: выкатка, автомасштабирование, переезд на другой узел. Вопрос навыка 12factor-disposability-fast-startup звучит буквально: можно ли убить и перезапустить любой процесс в случайный момент без видимых пользователю ошибок и без потерянной работы?

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

  1. Быстрый старт. От команды запуска до приёма трафика — секунды, не минуты. Тогда планировщик свободно перемещает процессы, а выкатка не тормозит. Тяжёлый прогрев (кэш, JIT, загрузка модели) — в тёплый пул или предзагрузку, а не в ожидание.
  2. Мягкая остановка по SIGTERM. Web-процесс перестаёт принимать новые соединения на порту и даёт текущим запросам завершиться. Worker возвращает текущую задачу в очередь — NACK в RabbitMQ, release в Beanstalkd, снятие блокировки в Delayed Job — и выходит.
  3. Устойчивость к внезапной смерти. Обработчик остановки может не сработать вовсе: OOM-kill, сбой железа, SIGKILL после истечения льготного окна. Дизайн должен переживать это тем же путём восстановления: очередь сама возвращает задачу, если потребитель отвалился, а обработчик идемпотентен — повторная доставка не спишет деньги дважды.

Протокол навыка добавляет два инженерных пункта, которые отличают «прочитал» от «сделал»: ограничить дренаж конечным таймаутом (платформа всё равно пришлёт SIGKILL), и то, что не успевает завершиться в окне, сделать возобновляемым, а не просто ожидаемым; и использовать visibility timeout плюс ключ дедупликации, чтобы медленный, но живой worker не был обработан дважды.

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

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

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

11. Фактор X. Три разрыва между dev и prod

Разрыв во времени, в людях и в инструментах — и почему переносимость ORM не заменяет настоящий Postgres на ноутбуке.

Ключевая мысль: три разрыва между dev и prod

Остановись на секунду и оцени себя честно: сколько раз за последний год ты произносил «у меня локально работает»? Артём насчитал четыре. Каждый раз причина находилась, каждый раз — разная, и ни разу он не увидел, что это одна и та же болезнь.

Навык 12factor-dev-prod-parity называет её. Между разработкой и продом есть три разрыва, и фактор X требует сознательно держать каждый из них маленьким:

  1. Разрыв во времени — сколько изменение ждёт от написания до прода. Недели — традиционное приложение; часы или минуты — двенадцатифакторное.
  2. Разрыв в людях — кто выкатывает. Автор передаёт код «через стену» отделу эксплуатации — или сам выкатывает и сам смотрит, как оно живёт.
  3. Разрыв в инструментах — насколько локальный стек (ОС, веб-сервер, хранилище, версии и особенно ТИП каждого сервиса) повторяет прод.

У Артёма все три широкие: релиз раз в месяц, выкатывает отдельный человек, а локально SQLite вместо продового Postgres, «потому что быстрее ставится». Три разрыва — четыре «у меня работает» за год. Совпадение перестаёт быть совпадением.

Самая коварная часть — третий разрыв, и навык говорит о ней прямо: переносимость адаптера — не паритет сервиса. ORM, который умеет MySQL, Postgres и SQLite, делает подмену безопасной НА ВИД, но прячет ровно те несовместимости, которые вылезут на проде. Лечение недорогое: Postgres локально ставится пакетным менеджером или контейнером за минуты.

И честная граница из навыка: нулевой разрыв — не цель и не реальность. Большой управляемый кластер на ноутбук не положишь — берётся ближайший эквивалент или общий staging. В регулируемых областях разделение обязанностей и медленный темп могут быть предписаны, и это законно торгуется против разрывов во времени и в людях. Цель — держать разрывы такими, чтобы «прошло локально» было надёжным сигналом про прод.

Компромисс: узкие разрывы стоят дисциплины частых мелких выкаток и настоящего Postgres на каждом ноутбуке, зато локальный зелёный прогон снова предсказывает поведение прода.

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

12. Фактор XI. Логи как поток событий в stdout

Приложение излучает события по одному в строку без буфера; захват, сбор, ротация и алерты — работа среды, а не кода.

Ключевая мысль: логи как поток событий в stdout

Помнишь /var/log/orders.log с ротацией по пятницам из первой секции? Пришло его время. Артём защищает его до последнего: «а куда ещё писать? В контейнере файл хотя бы можно скачать». Навык 12factor-logs-as-streams отвечает вопросом: а кто в твоём контейнере будет ротировать этот файл, когда подов станет двадцать?

Решение фактора XI: логи как поток событий в stdout. Каждая строка — одно событие в непрерывном упорядоченном по времени потоке. Приложение пишет его в стандартный вывод без буфера и ничего не знает о том, куда поток попадёт дальше. Открыть именованный файл, выбрать каталог, ротировать по размеру или времени, отправить в архив — всё это приложение НЕ делает нарочно.

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

Путь события после stdout — по шагам, и Артём собирает его в упражнении:

  1. Приложение пишет событие в stdout, одну строку, без буфера.
  2. Среда исполнения захватывает поток каждого процесса.
  3. Роутер (Fluentd, Logplex) собирает потоки всех процессов приложения воедино.
  4. Индексатор или хранилище данных: поиск по истории, графики «запросов в минуту», алерт «ошибок в минуту больше порога». Всё это — ПОСЛЕ stdout, никогда внутри процесса.

Две практические детали, без которых «просто пиши в stdout» ломается:

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

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

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

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

13. Фактор XII. Разовый процесс против того же релиза

Миграция, бэкфилл, REPL к живой базе — как короткоживущий процесс на том же коде, конфиге и изоляции, что и web.

Ключевая мысль: разовый процесс против того же релиза

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

Навык 12factor-admin-processes отвечает на второй вопрос. Любая разовая управленческая задача — миграция, бэкфилл, скрипт починки данных, REPL к живым моделям — запускается как разовый процесс против того же релиза, что и работающие web и worker: тот же код, тот же конфиг, тот же механизм изоляции зависимостей. Админ-задача — не особое окружение и не «мой ноутбук», а ещё один процесс, запущенный против задеплоенного релиза, только короткоживущий.

Протокол навыка — пять пунктов:

  1. Классифицировать. Это одноразовое действие (схема, бэкфилл, починка, осмотр), а не постоянное обслуживание? Тогда — разовый процесс.
  2. Привязать к релизу. Задача выполняется против того же релиза (код + конфиг), что живые процессы, — и не может разъехаться с ними. Это опора на фактор V.
  3. Использовать ту же изоляцию. Тот же вендорный интерпретатор или bundler, что у web, — не другой рантайм. Это опора на фактор II.
  4. Коммитить разовые скрипты. fix_bad_records живёт в репозитории рядом с приложением, а не в файле на одной машине, который никто не воспроизведёт.
  5. Запускать по месту. Локально — из shell в каталоге приложения; в проде — через SSH или удалённый запуск платформы, всё так же против релиза.

Лакмус навыка — показать код, и он показательно короткий: если web стартует через bundle exec thin start, миграция запускается через bundle exec rake db:migrate. Тот же bundler, тот же интерпретатор — проходит. Через другой рантайм — не проходит. Для Python на virtualenv — тот же вендорный bin/python для manage.py migrate, что и для веб-сервера.

Артём смотрит на путь, который прошёл: пароль в окружении, ресурсы через локаторы, три стадии деплоя, процессы без состояния, свой порт, формация web и worker, дренаж по SIGTERM, Postgres на ноутбуке, логи в stdout — и миграция, запущенная против того же релиза через ту же изоляцию. Сервис заказов готов к Kubernetes не потому, что его упаковали в контейнер, а потому, что по каждому из двенадцати решений у него теперь есть ответ.

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

💬 Спроси агента. «Как запустить миграцию против прода» — пусковая фраза навыка. Ответ: разовый процесс против задеплоенного релиза, через SSH или удалённый раннер, тем же bundler или интерпретатором, что у web; никогда — из рассинхронизированной локальной копии.

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

Как поставить пакет и нужно ли запоминать имена навыков?

dz install @dzhechkov/skills-12factor --target claude-code — все двенадцать навыков лягут в .claude/skills/. Имена запоминать не нужно: опиши задачу обычными словами, агент сам включит нужный навык на момент решения. Хочешь один навык — dz init --target claude-code --select 12factor-config-in-environment.

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

Все двенадцать помечены trust_tier 0 — машинно извлечено из The Twelve-Factor App, человеком не проверено. Дословных совпадений с источником длиной от 8 слов нет (гейт проходит с нулём), лицензия CC BY 4.0 с указанием авторов в NOTICE. Каждое найденное нарушение стоит сверить с 12factor.net, прежде чем менять код.

В каких ситуациях пакет окупается?

README называет пять: аудит существующего сервиса на соответствие двенадцати факторам; проектирование нового сервиса правильно с первого дня; одно решение прямо сейчас, в момент развилки; подготовка к контейнерам и Kubernetes (порты, мягкая остановка, масштабирование, логи); объяснение «почему» при онбординге и на ревью.

Чем фактор III (конфиг) отличается от фактора IV (ресурсы)? Они же оба про строку подключения.

III решает, ГДЕ физически лежит строка подключения — в переменной окружения. IV решает, КАК код относится к тому, что за ней стоит: как к подключаемому ресурсу, который можно заменить без правки кода. Конфиг даёт локатор, ресурс его потребляет.

Зачем факторы VI, VIII и IX разнесены по трём навыкам, если все про процессы?

У каждого своё решение. VI — где процесс держит состояние (нигде локально). VIII — как масштабироваться (типы процессов и формация) и кто управляет жизненным циклом (внешний менеджер). IX — как процесс стартует и останавливается (секунды до готовности, дренаж по SIGTERM, устойчивость к внезапной смерти). Каждый навык в разделе Related decisions называет соседа, к которому уходит смежный вопрос.

Можно ли использовать пакет не с Claude Code?

Да: цели установки — claude-code, codex, opencode, hermes, openclaude, copilot. Для произвольного потребителя есть dz bundle --select <навыки> --out ./12factor-bundle — самодостаточное дерево skills/.

Нулевой разрыв между dev и prod — обязательное требование?

Нет. Фактор X требует держать три разрыва (время, люди, инструменты) достаточно узкими, чтобы «прошло локально» было надёжным сигналом. Большой управляемый кластер на ноутбук не положить — берётся ближайший эквивалент или общий staging; регуляторные требования законно торгуются против разрывов во времени и в людях.