MidТеория4 min

12-Factor App

Методология 12-Factor App: все 12 факторов с примерами, Beyond 12-Factor (15-Factor) и применение в Cloud Native

Что такое 12-Factor App

12-Factor App — методология создания SaaS-приложений, опубликованная инженерами Heroku в 2011 году. Описывает 12 принципов для построения приложений, которые легко деплоить, масштабировать и поддерживать в облаке.

Почему важно для System Design: 12-Factor — фундамент Cloud Native архитектуры. На собеседованиях ожидают, что вы понимаете эти принципы и можете объяснить, почему каждый из них критичен.

12 Факторов

I. Codebase — Одна кодовая база, много деплоев

Одно приложение = один репозиторий. Разные окружения (dev, staging, production) — это разные деплои одной кодовой базы.

Репозиторий ──┬── deploy → dev.example.com
              ├── deploy → staging.example.com
              └── deploy → example.com (prod)

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

II. Dependencies — Явная декларация зависимостей

Все зависимости должны быть явно объявлены в манифесте. Приложение никогда не полагается на системные пакеты.

// package.json — explicit dependency declaration
{
  "dependencies": {
    "express": "^4.18.0",
    "pg": "^8.11.0"
  }
}
// go.mod — explicit Go dependencies
module github.com/example/service

go 1.25

require (
    github.com/jackc/pgx/v5 v5.7.0
    github.com/go-chi/chi/v5 v5.1.0
)

Нарушение: apt install curl внутри кода, зависимость от глобально установленного ImageMagick.

III. Config — Конфигурация через переменные окружения

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

# docker-compose.yml — config via environment
services:
  api:
    image: myapp:latest
    environment:
      DATABASE_URL: "postgres://user:pass@db:5432/myapp"
      REDIS_URL: "redis://cache:6379"
      LOG_LEVEL: "info"
      FEATURE_NEW_UI: "true"
// Reading config from environment
type Config struct {
    DatabaseURL string
    RedisURL    string
    LogLevel    string
    Port        int
}

func LoadConfig() (*Config, error) {
    port, err := strconv.Atoi(os.Getenv("PORT"))
    if err != nil {
        port = 8080 // default
    }
    return &Config{
        DatabaseURL: os.Getenv("DATABASE_URL"),
        RedisURL:    os.Getenv("REDIS_URL"),
        LogLevel:    os.Getenv("LOG_LEVEL"),
        Port:        port,
    }, nil
}

Лакмусовый тест: может ли кодовая база быть открыта в open source прямо сейчас без компрометации учётных данных?

IV. Backing Services — Сторонние сервисы как подключаемые ресурсы

База данных, очередь, SMTP-сервер, кэш — это подключаемые ресурсы. Замена локального PostgreSQL на Amazon RDS не должна требовать изменения кода.

Приложение
    ├── DATABASE_URL=postgres://local:5432/db     (dev)
    ├── DATABASE_URL=postgres://rds.aws:5432/db   (prod)
    ├── SMTP_URL=smtp://mailhog:1025              (dev)
    └── SMTP_URL=smtp://sendgrid.net:587          (prod)

V. Build, Release, Run — Строгое разделение стадий

Build (сборка)     → Код + зависимости → артефакт (Docker image)
Release (релиз)    → Артефакт + конфигурация → релиз с версией
Run (запуск)       → Запуск релиза в окружении
# CI/CD pipeline — strict separation
stages:
  build:
    # Compile code, run tests, create Docker image
    - docker build -t myapp:$CI_COMMIT_SHA .
    - docker push registry/myapp:$CI_COMMIT_SHA

  release:
    # Tag with version, attach config
    - docker tag myapp:$CI_COMMIT_SHA myapp:v1.2.3

  run:
    # Deploy the versioned release
    - kubectl set image deployment/myapp app=myapp:v1.2.3

Критично: стадия Run не должна собирать код. Каждый релиз имеет уникальный ID и может быть откачен.

VI. Processes — Приложение как stateless-процессы

Процессы stateless и share-nothing. Любое состояние хранится в backing service (Redis, PostgreSQL).

// Stateless handler — no in-memory state between requests
func (h *Handler) HandleOrder(w http.ResponseWriter, r *http.Request) {
    // Read from backing service, not from memory
    session, err := h.sessionStore.Get(r.Context(), sessionID)
    if err != nil {
        http.Error(w, "session not found", http.StatusUnauthorized)
        return
    }

    // Process and save back to backing service
    order := processOrder(r)
    if err := h.db.SaveOrder(r.Context(), order); err != nil {
        http.Error(w, "failed to save", http.StatusInternalServerError)
        return
    }
}

Нарушение: sticky sessions, хранение файлов сессий на локальном диске, in-memory cache без синхронизации.

VII. Port Binding — Экспорт сервисов через привязку портов

