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. Сводная таблица факторов:

ФакторСутьТипичное нарушение
ICodebaseОдна кодовая база в VCS, много деплоевНесколько приложений в одном репозитории; одно приложение в нескольких репозиториях
IIDependenciesЗависимости объявлены явно и изолированыОпора на «и так установленные» системные пакеты
IIIConfigКонфигурация — в окружении, не в кодеПароль от БД в репозитории; ветвление поведения по имени сервера
IVBacking servicesБД, очереди, хранилища — подключаемые ресурсы по URLЖёсткая привязка кода к конкретному инстансу или вендору
VBuild, release, runСтрогое разделение стадий сборки, релиза и выполненияПравки кода руками на работающем сервере
VIProcessesПроцессы stateless и разделяемыеСессии в памяти процесса; sticky sessions в балансировщике
VIIPort bindingСервис экспортируется привязкой к портуПриложение не запускается без внешнего веб-сервера
VIIIConcurrencyМасштабирование через процессы, а не наращивание одного процессаОдин гигантский процесс, который масштабируется только как целое
IXDisposabilityБыстрый запуск и graceful shutdownДолгая «прогревка»; игнорирование SIGTERM
XDev/prod parityМинимум различий между окружениямиDev на SQLite и macOS, prod на Postgres и Linux
XILogsЛоги — потоки событий в stdout/stderrПриложение пишет и ротирует собственные файлы логов
XIIAdmin 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. Configdocker run -e, ConfigMap и Secret → env-переменные контейнера
V. Build, release, runMulti-stage build → тегированный неизменяемый образ (release) → Deployment (run); rollback = kubectl rollout undo
VI. ProcessesPod — одноразовая stateless-единица; состояние — в backing services
VII. Port bindingКонтейнер слушает порт; Service/Ingress маршрутизируют; DNS-имя вместо IP
VIII. ConcurrencyDeployment’ы web/worker из одного образа; Horizontal Pod Autoscaler добавляет реплики
IX. DisposabilityLiveness/readiness-пробы, terminationGracePeriodSeconds, rolling updates
XI. Logsstdout контейнера → драйвер логирования → централизованный конвейер
XII. Admin processesJobs, 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 для документирования архитектуры.