Перейти к основному содержимому

StatefulSet и Deployment в Kubernetes

Разработчику Архитектору Инженеру

Deployment и StatefulSet что выбирать

В Kubernetes приложения запускают через контроллеры. Контроллер - это компонент, который постоянно сверяет желаемое состояние и текущее состояние в кластере.

Для старта важно понимать три базовых типа

  • Deployment подходит для stateless-сервисов
  • StatefulSet подходит для stateful-сервисов
  • DaemonSet запускает по одной копии Pod на каждой ноде

Термины

  • stateless - сервис хранит состояние вне Pod, например в базе данных или в кеше
  • stateful - сервис хранит данные, для которых важны постоянное имя Pod и постоянный диск
  • Pod - минимальная единица запуска в Kubernetes
  • node - сервер или виртуальная машина, где запускаются Pod
  • controller - логика в control plane, которая управляет ресурсами

Важно

  • ресурса DiamondSet в Kubernetes нет
  • чаще всего имели в виду DaemonSet

Базовая карта объектов Kubernetes

Перед практикой полезно видеть общую картину из каких объектов состоит рабочее приложение в кластере

  • Namespace разделяет окружения и команды
  • Deployment или StatefulSet управляет Pod
  • Service дает стабильную точку доступа внутри кластера
  • Ingress дает внешний HTTP и HTTPS доступ
  • ConfigMap хранит несекретные настройки
  • Secret хранит секретные значения
  • PersistentVolumeClaim запрашивает постоянный диск
  • HorizontalPodAutoscaler автоматически меняет число реплик

Что происходит после kubectl apply

  1. API Server принимает манифест
  2. объект сохраняется в etcd
  3. соответствующий контроллер видит новый объект
  4. контроллер создает или обновляет Pod
  5. kube-scheduler подбирает ноду
  6. kubelet на ноде скачивает образ и запускает контейнер
  7. Service начинает направлять трафик в готовые Pod

Готовые образы и свои образы

Разделяйте две сущности

  • container image в поле image
  • manifest в YAML для Kubernetes

Практическое правило

  • для инфраструктуры обычно берут готовые официальные образы, например postgres, redis, nginx
  • для бизнес-логики обычно собирают свой образ из Dockerfile
  • готовый образ публикуют в registry и указывают тег в манифесте, например my-registry/app:v1.2.0

Связанные материалы


Как правильно выбирать и версионировать образ

Хорошая практика для production

  • использовать конкретные теги вместо latest
  • хранить тег релиза и git SHA
  • подписывать и сканировать образ до деплоя
  • не хранить окруженческие настройки внутри образа

Пример тега

registry.company.ru/backend-api:v1.8.3
registry.company.ru/backend-api:git-7f2c11a

Минимальный pipeline сборки и публикации

docker build -t registry.company.ru/backend-api:v1.8.3 .
docker push registry.company.ru/backend-api:v1.8.3

Общий цикл деплоя в Kubernetes

  1. CI собирает образ из исходников
  2. образ публикуется в registry
  3. новый тег подставляется в YAML или в values.yaml чарта
  4. выполняется kubectl apply или helm upgrade
  5. контроллеры приводят кластер к целевому состоянию

Базовые команды проверки

kubectl get deploy,statefulset,pods -A
kubectl rollout status deployment/backend-full-cycle -n default
kubectl describe pod <pod-name> -n default

Подробнее про базовые команды


Deployment для stateless приложения

Ниже минимальный рабочий манифест для backend-сервиса

apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-full-cycle
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: api-service
template:
metadata:
labels:
app: api-service
spec:
containers:
- name: main-backend
image: my-registry.ru/backend/api:v1.2.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 20
periodSeconds: 10

Что важно новичку

  • selector.matchLabels должен совпадать с template.metadata.labels
  • readinessProbe отвечает за подачу трафика
  • livenessProbe отвечает за перезапуск зависшего контейнера
  • resources.requests нужны для планировщика и HPA

HPA по CPU для Deployment

HorizontalPodAutoscaler или HPA автоматически меняет число реплик у Deployment.

Что такое метрики в этом контексте

  • CPU utilization - доля текущей загрузки CPU относительно requests.cpu
  • requests - гарантированный минимум ресурса для контейнера
  • limits - верхняя граница ресурса, которую контейнер не может превысить

Обязательные условия для CPU-based HPA

  • в кластере установлен metrics-server
  • у контейнера заполнены resources.requests
  • поле averageUtilization рассчитывается от requests, не от limits

Пример backend-hpa.yaml:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: backend-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: backend-full-cycle
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70

Проверка

kubectl get hpa
kubectl describe hpa backend-scaler -n default

Ссылки


Как HPA принимает решение по репликам