Приложение самодостаточно — оно экспортирует HTTP как сервис, привязываясь к порту. Не зависит от внешнего веб-сервера (Apache, Nginx).

// Self-contained HTTP server
func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    srv := &http.Server{
        Addr:    ":" + port,
        Handler: setupRoutes(),
    }
    log.Fatal(srv.ListenAndServe())
}

VIII. Concurrency — Масштабирование через процессы

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

# Kubernetes HPA — horizontal scaling via processes
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

IX. Disposability — Быстрый запуск и graceful shutdown

Процессы запускаются быстро и завершаются корректно. При получении SIGTERM — завершают текущие запросы и освобождают ресурсы.

// Graceful shutdown pattern
func main() {
    srv := &http.Server{Addr: ":8080", Handler: router}

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("HTTP server error: %v", err)
        }
    }()

    // Wait for termination signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("shutdown error: %v", err)
    }
    log.Println("server stopped gracefully")
}

X. Dev/Prod Parity — Минимизация различий между окружениями

Dev, staging и production должны быть максимально идентичны.

Разрыв Традиционно 12-Factor
Временной Недели между деплоями Часы
Кадровый Разные люди пишут и деплоят Одна команда
Инструментальный SQLite в dev, PostgreSQL в prod PostgreSQL везде
# docker-compose.yml — same backing services as production
services:
  app:
    build: .
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  postgres:
    image: postgres:18-alpine  # Same version as production
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]

  redis:
    image: redis:7-alpine      # Same version as production

XI. Logs — Логи как поток событий

Приложение пишет логи в stdout/stderr. Сбор и маршрутизация — ответственность платформы.

// Structured logging to stdout
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))

logger.Info("order processed",
    slog.String("order_id", order.ID),
    slog.Int("items", len(order.Items)),
    slog.Duration("duration", time.Since(start)),
)
// Output: {"time":"...","level":"INFO","msg":"order processed","order_id":"abc","items":3,"duration":"12ms"}

Нарушение: запись в файл /var/log/app.log, ротация логов внутри приложения.

XII. Admin Processes — Административные задачи как разовые процессы

Миграции, консольные скрипты, одноразовые задачи запускаются в том же окружении, что и приложение.

# Run migration as a one-off process in the same environment
kubectl exec -it deployment/api -- ./app migrate up

# Run one-off data fix
kubectl run --rm -it fix-data \
  --image=myapp:v1.2.3 \
  --env="DATABASE_URL=$DATABASE_URL" \
  -- ./app fix-orphaned-orders

Beyond 12-Factor: 15-Factor App

Современные Cloud Native системы добавляют ещё 3 фактора:

XIII. API First

Проектирование начинается с API-контракта. OpenAPI/gRPC спецификация создаётся до реализации.

# OpenAPI contract-first design
openapi: "3.1.0"
info:
  title: Orders API
  version: "1.0.0"
paths:
  /orders:
    post:
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order created

XIV. Telemetry

Три столпа наблюдаемости встроены в приложение: метрики, логи, трейсы.

Столп Инструмент Что показывает
Metrics Prometheus Числовые показатели (RPS, latency)
Logs ELK/Loki Текстовые события
Traces Jaeger/Tempo Путь запроса через сервисы

XV. Authentication & Authorization

Безопасность — first-class citizen, а не afterthought. Zero-trust модель по умолчанию.

Шпаргалка: 12 факторов

# Фактор Одной фразой
I Codebase Один репозиторий — много деплоев
II Dependencies Явная декларация, никаких системных пакетов
III Config Переменные окружения, не файлы
IV Backing Services БД, кэш, очередь — подключаемые ресурсы
V Build/Release/Run Строгое разделение стадий
VI Processes Stateless, share-nothing
VII Port Binding Самодостаточный HTTP-сервер
VIII Concurrency Масштабирование процессами
IX Disposability Быстрый старт, graceful shutdown
X Dev/Prod Parity Минимум отличий между окружениями
XI Logs Stdout, никаких файлов
XII Admin Processes Разовые задачи в том же окружении

Типичные нарушения на собеседованиях

Нарушение Фактор Правильный подход
Хардкод URL базы данных III (Config) Переменные окружения
Хранение сессий в памяти VI (Processes) Redis/Memcached
Запись логов в файл XI (Logs) stdout + log aggregator
SQLite в dev, PostgreSQL в prod X (Parity) PostgreSQL везде
Ручная миграция через SSH XII (Admin) Kubernetes Job / one-off process
Зависимость от ImageMagick на хосте II (Dependencies) Контейнер с явными зависимостями

Проверь себя

Какой фактор описывает паттерн graceful shutdown?

Какой фактор нарушается при хранении пользовательских сессий в оперативной памяти приложения?

Какой дополнительный фактор (Beyond 12-Factor) требует проектирования API-контракта до реализации?

Что означает принцип Dev/Prod Parity (фактор X)?

Согласно фактору III (Config), где должна храниться конфигурация?