Начни здесь

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

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

Знакомься: Артём

Артём — бэкенд-разработчик, которому достался сервис заказов трёхлетней давности. Задача на квартал: переехать в Kubernetes. Он ставит пакет skills-12factor и идёт по факторам вместе с тобой: в каждой секции сначала находит нарушение в своём сервисе, потом чинит — и ты решаешь вместе с ним.

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

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; регуляторные требования законно торгуются против разрывов во времени и в людях.

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

Лицензии шрифтов

Onest

Copyright 2021 The Onest Project Authors (https://github.com/googlefonts/onest)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

JetBrainsMono

Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.