Learning Platform
Глоссарий Troubleshooting
Урок 20.01 · 25 мин
Средний
12-factorcloud-nativeConfigMapSecretServicestatelessJobsKustomizestructured logging

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 разработки.

NOTE

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"]
WARNING

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, не изменение кода.

Backing services как attached resources
app PodПриложение читает DATABASE_HOST из env. Не знает, локальная это БД или managed cloud-resource — просто DNS-имя.
Service postgresKubernetes Service. Может быть ClusterIP (selector на in-cluster Pod) или ExternalName (CNAME на cloud-resource). Замена одного на другое — без изменения приложения.
seamless swap
in-clusterStatefulSet с Postgres, например через CloudNativePG operator. Selector в Service указывает на Postgres Pods.
managed cloudAWS RDS, GCP CloudSQL, Azure Database. Service.type=ExternalName делегирует DNS-resolve в облако. Поменяли externalName — приложение не заметило.

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).
  • Releasekubectl 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.

TIP

Антипаттерн: 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).

NOTE

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
TIP

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
CodebaseGit + manifests + CI
DependenciesDockerfile + container image
ConfigConfigMap + Secret
Backing servicesService / ExternalName
Build/release/runCI build → kubectl apply → kubelet runs
ProcessesDeployment (stateless), StatefulSet (stateful)
Port bindingcontainerPort + Service
Concurrencyreplicas + HPA
DisposabilityreadinessProbe + preStop + terminationGracePeriodSeconds
Dev/prod paritysame image, Kustomize overlays
Logsstdout → log aggregator
Admin processesJob / 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 горизонтально.

Проверка знанийKnowledge check
Приложение записывает uploaded user files в /var/lib/app/uploads/ внутри контейнера. Какие 12-factor принципы нарушены и как это починить в K8s?
ОтветAnswer
Нарушены два фактора: VI (Processes — stateless) и IX (Disposability). При удалении Pod (rollout, eviction, OOMKill) файлы исчезают. При replicas>1 один Pod не видит файлы другого. Fix варианты: (1) Object storage — S3/GCS/MinIO через S3-API в коде, файлы в bucket. Самый правильный cloud-native подход. (2) ReadWriteMany PersistentVolume (NFS, CephFS) — все Pods монтируют один том. Работает, но дороже и сложнее. (3) Если файлы должны быть private к одному Pod (cache) — StatefulSet с volumeClaimTemplates. Главное — не emptyDir и не hostPath, оба потеряют данные.
Проверка знанийKnowledge check
Почему 12-factor требует ОДИН и тот же image в dev/staging/prod, и как Kustomize/Helm помогают это реализовать?
ОтветAnswer
Фактор X (Dev/prod parity) — минимизация divergence. Если в dev image содержит debug-инструменты, а в prod — slim variant, баги (зависящие от ENV или библиотек) проявляются только в prod. Один image гарантирует одинаковое поведение run-time. Разница окружений только в КОНФИГЕ — DB hostname, log level, replicas, feature flags. Kustomize: один base/ с image:tag (без указания tag или с placeholder) + overlays/dev/, overlays/prod/ которые патчат ConfigMap, replicas, resources. Helm: один Chart, values-dev.yaml / values-prod.yaml. В обоих случаях image остаётся неизменным, меняется только runtime config. CI пушит один image в registry → apply разных overlays.
Проверка знанийKnowledge check
Команда пишет логи в файл /var/log/app.log внутри Pod-а и просит ops 'настроить копирование в Elasticsearch'. Почему это анти-12-factor и какое правильное решение?
ОтветAnswer
Нарушен фактор XI (Logs as event streams). Приложение НЕ должно знать, куда логи в итоге попадают — оно просто пишет в stdout/stderr. Платформа (K8s) решает остальное. Проблема файлового подхода: (1) Нужен sidecar для tail -F файла → stdout (лишний контейнер, ресурсы). (2) При Pod restart файл может быть потерян (если emptyDir). (3) Файлы растут — нужна rotation. (4) Несколько replicas → несколько одинаковых файлов, путаница. Правильное решение: print/log в stdout (Python: logging.StreamHandler() или print(), Go: log.Println, Node.js: console.log). kubelet собирает stdout в /var/log/pods/.../X.log на ноде. Fluent Bit / Vector DaemonSet tail эти файлы и отправляет в Loki/Elasticsearch с автоматическими лейблами (namespace, pod, container). Никакой sidecar не нужен.
Проверка знанийKnowledge check
Почему K8s называют 'perfect fit для 12-factor', и какие приложения НЕ ложатся на 12-factor (значит и не на K8s естественно)?
ОтветAnswer
K8s primitives буквально маппят 12 факторов: ConfigMap (III), Service (IV), Deployment (VI), HPA (VIII), readinessProbe/preStop (IX), Job (XII). Каждый фактор реализуется идиоматично, без обходных путей. Поэтому 12-factor app в K8s 'просто работает'. Что НЕ ложится: (1) Stateful monoliths с локальным state и долгим shared memory (legacy enterprise apps типа SAP, Oracle EBS). Можно засунуть в Pod, но без преимуществ K8s. (2) GUI-приложения, требующие display (X11). (3) Apps с huge startup time (5+ минут) — HPA не успевает scale, rolling update медленный. ML inference с 100GB моделями — пограничный случай (init container прогрева, длинный readinessProbe). (4) Apps без health endpoint — Probes не работают, rolling update слепой. Для таких apps часто оборачивают legacy в operator-pattern (StatefulSet + custom controller, knows о специфике).

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Команда написала Dockerfile: FROM python:latest. Какой фактор 12-factor нарушен и как починить?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5