Упрощенная логика для CPU

  • HPA периодически получает текущую метрику CPU каждого Pod
  • HPA считает среднюю загрузку относительно requests.cpu
  • если среднее выше цели, число реплик увеличивается
  • если среднее ниже цели, число реплик уменьшается

Полезно помнить

  • слишком маленький requests.cpu вызывает ложные масштабирования
  • слишком большой requests.cpu может занижать фактическую нагрузку
  • для сервисов с резкими пиками стоит добавить поведенческие настройки HPA

Пример с поведением масштабирования

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: backend-scaler-advanced
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: backend-full-cycle
minReplicas: 2
maxReplicas: 10
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 100
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 20
periodSeconds: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70

Почему для баз данных используют StatefulSet

StatefulSet полезен для систем, где данные должны переживать перезапуск Pod.

Для СУБД это стандартный выбор, потому что важны:

  • постоянное имя Pod
  • постоянный диск
  • предсказуемый порядок старта и остановки

Ключевые гарантии StatefulSet

  • стабильные имена Pod, например postgres-0, postgres-1
  • предсказуемый порядок запуска и остановки
  • отдельный PersistentVolumeClaim на каждый Pod
  • после перезапуска Pod подключается к своему PVC

Связка сущностей

  • PersistentVolume или PV - диск, доступный в кластере
  • PersistentVolumeClaim или PVC - запрос на диск от приложения
  • StorageClass - класс хранилища, который описывает, как выделять диск

Ссылки


Что такое headless Service и зачем он нужен StatefulSet

StatefulSet требует headless Service с clusterIP: None.

Почему это важно

  • каждый Pod получает DNS-имя
  • можно обращаться к конкретному Pod по имени
  • это необходимо для репликации и кластерных протоколов БД

Пример имен

  • postgres-0.postgres-headless.default.svc.cluster.local
  • postgres-1.postgres-headless.default.svc.cluster.local

Пример StatefulSet для PostgreSQL

Ниже минимально рабочая схема

  • headless Service для DNS-имен Pod
  • StatefulSet для стабильных Pod и дисков
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
labels:
app: postgres
spec:
ports:
- port: 5432
name: db
clusterIP: None
selector:
app: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: "postgres-headless"
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
name: db
env:
- name: POSTGRES_DB
value: "production_db"
- name: POSTGRES_USER
value: "db_admin"
- name: POSTGRES_PASSWORD
value: "SuperSecretPassword123"
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
volumeMounts:
- name: pg-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: pg-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "standard"
resources:
requests:
storage: 20Gi

Для production

  • пароль и другие секреты выносят в Secret или во внешний Secret Manager
  • в репозиторий не коммитят пароли в открытом виде

StatefulSet с секретом вместо открытого пароля

Secret удобнее создать отдельно и подключить через env.valueFrom.secretKeyRef.

Пример Secret

apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
type: Opaque
stringData:
POSTGRES_PASSWORD: "ChangeMeStrongPassword"

Фрагмент контейнера PostgreSQL

env:
- name: POSTGRES_DB
value: "production_db"
- name: POSTGRES_USER
value: "db_admin"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: POSTGRES_PASSWORD

Сервисы для базы и для приложения

Для базы и backend обычно делают разные типы сервисов

  • ClusterIP для внутреннего доступа внутри кластера
  • Headless Service для StatefulSet
  • NodePort и LoadBalancer обычно для внешнего доступа

Пример Service для backend

apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
selector:
app: api-service
ports:
- port: 80
targetPort: 8080
type: ClusterIP

Разнесение Pod по нодам через affinity

Для отказоустойчивости backend и database лучше размещать на разных нодах.

Термины

  • NodeAffinity ограничивает тип нод, например запуск базы только на SSD
  • PodAntiAffinity запрещает размещение Pod рядом по определенным меткам
  • topologyKey определяет уровень ограничения, обычно это kubernetes.io/hostname

Пример anti-affinity в Deployment backend:

apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-full-cycle
spec:
replicas: 2
selector:
matchLabels:
app: api-service
template:
metadata:
labels:
app: api-service
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- postgres-db
topologyKey: "kubernetes.io/hostname"
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- api-service
topologyKey: "kubernetes.io/hostname"
containers:
- name: main-backend
image: my-backend-app:v1

NodeAffinity для привязки базы к типу нод

Если в кластере есть ноды с быстрыми SSD, базу можно привязать к ним через label.

Пример label для ноды

kubectl label nodes worker-1 disktype=ssd

Фрагмент NodeAffinity для StatefulSet

affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values:
- ssd

Команды проверки планирования Pod

После добавления affinity полезно проверить распределение Pod

kubectl get pods -o wide -n default
kubectl describe pod backend-full-cycle-xxxx -n default
kubectl get nodes --show-labels

