Twelve-Factor App
Назначение: 12 принципов проектирования приложений для облачных платформ.
Аудитория: разработчики, девопс-инженеры, техлиды, архитекторы.
Статус: классическая методология (Adam Wiggins и соавторы, Heroku, 12factor.net, 2011).
Не путать с: cloud-native как общим направлением (CNCF) — Twelve-Factor лишь часть его фундамента; SOLID — принципами дизайна кода, а не эксплуатации.
Общее
Twelve-Factor App («двенадцатифакторное приложение») — методология проектирования приложений, пригодных для эксплуатации на современных облачных платформах. Она фиксирует двенадцать правил — факторов, — каждое из которых снимает конкретный класс проблем развёртывания, масштабирования и сопровождения: хранение конфигурации, объявление зависимостей, разделение стадий поставки, отсутствие состояния в процессах, логирование потоками. Методология опубликована в 2011 году на сайте 12factor.net; автор — Адам Уиггинс (Adam Wiggins), сооснователь PaaS-платформы Heroku, при участии инженеров команды. Текст вырос не из теоретических рассуждений, а из операционного опыта: Heroku эксплуатировала сотни тысяч приложений своих клиентов и наблюдала, какие свойства кода позволяют платформе автоматически деплоить, масштабировать и восстанавливать приложение — а какие превращают каждое из этих действий в ручную работу.
Философия методологии выражается одной фразой: приложение — самодостаточный сервис, которым платформа может управлять без человека. Деплой, остановка, замену процессов, сбор логов и подключение ресурсов должна выполнять платформа; от приложения требуется лишь соблюдать контракт — держать конфигурацию снаружи, не хранить состояние в памяти процесса, писать логи в стандартные потоки, быстро стартовать и останавливаться. Отсюда исходная формулировка адресата: методология писалась для SaaS-приложений, живущих в непрерывном деплое и эластичном облаке, — но на практике оказалась полезна любому приложению, которое эксплуатируется хотя бы на двух окружениях.
Авторы формулировали цели явно. Приложение, соблюдающее факторы:
- использует декларативные форматы автоматизации подготовки окружения — это удешевляет ввод новых разработчиков в проект;
- имеет чистый контракт с операционной системой — максимальную переносимость между средами исполнения;
- пригодно к деплою на современных облачных платформах — без ручного администрирования серверов;
- минимально расходится между разработкой и продакшеном — что делает возможным continuous deployment;
- масштабируется без существенных изменений в инструментах, архитектуре и процессах разработки.
Терминология методологии компактна. Приложение (app) — единица поставки: код + набор процессов + конфигурация. Деплой (deploy) — запущенный экземпляр приложения в конкретном окружении: их может быть много (prod, staging, локальные окружения разработчиков), и все они собираются из одной кодовой базы. Фактор — независимое правило: факторы не образуют иерархии и применимы по отдельности, хотя и усиливают друг друга (stateless-процессы VI осмысленны вместе с concurrency VIII и disposability IX).
Важна граница применимости. Twelve-Factor — методология одного приложения как единицы поставки, а не декомпозиции системы: она не говорит, как разбить систему на сервисы и не противопоставляет монолит микросервисам. Каждый микросервис — это отдельное twelve-factor-приложение со своей кодовой базой и своими деплоями; корректно спроектированный монолит тоже может следовать всем двенадцати факторам. Исторически методология занимает место между сервисными подходами 2000-х (SOA) и облачной экосистемой 2010-х: она вышла раньше Docker (2013) и популяризации микросервисов (2014), но зафиксировала именно те свойства, которые контейнеры и оркестраторы затем сделали нормой.
Практическая ценность методологии — общий язык. Фразы «конфигурация — в окружении», «процессы stateless», «логи — потоки событий» компактны и общеизвестны; на них удобно ссылаться при обсуждении эксплуатации из статей по микросервисам и observability, в код-ревью инфраструктурных изменений и при миграции в Kubernetes. Сводная таблица факторов:
| № | Фактор | Суть | Типичное нарушение |
|---|---|---|---|
| I | Codebase | Одна кодовая база в VCS, много деплоев | Несколько приложений в одном репозитории; одно приложение в нескольких репозиториях |
| II | Dependencies | Зависимости объявлены явно и изолированы | Опора на «и так установленные» системные пакеты |
| III | Config | Конфигурация — в окружении, не в коде | Пароль от БД в репозитории; ветвление поведения по имени сервера |
| IV | Backing services | БД, очереди, хранилища — подключаемые ресурсы по URL | Жёсткая привязка кода к конкретному инстансу или вендору |
| V | Build, release, run | Строгое разделение стадий сборки, релиза и выполнения | Правки кода руками на работающем сервере |
| VI | Processes | Процессы stateless и разделяемые | Сессии в памяти процесса; sticky sessions в балансировщике |
| VII | Port binding | Сервис экспортируется привязкой к порту | Приложение не запускается без внешнего веб-сервера |
| VIII | Concurrency | Масштабирование через процессы, а не наращивание одного процесса | Один гигантский процесс, который масштабируется только как целое |
| IX | Disposability | Быстрый запуск и graceful shutdown | Долгая «прогревка»; игнорирование SIGTERM |
| X | Dev/prod parity | Минимум различий между окружениями | Dev на SQLite и macOS, prod на Postgres и Linux |
| XI | Logs | Логи — потоки событий в stdout/stderr | Приложение пишет и ротирует собственные файлы логов |
| XII | Admin processes | Административные задачи — разовые процессы в среде приложения | Миграция схемы с ноутбука разработчика напрямую в prod |
Двенадцать факторов
Каждый фактор ниже разобран по одной схеме: суть — зачем — типичное нарушение — как это проявляется в микросервисах и контейнерах.
I. Codebase — одна кодовая база, много деплоев
Суть. Приложение отслеживается в системе контроля версий как одна кодовая база; из неё собирается неизменяемый артефакт, который развёртывается многократно: production, staging, локальное окружение разработчика, инстанс для каждого клиента. Деплой = артефакт + конфигурация; кодовая база при этом всегда одна. Обратное тоже обязательно: если система состоит из нескольких приложений, у каждого — собственная кодовая база.
Зачем. Единство кодовой базы гарантирует, что любой деплой воспроизводим из истории версий, а различия между окружениями сводятся к конфигурации (фактор III), но не к коду. Критерий разграничения «приложение против системы приложений»: деплоятся ли части по отдельности — значит, это разные кодовые базы.
Типичное нарушение. «Репозиторий-помойка» с несколькими приложениями внутри: у них общий релизный цикл, невозможно деплоить независимо, теги бессмысленны. Зеркальное нарушение — одно приложение, размазанное по нескольким репозиториям с рассинхронизированными версиями.
В микросервисах и контейнерах. Каждый микросервис — своя кодовая база и свой конвейер поставки; это и есть граница декомпозиции, проведённая по фактору I. Контейнерный образ — материализация тезиса «один артефакт — много деплоев»: один образ с тегом v1.4.2 запускается в staging и production, различаясь только переменными окружения. Контраст с монолитом здесь условен: монолит — тоже один артефакт и много деплоев; разница лишь в том, что частей, которые хочется деплоить раздельно, у него нет.
II. Dependencies — явное объявление и изоляция зависимостей
Суть. Все зависимости приложения — библиотеки, драйверы, утилиты — объявляются явно (манифест: package.json, requirements.txt, go.mod) и изолируются от системных (vendoring, виртуальное окружение, npm ci в чистую директорию). Приложение никогда не полагается на неявное существование системных пакетов.
Зачем. Явный манифест делает сборку воспроизводимой: любой разработчик и CI-сервер разворачивают идентичный набор зависимостей одной командой. Изоляция исключает «конфликт версий с тем, что установлено в системе» — класс порчи окружения, когда обновление системной библиотеки ломает приложение, а выяснить зависимость невозможно.
Типичное нарушение. Приложение молча использует системный libxml или установленный глобально CLI-инструмент: «у меня работает». На машине без этого пакета сборка или запуск падают загадочными ошибками. Сюда же — незакреплённые версии (^, latest): сборка «вчера» и «сегодня» различаются без единого изменения в коде.
В микросервисах и контейнерах. Контейнерный образ доводит фактор до уровня операционной системы: Dockerfile декларирует зависимости целиком — от npm ci до системных библиотек конкретного базового образа, а слои кэшируют их установку. Для микросервисов фактор означает, что сборка каждого сервиса самодостаточна и не требует «подготовленной» build-машины.
# II: зависимости объявлены явно и зафиксированы lock-файлом
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
III. Config — конфигурация в окружении, не в коде
Суть. Всё, что различается между деплоями (endpoints БД и очередей, учётки сторонних сервисов, ключи), — конфигурация, и она хранится в переменных окружения, а не в коде. Код одинаков во всех деплоях; окружение различается. Секреты — тоже конфигурация, а не «особый случай», попадающий в репозиторий.
Зачем. Строгий раздел «код против конфигурации» делает смену окружения операцией без пересборки, а смену кода — операцией без редактирования настроек. Переменные окружения не зависят от языка и ОС, не попадают в VCS случайно и задаются платформой деплоя на любом уровне — от процесса до кластера.
Типичное нарушение. config/production.js с паролем от БД, закоммиченный в репозиторий: утечка при следующем git push в публичное зеркало. Или ветвление поведения по имени хоста (if (host === 'prod-01')) — конфигурация, замаскированная под код, которую невозможно изменить без релиза.
В микросервисах и контейнерах. В Kubernetes переменные окружения подставляются из ConfigMap (обычные настройки) и Secret (чувствительные данные) — это каноническое воплощение фактора III. Один и тот же образ сервиса запускается в dev и prod, различаясь только env:
# один образ — два деплоя, отличие только в окружении
docker run --rm -d \
-e DATABASE_URL=postgres://staging-db:5432/shop \
-e S3_BUCKET=shop-staging \
shop:v1.4.2
docker run --rm -d \
-e DATABASE_URL=postgres://prod-db:5432/shop \
-e S3_BUCKET=shop-prod \
shop:v1.4.2
// config.ts — вся конфигурация читается из окружения при старте
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Обязательная переменная ${name} не задана`);
return value;
}
export const config = {
port: Number(process.env.PORT ?? 8080),
databaseUrl: required('DATABASE_URL'),
sessionStore: required('SESSION_STORE'),
s3Bucket: required('S3_BUCKET'),
} as const;
IV. Backing services — подключаемые ресурсы
Суть. Базы данных, очереди, кэши, почтовые шлюзы, S3-хранилища — backing services, и приложение работает с ними одинаково: как с подключёнными ресурсами по URL/учётке из конфигурации. Локальный PostgreSQL в контейнере разработчика и управляемый облачный кластер — один и тот же ресурс с разными адресами; смена одного на другого не требует изменения кода.
Зачем. Ресурс заменяем без релиза: достаточно поменять переменную окружения и перезапустить процессы. Это же выравнивает окружения (фактор X): «на моей машине» используется тот же механизм подключения, что и в production. Граница между «нашей» инфраструктурой и сторонним API размывается там, где ей и следует быть, — в конфигурации.
Типичное нарушение. Код «знает» про конкретный инстанс: зашитый путь к локальному файлу, обращение к сетевому диску, обращение к БД через сокет на localhost, специфичные для вендора расширения там, где достаточно стандарта. Перенос такого приложения в облако превращается в релиз кода.
В микросервисах и контейнерах. Сетевые адреса backing services приходят в контейнер через service discovery или DNS-имя сервиса Kubernetes; обмен событиями через очереди — типовой backing service для микросервисов (очереди сообщений и интеграционные паттерны — темы отдельных статей базы знаний, в подготовке). Учёт этого фактора — условие переносимости между облаками и локальным контуром.
V. Build, release, run — строгое разделение стадий
Суть. Поставка разделена на три стадии, которые не смешиваются. Build: из кодовой базы по фиксированной версии собирается артефакт и подтягиваются зависимости. Release: артефакт объединяется с конфигурацией конкретного деплоя — получается релиз, неизменяемый и имеющий уникальный идентификатор (например, v1.4.2). Run: платформа запускает процессы из релиза в нужном количестве. Релиз нельзя менять — можно только собрать следующий или откатиться на предыдущий.
Зачем. Неизменяемый адресуемый релиз решает сразу три задачи: воспроизводимость (баг репортится против конкретного идентификатора), аудит (что и когда крутилось в prod) и откат (повторный запуск прошлого релиза — минутная операция, а не «пересобрать прошлую версию по памяти»). Побочный эффект: попытки «подправить на работающем сервере» становятся архитектурно невозможными.
Типичное нарушение. SSH на продакшен-сервер и правка файла/«горячая» замена библиотеки: следующий деплой стирает правку, а расследование инцидента упирается в вопрос «а что реально там крутилось?». Сюда же — запуск миграций руками в момент деплоя без фиксации релиза.
┌──────────┐ ┌─────────────┐ ┌───────────────────┐ ┌────────────────┐
│ Codebase │ │ Build │ │ Release │ │ Run │
│ (Git) │────►│ компиляция, │────►│ артефакт + config │────►│ web ×3 │
└──────────┘ │ зависимости │ │ v1.4.2 │ │ worker ×5 │
└─────────────┘ └─────────┬─────────┘ └────────────────┘
воспроизводим │ масштабируется
неизменяем и адресуем
└─ откат = запуск v1.4.1
В микросервисах и контейнерах. Контейнерная экосистема буквально построена на этой схеме: docker build — build, тегированный образ (лучше по digest, чем по мутируемому тегу latest) — release, docker run / Deployment — run. CI/CD-конвейер микросервиса собирает образ один раз и проводит его по конвейеру dev → staging → prod, подмешивая только конфигурацию; сборка отдельно для каждого окружения — распространённое нарушение фактора V.
VI. Processes — процессы без состояния
Суть. Приложение выполняется как один или несколько stateless-процессов: всё, что должно пережить запрос или перезапуск, — сессии, файлы, кэш, задачи — хранится в backing services (фактор IV). Память процесса и его локальный диск эфемерны: всё, что в них осталось, исчезает вместе с процессом, поэтому локальное хранилище годится разве что под одноразовый кэш.
Зачем. Statelessness — ключ к горизонтальному масштабированию (фактор VIII) и отказоустойчивости: любой запрос может обработать любой процесс, поэтому процессы можно добавлять, убивать и заменять без потери данных и без «привязки» пользователя к конкретной машине.
Типичное нарушение. HTTP-сессии в памяти процесса, вынуждающие включать sticky sessions в балансировщике: теперь нельзя просто добавить процесс, а падение одного узла выкидывает пользователей из системы. Другие варианты — загруженные пользователем файлы на локальном диске и «инмемори» кэш, который живёт в каждом процессе по-своему и не инвалидируется согласованно.
В микросервисах и контейнерах. Под Kubernetes pod — одноразовая единица: у него нет гарантий адреса и времени жизни, перезапуск происходит при каждом обновлении и на каждом подозрении на неисправность; состояние обязано жить снаружи — в Redis, Postgres, S3. Для микросервисов это почти тавтология: сервис, хранящий состояние в памяти процесса, теряет главные преимущества микросервисной архитектуры — независимое масштабирование и устойчивость к падениям.
┌──────────────────────┐
│ балансировщик │
└──────────┬───────────┘
round-robin, без sticky sessions
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ stateless-процессы:
│ web #1 │ │ web #2 │ │ web #3 │ любой запрос — любому;
└────┬────┘ └────┬────┘ └────┬────┘ падение любого не теряет данные
│ │ │
└───────┬───────┴───────┬───────┘
▼ ▼
┌────────────┐ ┌────────────┐ backing services — единственное
│ Postgres │ │ Redis │ место, где живёт состояние
│ (данные) │ │ (сессии) │
└────────────┘ └────────────┘
VII. Port binding — экспорт через привязку к порту
Суть. Приложение — самодостаточный сервис: оно само открывает слушающий сокет (порт) и обслуживает HTTP-запросы; веб-сервер существует внутри процесса как библиотека, а не как внешний контейнер, в который приложение «вкладывается». Приложение, слушающее порт, может стать backing-сервисом для другого приложения.
Зачем. Самодостаточность делает приложение единым юнитом поставки: его запускает платформа обычным process-менеджером, без установки и настройки Apache/nginx на хосте. Взаимодействие сервисов друг с другом сводится к URL — тот же механизм, что и для внешних ресурсов (фактор IV).
Типичное нарушение. Приложение в модели «внешний веб-сервер + модуль» (классический PHP-хостинг): код не запускается сам, а исполняется сервером при запросе; деплой превращается в настройку сервера, а не в запуск приложения.
В микросервисах и контейнерах. В мире контейнеров фактор стал нормой по умолчанию: каждый контейнер запускает один процесс, слушающий порт (8000, 3000, 8080), а сетевой слой кластера — Service, Ingress, mesh — маршрутизирует трафик. Внутрисервисные вызовы в Kubernetes идут по DNS-имени сервиса: http://orders:8080 — тот же port binding, поднятый на уровень кластера.
VIII. Concurrency — масштабирование через процессы
Суть. Модель масштабирования — Unix-модель процессов. Приложение объявляет типы процессов с разными ролями (HTTP-обработчики web, фоновые обработчики worker, планировщик clock), и ёмкость наращивается запуском дополнительных процессов каждого типа — web×4, worker×10. Потоки и event loop внутри процесса — внутренняя реализация, а не инструмент эксплуатации.
Зачем. Процесс — универсальная, понимаемая операционной системой и платформой единица масштабирования: его можно добавить и убрать, ограничить по памяти, изолировать, перезапустить. Распределение нагрузки по типам процессов (больше воркеров, меньше вебов) позволяет масштабировать то, что узко, — вместо вертикального наращивания одной машины.
Типичное нарушение. Один гигантский процесс, в котором «всё»: HTTP, фоновые задачи, cron. Масштабировать его можно только целиком (дорогая машина на всё), падение забирает сразу все функции, а тихий фоновый цикл способен уронить обработку запросов.
В микросервисах и контейнерах. Тип процесса = тип контейнера: Deployment’ы web и worker из одного образа масштабируются независимо, Horizontal Pod Autoscaler добавляет реплики по метрикам нагрузки. Разделение web/worker через очередь — стандартная идиома микросервисов; горизонтальное масштабирование как таковое — тема отдельной готовящейся статьи о масштабируемости.
IX. Disposability — быстрый запуск и graceful shutdown
Суть. Процессы одноразовы: их можно запустить за секунды и убить в любой момент. При получении SIGTERM процесс обязан завершить текущую работу (договорить HTTP-запрос, доразобрать сообщение из очереди), освободить соединения и выйти — а не игнорировать сигнал и не бросать полуобработанные задачи.
Зачем. Быстрый старт делает дешёвыми эластичное масштабирование, деплой (стоп-старт вместо долгого обновления) и восстановление после падения. Корректная остановка нужна деплою без разрывов соединений и обрыва транзакций: платформа, которая убивает процессы регулярно, рассчитывает на их согласие умереть.
Типичное нарушение. «Прогрев» на 20 минут (лениво строящиеся кэши), из-за которого автоскейлинг бесполезен: процесс не успевает начать работать. Или обработчик, игнорирующий SIGTERM: оркестратор терпеливо ждёт timeout, затем убивает процесс с потерей обрабатываемого запроса.
В микросервисах и контейнерах. Kubernetes построен на одноразовости: плавные обновления (rolling updates) — это скоординированное «убей старые реплики, подними новые», где liveness- и readiness-пробы определяют момент переключения трафика, а terminationGracePeriodSeconds — окно на graceful shutdown. Отказоустойчивость как свойство системы (тема готовящейся статьи о high availability) опирается на этот фактор напрямую: неубиваемые процессы не бывают частью отказоустойчивой системы.
// IX: graceful shutdown — по SIGTERM заканчиваем текущие запросы и выходим
const server = app.listen(config.port);
process.on('SIGTERM', () => {
server.close(async () => {
await closeDbPool();
process.exit(0);
});
});
X. Dev/prod parity — одинаковость окружений
Суть. Разрыв между разработкой, staging и production минимизируется по трём осям: время (идея до продакшена — часы/минуты, а не недели), люди (деплоят те, кто пишет, а не отдельная группа), инструменты (окружения максимально совпадают — вплоть до операционной системы).
Зачем. Каждый разрыв — класс багов, который обнаруживается только в production: «у нас локально другая база», «на проде другой Linux». Сбитые окружения — источник самых дорогих инцидентов именно потому, что не воспроизводятся у разработчика.
Типичное нарушение. Dev на SQLite, prod на Postgres (разный SQL-диалект и разное поведение транзакций); локально macOS, на проде Linux (другая чувствительность к регистру имён файлов и другой fs). Показательные примеры — «light-версия» данных в dev, на которой не проявляются ни пагинация, ни деградация запросов.
В микросервисах и контейнерах. Контейнеры — главный выравниватель инструментов: разработчик и prod запускают один образ из одного реестра; различия сводятся к конфигурации (факторы III и V). Docker Compose и локальные кластеры (kind, minikube) приближают даже инфраструктурный контур. Полное следование фактору для данных острее: производственные объёмы и управляемые СУБД в dev дороги, поэтому parity стремятся достичь по стеку, а не по копии данных.
XI. Logs — логи как потоки событий
Суть. Приложение не управляет своими логами: не решает, куда писать файлы, как их ротировать и куда отправлять. Оно пишет события в stdout/stderr — неструктурированно или, лучше, машиночитаемо (JSON), — а сбор, буферизация, агрегация и хранение — задача платформы эксплуатации.
Зачем. Лог-файлы на локальном диске процесса разбухают (диск кончается), ротируются криво (теряются при перезапуске) и не читаются никем, кроме случайного SSH-гостя. Поток в stdout отвязывает приложение от судьбы его хоста: событие подхватывается централизованным конвейером — Loki, ELK — и доступно с retention’ом и поиском.
Типичное нарушение. log4j-конфиг с собственными файлами и ротацией внутри приложения: файлы расползаются по томам, в Kubernetes исчезают вместе с подом, а запрос «покажи логи заказа по orderId за вчера» не решается в принципе. Сюда же — логирование в таблицу БД: двойная нагрузка и рекурсия при падении самой БД.
В микросервисах и контейнерах. docker logs и kubectl logs читают именно stdout контейнера — контракт фактора XI встроен в инструментарий. Далее поток обычно уходит в node-agent’а и централизованное хранилище; метрики, трейсинг и логи вместе образуют observability — предмет отдельной готовящейся статьи. Для событийно-ориентированной архитектуры фактор имеет и концептуальное продолжение: логи — тоже события, и обращение с ними как с потоком унифицирует диагностику распределённой системы.
// XI: логи — поток машиночитаемых событий в stdout
const log = (level: string, message: string, extra: Record<string, unknown> = {}) => {
process.stdout.write(
JSON.stringify({ ts: new Date().toISOString(), level, message, ...extra }) + '\n',
);
};
log('info', 'order_created', { orderId: 10427, total: 15990 });
XII. Admin processes — административные задачи как разовые процессы
Суть. Разовые задачи — миграции схемы, импорт данных, ручные исправления, REPL-консоль — запускаются как отдельные недолговечные процессы из того же релиза (тот же артефакт и та же конфигурация), что и штатные процессы приложения, — а не как «особый режим» на сервере или скрипт с ноутбука разработчика.
Зачем. Миграция, запущенная из другой версии кода, чем приложение, — источник трудноуловимых рассинхронизаций схемы и кода. Единство релиза гарантирует, что админ-процесс видит ту же схему данных, те же библиотеки и ту же конфигурацию, что и production-процессы; шансы «у меня в скрипте работало» исчезают вместе с самим феноменом ноутбука против продакшена.
Типичное нарушение. Разработчик запускает миграцию локальным скриптом против production-БД по VPN: локальная версия кода расходится с продовской, миграция применяется дважды или не полностью, а вход в критический момент — единственный и непроверяемый.
В микросервисах и контейнерах. В Kubernetes разовые задачи — Jobs и CronJob’ы из того же образа и с тем же ConfigMap/Secret; миграции часто выполняются как init-контейнер или helm-хук, гарантирующий «образ деплоя = образ миграции». Для микросервисов фактор особенно важен: у каждого сервиса свои миграции, и ручное исполнение их в кластере из десятков сервисов не масштабируется.
Twelve-Factor, контейнеры и cloud-native
Docker и Kubernetes часто описывают как «двенадцать факторов, ставшие инфраструктурой» — и это почти буквально так. Контейнеры материализовали ключевые факторы в механизмах платформы:
| Фактор | Воплощение в Docker/Kubernetes |
|---|---|
| II. Dependencies | Образ: приложение + зависимости + ОС-слой как единый артефакт; Dockerfile — декларация |
| III. Config | docker run -e, ConfigMap и Secret → env-переменные контейнера |
| V. Build, release, run | Multi-stage build → тегированный неизменяемый образ (release) → Deployment (run); rollback = kubectl rollout undo |
| VI. Processes | Pod — одноразовая stateless-единица; состояние — в backing services |
| VII. Port binding | Контейнер слушает порт; Service/Ingress маршрутизируют; DNS-имя вместо IP |
| VIII. Concurrency | Deployment’ы web/worker из одного образа; Horizontal Pod Autoscaler добавляет реплики |
| IX. Disposability | Liveness/readiness-пробы, terminationGracePeriodSeconds, rolling updates |
| XI. Logs | stdout контейнера → драйвер логирования → централизованный конвейер |
| XII. Admin processes | Jobs, CronJob, init-контейнеры, helm-хуки из того же образа |
Обратное тоже верно: приложение, не соблюдающее факторы, плохо живёт даже в идеальном кластере. Sticky sessions ломают балансировку Service’ов; логи в файлы невидимы для kubectl logs; зависимость от «прогретого» состояния процесса превращает каждый rolling update в лотерею; конфигурация, зашитая в образ, множит число образов на число окружений. Поэтому Twelve-Factor — стандартный чек-лист при миграции в Kubernetes: сначала приводишь приложение к факторам, потом переносишь.
Как факторы собираются в один Dockerfile — от build-стадии (фрагмент приведён в факторе II) до run-стадии:
# run-стадия: один процесс, без исходников и build-инструментов (II, V)
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
# VI–VII: один процесс на контейнер, слушающий порт из окружения
# IX: старт за секунды; USER — минимальные привилегии
USER node
EXPOSE 8080
CMD ["node", "dist/server.js"]
Каждая строка здесь — прямое следствие фактора: неизменяемый образ без исходников (V), конфигурация только через env (III), один не-root-процесс, слушающий порт (VI–VII), мгновенный старт (IX). Хорошая проверка «двенадцатифакторности» образа — он одинаков для всех окружений и не содержит секретов: секрет появляется только в момент запуска — в переменных окружения конкретного деплоя.
Наконец, методология легла в фундамент cloud-native-подхода — набора практик, в котором приложения строятся специально для облачной среды: контейнеры, декларативные API, микросервисы, автоматизация поставки. CNCF-экосистема добавила к этому слои, которых в 2011 году не существовало: оркестрацию, service mesh, иммутабельную инфраструктуру, observability-конвейеры. Twelve-Factor не покрывает и не претендует на покрытие этой территории — он фиксирует минимальный контракт приложения с платформой, достаточный, чтобы платформа могла им управлять. Внутреннее же устройство приложения — слои, границы ответственности, зависимости внутри кода — остаётся предметом слоистой архитектуры и Clean Architecture; это ортогональные уровни требований: Clean Architecture отделяет бизнес-логику от инфраструктуры внутри кода, Twelve-Factor — приложение от платформы снаружи.
Критика и эволюция
Что методология не покрывает
- Только приложение, не данные. Единица внимания Twelve-Factor — процесс приложения. Базы данных, очереди и прочие stateful-сервисы упомянуты лишь как подключаемые ресурсы; как их эксплуатировать (репликация, бэкапы, консистентность) — вне поля зрения. Ответ экосистемы пришёл позже: StatefulSets, операторы и managed-сервисы сместили эту заботу с приложения на платформу.
- Безопасность отсутствует. Ни один фактор не касается аутентификации, авторизации, ротации секретов, изоляции. В 2011 году это считалось заботой платформы; в современном кластере из десятков микросервисов — нет. В сообществе безопасность давно называют «13-м фактором», а Beyond Twelve-Factor формализовал это дополнение.
- Предположение PaaS. Факторы писались под модель Heroku: есть платформа, которая берёт на себя сборку, маршрутизацию, сбор логов и запуск. В мире голого Kubernetes всё это строит сама команда — методология остаётся верной, но «платформа» из данного превращается в артефакт собственной сборки.
Возраст тезисов
Часть факторов успела стать общим местом: разделение build/release/run — определение CI/CD; dev/prod parity радикально упростился контейнерами; одноразовость процессов — операционная норма оркестраторов. Это не делает методологию устаревшей: её цитируют до сих пор не потому, что тезисы спорны, а потому, что нарушения (пароль в репозитории, sticky sessions, логи в файлы) встречаются и сегодня — просто теперь они обязаны быть осознанными, а не невежественными.
Beyond Twelve-Factor
В 2016 году Кевин Хоффман (Kevin Hoffman, Pivotal) выпустил книгу Beyond the Twelve-Factor App — переосмысление методологии для эпохи микросервисов и облачных платформ. Хоффман сохранил ядро (stateless-процессы, конфигурация в окружении, логи потоками) и добавил три фактора, которых в 2011 году не хватало:
- API first — сервис проектируется от контракта API, который является публичным обязательством перед потребителями;
- Telemetry — сервис сам экспортирует метрики, логи и трейсинг, пригодные для наблюдения платформой;
- Authentication/authorization — доверие внутри кластера нулевое: каждый сервис аутентифицирует потребителей и авторизует действия.
Дополнения Хоффмана фактически описали service mesh, observability-конвейеры и zero-trust — направления, оформившиеся в мейнстрим во второй половине 2010-х. Историческая линия получилась непрерывной: SOA → Twelve-Factor → Docker/Kubernetes → cloud-native — методология стоит в её середине как первый сформулированный контракт приложения с облаком.
Связанные статьи
- Введение в архитектуру — место контрактных требований к приложению среди задач архитектуры ПО.
- Микросервисы — каждый сервис как twelve-factor-приложение: своя кодовая база, свои деплои, stateless-процессы.
- Монолитная архитектура — контраст по фактору I: один деплой против многих; методология применима к обоим стилям.
- Сервис-ориентированная архитектура (SOA) — историческая линия сервисных подходов, в которую встраивается Twelve-Factor.
- Событийно-ориентированная архитектура (EDA) — очереди и обмен событиями между процессами; логи как частный случай потока событий.
- Clean Architecture — разделение ответственности внутри кода как ортогональный уровень к разделению «приложение/платформа».
- Слоистая архитектура (Layered / N-tier) — внутренняя структура приложения при внешнем контракте Twelve-Factor.
- Эволюционная архитектура — факторы как защитимые инварианты: свойства, которые architecture fitness-функции проверяют автоматически.
- Темы смежных статей в подготовке: observability (логи, метрики, трейсинг), масштабируемость и high availability, интеграционные паттерны и очереди сообщений, модель C4 для документирования архитектуры.