12-factor app на Kubernetes
12-factor app — методология построения SaaS-приложений, опубликованная Heroku в 2011 году. Спустя 15 лет она остаётся актуальной, потому что описывает операционные свойства приложения, а не конкретные технологии. И самое интересное: Kubernetes как платформа спроектирован так, что каждый из 12 факторов имеет свой first-class primitive — ConfigMap для config, Service для backing services, Deployment для stateless processes, Job для admin tasks.
Если приложение уже написано в стиле 12-factor, переезд в K8s проходит почти без боли. Если нет — обычно начинаются мучения: locally-written files, hardcoded configs, processes которые нельзя перезапустить, missing health endpoints. Понять 12 факторов в K8s-контексте — значит знать, как должно быть устроено production-приложение, и почему именно так.
WORKDIR, ENV, ARG: настройка контейнера через переменные
Контекст: откуда взялись 12 факторов
Heroku — PaaS, который в 2010-х хостил десятки тысяч приложений. Команда обобщила, какие приложения работают хорошо в облаке, какие — плохо, и сформулировала 12 факторов. Это не про конкретный язык или фреймворк — это про операционные характеристики:
- Можно ли запустить приложение многократно (горизонтальное масштабирование)?
- Можно ли убить процесс и поднять заново без потери данных?
- Можно ли версионировать конфиг отдельно от кода?
Эти вопросы перекликаются с тем, что Kubernetes ожидает от Pod: stateless, idempotent, configurable via env, observable via stdout. К 2015 году, когда K8s стал GA, 12-factor превратился в де-факто стандарт cloud-native разработки.
12-factor — это guidance, не закон. Реальные приложения часто отклоняются (например, ML-модели держат большие веса в памяти и не вписываются в “fast startup”). Цель — понять trade-offs, а не слепо следовать.
I. Codebase — один codebase, много deploys
One codebase tracked in revision control, many deploys
12-factor: одно приложение = один Git-репозиторий. Несколько окружений (dev, staging, prod) — это deploys одного codebase, не разные codebase.
K8s-implementation:
my-app/
├── src/ # код
├── Dockerfile # build instructions
├── manifests/ # k8s manifests
│ ├── base/ # общая база
│ └── overlays/
│ ├── dev/ # dev-overlay
│ ├── staging/
│ └── prod/ # prod-overlay
└── .github/workflows/ # CI/CD
CI собирает образ → kubectl apply -k manifests/overlays/prod (или helm upgrade). Один codebase, три deploys через Kustomize overlays.
Антипаттерн: отдельный fork репозитория “myapp-prod” с patched configs. Это нарушает фактор и создаёт divergence.
II. Dependencies — явная декларация
Explicitly declare and isolate dependencies
12-factor: никаких implicit system dependencies. Все зависимости описаны в manifest (requirements.txt, package.json, go.mod) и locked (*-lock.json, Pipfile.lock).
K8s-implementation: Dockerfile — это явный manifest зависимостей.
FROM python:3.13-slim # base image PINNED по версии
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
FROM python:latest или FROM python:3 — антипаттерн. Сегодня latest это 3.13, завтра 3.14 — build не воспроизводим. Pin до minor: python:3.13-slim. Ещё лучше — digest: python@sha256:abc123....
Container image — это immutable snapshot всех зависимостей. Один образ в registry = один deterministic deployable artifact.
III. Config — в окружении
Store config in the environment
12-factor: всё что варьируется между deploys (DB hostnames, credentials, feature flags) — в env-переменных. Не в коде, не в config-файлах, коммитнутых в Git.
K8s-implementation: ConfigMap (non-secret) + Secret (secret).
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
data:
DATABASE_HOST: "postgres.prod.svc.cluster.local"
LOG_LEVEL: "info"
FEATURE_NEW_CHECKOUT: "true"
---
apiVersion: v1
kind: Secret
metadata:
name: web-secrets
type: Opaque
data:
DATABASE_PASSWORD: cGFzc3dvcmQxMjM= # base64
---
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: web
envFrom:
- configMapRef:
name: web-config
- secretRef:
name: web-secrets
Один и тот же image, разные ConfigMap/Secret в dev/staging/prod. Никакого пересборки образа при изменении конфига.
IV. Backing services — attached resources
Treat backing services as attached resources
12-factor: база данных, очередь, кеш — это attached resources, к которым приложение подключается по URL. Локальный Postgres или managed RDS — для приложения всё равно.
K8s-implementation: внутри кластера — Service, снаружи — Service тип ExternalName или прямой URL в Secret.
# In-cluster Postgres (управляется PostgreSQL Operator)
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
ports:
- port: 5432
selector:
app: postgres
---
# External managed Postgres (AWS RDS)
apiVersion: v1
kind: Service
metadata:
name: postgres-rds
spec:
type: ExternalName
externalName: prod-db.abc123.eu-west-1.rds.amazonaws.com
В приложении — одинаково: DATABASE_HOST=postgres (или postgres-rds). Переключение in-cluster ↔ managed — изменение Service, не изменение кода.
V. Build, release, run — строгое разделение
Strictly separate build and run stages
12-factor: три фазы — build (компиляция, образ), release (build + config), run (запуск). Каждая иммутабельна.
K8s-implementation:
- Build — CI собирает image, push в registry (
docker push myregistry/web:v1.2.3). - Release —
kubectl apply -f deployment.yamlсimage: myregistry/web:v1.2.3и привязкой к ConfigMap/Secret. Это создаётReplicaSet— release artifact. - Run — kubelet запускает Pod из ReplicaSet.
# Build (CI)
docker build -t myregistry/web:v1.2.3 .
docker push myregistry/web:v1.2.3
# Release (CI/CD or kubectl)
kubectl set image deploy/web web=myregistry/web:v1.2.3
# или helm upgrade web . --set image.tag=v1.2.3
# Run — kubelet делает автоматически
Каждое изменение image → новый release (новый ReplicaSet). Старые release остаются в etcd (revisionHistoryLimit=10 по умолчанию) — можно rollback.
Антипаттерн: kubectl exec для изменения кода inside Pod. Это нарушает immutability run-фазы. Если нужен hot-fix — пересобирай образ и redeploy. Pod должен быть disposable.
VI. Processes — stateless и share-nothing
Execute the app as one or more stateless processes
12-factor: процесс не хранит state локально. Любые persistent данные — в backing service (DB, S3). Локальная файловая система — scratch space только.
K8s-implementation: Deployment (stateless) vs StatefulSet (stateful, когда без него никак).
apiVersion: apps/v1
kind: Deployment # stateless: любая replica взаимозаменяема
metadata:
name: web
spec:
replicas: 5
template:
spec:
containers:
- name: web
volumeMounts:
- name: cache
mountPath: /tmp/cache
volumes:
- name: cache
emptyDir: {} # scratch, ephemeral
EmptyDir — это scratch space, который исчезает при удалении Pod. Никакого долговременного state в Pod. Сессии — в Redis. Загруженные файлы — в S3. Логи — в stdout.
Когда state неизбежен (БД, message broker, файловое хранилище):
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
StatefulSet даёт stable network identity (postgres-0, postgres-1) и persistent volume per replica. Но даже здесь — state в PV, не в filesystem контейнера.
VII. Port binding — экспорт через порт
Export services via port binding
12-factor: приложение само биндит порт (listen 0.0.0.0:8080), не полагается на injection через runtime. HTTP-сервер встроен в приложение (не Apache + mod_php).
K8s-implementation:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: web
ports:
- containerPort: 8080 # информативное, но не открывает порт
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80 # Service port
targetPort: 8080 # Pod port
Container listens on 8080, Service exposes на 80. Никакого Apache/Nginx в роли application server — приложение само http-сервер.
VIII. Concurrency — масштабирование через процессы
Scale out via the process model
12-factor: чтобы handle больше нагрузки — добавь больше процессов, не одного процесса с большим threadpool.
K8s-implementation: replicas в Deployment + HorizontalPodAutoscaler для автомасштабирования.
# Manual scale
kubectl scale deployment web --replicas=20
# Auto scale
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
HPA controller увеличивает replicas, когда avg CPU > 70%. Уменьшает, когда падает. Это horizontal scaling — фундаментальная концепция cloud-native.
IX. Disposability — fast startup, graceful shutdown
Maximize robustness with fast startup and graceful shutdown
12-factor: процесс должен стартовать быстро (секунды, не минуты) и graceful shutdown на SIGTERM.
K8s-implementation: readinessProbe + preStop + terminationGracePeriodSeconds.
spec:
containers:
- name: web
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"] # дать kube-proxy обновить endpoint
terminationGracePeriodSeconds: 30
Fast startup → быстрое масштабирование, быстрое восстановление при failure. Graceful shutdown → no dropped requests при rollout. Этой теме посвящён следующий урок.
X. Dev/prod parity — минимальные расхождения
Keep development, staging, and production as similar as possible
12-factor: dev и prod должны быть максимально похожи — same OS, same backing services, same dependencies. Никаких “у меня на ноуте работает”.
K8s-implementation: same image в dev/staging/prod, разница только в ConfigMap/Secret через Kustomize overlays.
manifests/
├── base/
│ ├── deployment.yaml # image: web (без tag), config envFrom
│ └── kustomization.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml # image tag: dev, replicas: 1
│ └── configmap-patch.yaml # LOG_LEVEL: debug
├── staging/
└── prod/
├── kustomization.yaml # image tag: v1.2.3, replicas: 10
└── configmap-patch.yaml # LOG_LEVEL: info
Локально — kind или minikube с базовым stack. Не SQLite в dev и Postgres в prod — а Postgres везде (через docker-compose или kind).
Kustomize — встроенная в kubectl (kubectl apply -k) система overlays. Альтернатива — Helm с values per environment. Обе подходят, выбор зависит от команды.
XI. Logs — event streams
Treat logs as event streams
12-factor: приложение пишет в stdout/stderr. Не в файлы, не в syslog, не в специфический logger. Платформа сама собирает и роутит.
K8s-implementation:
# Хорошо
print(json.dumps({"level": "info", "msg": "request handled", "user_id": 42}))
# Плохо
logging.FileHandler("/var/log/app.log")
kubelet собирает stdout/stderr → файл /var/log/pods/.../container/X.log на ноде → log aggregator (Fluent Bit / Vector DaemonSet) → centralized storage (Loki / Elasticsearch / CloudWatch).
kubectl logs web-7d4f-abc12
kubectl logs web-7d4f-abc12 --previous # последний crashed container
kubectl logs -l app=web --tail=100 # все Pods с label
Structured logging (JSON) — обязательное в production. Тексты типа [INFO] Request handled for user 42 нельзя нормально парсить и фильтровать в Loki/Elasticsearch. JSON-формат {"level":"info","msg":"...","user_id":42} — мгновенный поиск по полю.
XII. Admin processes — одноразовые
Run admin/management tasks as one-off processes
12-factor: миграции БД, бэкапы, REPL — это одноразовые процессы, запускаемые той же codebase, но отдельно от long-running.
K8s-implementation: Job (одноразово) или CronJob (по расписанию). Для интерактива — kubectl exec.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate-v1-2-3
spec:
template:
spec:
containers:
- name: migrate
image: myregistry/web:v1.2.3 # тот же image!
command: ["python", "manage.py", "migrate"]
restartPolicy: OnFailure
Тот же образ что и web — миграции не отдельный codebase, а часть app. Запускается из CI или вручную перед deployment.
# Ad-hoc REPL
kubectl exec -it web-7d4f-abc12 -- python manage.py shell
Почему K8s — perfect fit для 12-factor
| Фактор | K8s primitive |
|---|---|
| Codebase | Git + manifests + CI |
| Dependencies | Dockerfile + container image |
| Config | ConfigMap + Secret |
| Backing services | Service / ExternalName |
| Build/release/run | CI build → kubectl apply → kubelet runs |
| Processes | Deployment (stateless), StatefulSet (stateful) |
| Port binding | containerPort + Service |
| Concurrency | replicas + HPA |
| Disposability | readinessProbe + preStop + terminationGracePeriodSeconds |
| Dev/prod parity | same image, Kustomize overlays |
| Logs | stdout → log aggregator |
| Admin processes | Job / CronJob / kubectl exec |
K8s предоставляет first-class primitive для каждого фактора. Это не случайность — Borg (предшественник K8s) и Heroku решали те же задачи “запустить много приложений на shared infrastructure”.
Антипаттерны: что ломается, когда нарушаешь 12-factor
- App пишет в локальные файлы для state — при Pod restart данные теряются. Fix: PV или внешний storage.
- Hardcoded DB hostname в коде — нельзя переключить dev/prod. Fix: env через ConfigMap.
docker execдля изменения config — мутации runtime, не воспроизводимы. Fix: ConfigMap + rollout.- Long startup (>30s) — autoscaling реагирует с задержкой, slow rollout. Fix: profile startup, lazy load.
- Не обрабатывает SIGTERM — connections dropping при rollout. Fix: graceful shutdown handler (см. следующий урок).
- Logs в файл /var/log/app.log — нужен sidecar для tail. Fix: print в stdout.
- Different images для dev/prod — bugs появляются только в prod. Fix: один image, разный config.
Killer-моменты
- K8s — perfect fit для 12-factor, но не насаждает методологию. Можно создать Deployment, который нарушает все 12 факторов — K8s не помешает. Но операционно будет очень больно.
- Heroku 2011 → K8s 2015: 12-factor предсказал cloud-native ещё до K8s. Все principles остались валидны.
- State — главная боль в K8s. Stateless apps масштабируются elastically. Stateful — нужен StatefulSet + PV + careful design. 90% apps должны быть stateless.
- One image, many configs — суть фактора Dev/prod parity. Меняешь только ConfigMap, не пересобираешь image. Это позволяет immutable infrastructure.
- Structured logging (JSON) — не nice-to-have в K8s, а mandatory для эффективного debugging в centralized log storage.
- Process model вместо threads/async: K8s масштабирует процессы (Pods), не потоки. Поэтому apps с heavy threadpool менее cloud-friendly чем те, что scale-out горизонтально.