На что смотреть

  • в describe pod блок Events и причины FailedScheduling
  • на какой ноде фактически запущен Pod
  • есть ли нужные labels на нодах

Helm и чарты для управления манифестами

Helm - пакетный менеджер для Kubernetes.

Он помогает

  • хранить параметры окружений в values.yaml
  • генерировать итоговые манифесты из шаблонов
  • ставить релизы и откатывать их

Типовая структура чарта:

my-backend-chart/
├── Chart.yaml
├── values.yaml
└── templates/
├── deployment.yaml
├── service.yaml
└── ingress.yaml

values.yaml обычно хранит параметры окружения

replicaCount: 3

image:
repository: my-registry.ru/backend/api
tag: "v1.2.0"
pullPolicy: IfNotPresent

resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"

service:
type: ClusterIP
port: 8080

ingress:
enabled: true
host: "api.mycompany.com"

Фрагмент шаблона templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-backend
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: web-app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
ports:
- containerPort: {{ .Values.service.port }}

Базовые команды Helm

helm install my-app ./my-backend-chart
helm upgrade my-app ./my-backend-chart -f values-prod.yaml
helm rollback my-app 1

Ссылки


Как работает values и шаблоны в Helm

Упрощенный процесс

  1. Helm читает values.yaml и дополнительные файлы значений
  2. Helm подставляет значения в шаблоны из templates
  3. Helm отправляет результат в Kubernetes API

Полезные команды отладки

helm lint ./my-backend-chart
helm template my-app ./my-backend-chart -f values-prod.yaml
helm upgrade --install my-app ./my-backend-chart -f values-prod.yaml --dry-run --debug

Что дают эти команды

  • lint проверяет синтаксис чарта и типичные ошибки
  • template показывает итоговые YAML до применения
  • dry-run имитирует установку без изменения кластера

Пример values для dev и prod окружений

Обычно создают отдельные файлы значений

  • values-dev.yaml
  • values-prod.yaml

Пример values-dev.yaml

replicaCount: 1
image:
repository: my-registry.ru/backend/api
tag: "v1.2.0-dev"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
ingress:
enabled: true
host: "api-dev.mycompany.com"

Пример values-prod.yaml

replicaCount: 3
image:
repository: my-registry.ru/backend/api
tag: "v1.2.0"
resources:
requests:
cpu: "300m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
ingress:
enabled: true
host: "api.mycompany.com"

Ingress в составе Helm чарта

Ingress дает внешний HTTP и HTTPS доступ к сервису по доменному имени.

Что участвует в маршруте

  • Ingress как правило маршрутизации
  • Ingress Controller как runtime-компонент, который применяет это правило
  • Service как внутренняя точка назначения для трафика

Пример templates/ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}-backend
port:
number: {{ .Values.service.port }}

TLS для Ingress через cert manager

Для production обычно нужен HTTPS с автоматическим продлением сертификата.

Пример фрагмента Ingress

spec:
tls:
- hosts:
- api.mycompany.com
secretName: api-tls
rules:
- host: api.mycompany.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 80

Пример ClusterIssuer для Let's Encrypt

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@mycompany.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx

Harbor, Vault и другие production-сервисы вокруг k8s

Полноценная платформа вокруг Kubernetes обычно включает

  • Harbor как приватный registry с RBAC и сканированием уязвимостей
  • Vault как централизованное хранилище секретов и механизм ротации
  • Prometheus и Grafana как стек метрик и дашбордов
  • cert-manager как автоматизация TLS-сертификатов
  • Argo CD или Flux как GitOps-инструменты доставки

Как это работает вместе

  • CI собирает и пушит образ в Harbor
  • сканер в Harbor проверяет CVE
  • Argo CD применяет манифесты или Helm chart из Git
  • Pod получает секреты через Vault интеграцию
  • Prometheus собирает метрики и отдает их в Grafana

Минимальный стек для старта команды

  • registry
  • секреты
  • мониторинг
  • доставка конфигурации
  • журналирование

Обычно к этому стеку добавляют Loki или ELK для логов

Связанные материалы


Когда StatefulSet лучше заменить оператором

Для сложных БД-кластеров один StatefulSet часто требует много ручной работы.

Оператор - это расширение Kubernetes, которое добавляет специальный контроллер для конкретного продукта.

Пример CloudNativePG для PostgreSQL

  • вы описываете желаемый кластер в одном CRD-ресурсе
  • оператор сам управляет репликами и ролями primary и replica
  • при сбое primary оператор проводит failover
  • оператор создает сервисы для read-write и read-only трафика

Термины

  • CRD - Custom Resource Definition, способ добавить новый тип ресурса в API Kubernetes
  • failover - переключение на резервную реплику при сбое основной
  • primary - инстанс, который принимает запись
  • replica - инстанс, который повторяет данные с primary и обычно отдает чтение

