Что такое 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) | Контейнер с явными зависимостями |