StatefulSet и Deployment в Kubernetes
Deployment и StatefulSet что выбирать
В Kubernetes приложения запускают через контроллеры. Контроллер - это компонент, который постоянно сверяет желаемое состояние и текущее состояние в кластере.
Для старта важно понимать три базовых типа
Deploymentподходит для stateless-сервисовStatefulSetподходит для stateful-сервисовDaemonSetзапускает по одной копии Pod на каждой ноде
Термины
stateless- сервис хранит состояние вне Pod, например в базе данных или в кешеstateful- сервис хранит данные, для которых важны постоянное имя Pod и постоянный дискPod- минимальная единица запуска в Kubernetesnode- сервер или виртуальная машина, где запускаются Podcontroller- логика в control plane, которая управляет ресурсами
Важно
- ресурса
DiamondSetв Kubernetes нет - чаще всего имели в виду
DaemonSet
Базовая карта объектов Kubernetes
Перед практикой полезно видеть общую картину из каких объектов состоит рабочее приложение в кластере
Namespaceразделяет окружения и командыDeploymentилиStatefulSetуправляет PodServiceдает стабильную точку доступа внутри кластераIngressдает внешний HTTP и HTTPS доступConfigMapхранит несекретные настройкиSecretхранит секретные значенияPersistentVolumeClaimзапрашивает постоянный дискHorizontalPodAutoscalerавтоматически меняет число реплик
Что происходит после kubectl apply
- API Server принимает манифест
- объект сохраняется в
etcd - соответствующий контроллер видит новый объект
- контроллер создает или обновляет Pod
kube-schedulerподбирает нодуkubeletна ноде скачивает образ и запускает контейнерServiceначинает направлять трафик в готовые Pod
Готовые образы и свои образы
Разделяйте две сущности
container imageв полеimagemanifestв 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
CIсобирает образ из исходников- образ публикуется в registry
- новый тег подставляется в YAML или в
values.yamlчарта - выполняется
kubectl applyилиhelm upgrade - контроллеры приводят кластер к целевому состоянию
Базовые команды проверки
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.labelsreadinessProbeотвечает за подачу трафикаlivenessProbeотвечает за перезапуск зависшего контейнераresources.requestsнужны для планировщика и HPA
HPA по CPU для Deployment
HorizontalPodAutoscaler или HPA автоматически меняет число реплик у Deployment.
Что такое метрики в этом контексте
CPU utilization- доля текущей загрузки CPU относительноrequests.cpurequests- гарантированный минимум ресурса для контейнера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.localpostgres-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для StatefulSetNodePortи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ограничивает тип нод, например запуск базы только на SSDPodAntiAffinityзапрещает размещение 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
Упрощенный процесс
- Helm читает
values.yamlи дополнительные файлы значений - Helm подставляет значения в шаблоны из
templates - 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.yamlvalues-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 Kubernetesfailover- переключение на резервную реплику при сбое основной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
Пример последовательности для учебного проекта
- собрать и опубликовать backend image
- применить
Secretдля БД - применить
StatefulSetPostgreSQL - применить
Deploymentbackend - применить
Servicebackend - применить
Ingress - применить
HPA - проверить 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.yamlsecret.yamlconfigmap.yamlpostgres-statefulset.yamlbackend-deployment.yamlbackend-service.yamlbackend-ingress.yamlbackend-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
Краткий практический чек-лист
- Для stateless-сервисов используйте
Deployment - Для СУБД и других stateful-нагрузок используйте
StatefulSetи PVC - Для автоскейлинга настройте
HPAи заполнитеresources.requests - Для отказоустойчивости добавьте
podAntiAffinity - Для релизов в нескольких окружениях используйте
Helm - Для production-платформы подключите Harbor, Vault, мониторинг и TLS-автоматизацию
Официальные ссылки