Пример ресурса Cluster

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: prod-database
spec:
instances: 3
storage:
size: 50Gi
storageClass: "premium-rwo"
postgresql:
parameters:
max_connections: "200"
shared_buffers: "512MB"
backup:
barmanObjectStore:
destinationPath: "s3://my-k8s-backups/postgres/"
endpointURL: "https://amazonaws.com"
wal:
compression: gzip
s3Credentials:
accessKeyId:
name: s3-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: s3-creds
key: SECRET_ACCESS_KEY

StatefulSet вручную и Operator в реальных командах

Когда обычно достаточно StatefulSet

  • одна база без сложной репликации
  • простые требования к бэкапам
  • команда готова сопровождать PostgreSQL вручную

Когда лучше использовать Operator

  • нужно автоматическое переключение primary
  • нужны регулярные бэкапы и восстановление
  • нужна репликация из коробки
  • нужен понятный day 2 operations

Что такое day 2 operations

  • обновления версии базы
  • управление инцидентами
  • смена паролей и сертификатов
  • проверка резервных копий
  • восстановление после аварии

Сценарий полного деплоя backend и postgres

Пример последовательности для учебного проекта

  1. собрать и опубликовать backend image
  2. применить Secret для БД
  3. применить StatefulSet PostgreSQL
  4. применить Deployment backend
  5. применить Service backend
  6. применить Ingress
  7. применить HPA
  8. проверить Pod, Service, Ingress, HPA

Пример команд

kubectl apply -f postgres-secret.yaml
kubectl apply -f postgres-statefulset.yaml
kubectl apply -f backend-deployment.yaml
kubectl apply -f backend-service.yaml
kubectl apply -f backend-ingress.yaml
kubectl apply -f backend-hpa.yaml
kubectl get pods,svc,ingress,hpa -n default

Диагностика частых проблем

Pod в состоянии Pending

Проверка

kubectl describe pod <pod-name> -n default

Частые причины

  • не хватает CPU или памяти на нодах
  • слишком жесткие affinity правила
  • не найден PersistentVolume для PVC

ImagePullBackOff

Проверка

kubectl describe pod <pod-name> -n default

Частые причины

  • неправильный тег образа
  • у кластера нет доступа к приватному registry
  • не настроен imagePullSecret

CrashLoopBackOff

Проверка

kubectl logs <pod-name> -n default --previous
kubectl describe pod <pod-name> -n default

Частые причины

  • ошибка приложения на старте
  • неверные переменные окружения
  • слишком агрессивная livenessProbe

HPA не масштабирует

Проверка

kubectl get hpa -n default
kubectl describe hpa backend-scaler -n default
kubectl top pods -n default

Частые причины

  • не установлен metrics-server
  • у контейнера нет resources.requests
  • фактическая нагрузка ниже порога

Безопасность манифестов для новичка

Базовые правила

  • не хранить пароли в открытом YAML
  • ограничивать права ServiceAccount
  • не запускать контейнеры под root без необходимости
  • фиксировать версии образов
  • включать сканирование уязвимостей образов

Фрагмент securityContext для контейнера

securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true

Минимальный набор манифестов для старта проекта

Для учебного backend проекта обычно достаточно

  • namespace.yaml
  • secret.yaml
  • configmap.yaml
  • postgres-statefulset.yaml
  • backend-deployment.yaml
  • backend-service.yaml
  • backend-ingress.yaml
  • backend-hpa.yaml

Удобная структура каталогов

k8s/
├── base/
│ ├── namespace.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── postgres-statefulset.yaml
│ ├── backend-deployment.yaml
│ ├── backend-service.yaml
│ ├── backend-ingress.yaml
│ └── backend-hpa.yaml
└── overlays/
├── dev/
└── prod/

Полезные команды kubectl для ежедневной работы

kubectl get pods -A
kubectl get deploy,statefulset,svc,ingress,hpa -A
kubectl logs -f deployment/backend-full-cycle -n default
kubectl describe statefulset postgres -n default
kubectl get pvc,pv -n default
kubectl rollout status deployment/backend-full-cycle -n default
kubectl rollout history deployment/backend-full-cycle -n default
kubectl top pods -n default
kubectl top nodes

Краткий практический чек-лист

  1. Для stateless-сервисов используйте Deployment
  2. Для СУБД и других stateful-нагрузок используйте StatefulSet и PVC
  3. Для автоскейлинга настройте HPA и заполните resources.requests
  4. Для отказоустойчивости добавьте podAntiAffinity
  5. Для релизов в нескольких окружениях используйте Helm
  6. Для production-платформы подключите Harbor, Vault, мониторинг и TLS-автоматизацию

Официальные ссылки

Содержание