Kubernetes Production — Helm, RBAC, Ingress

Kubernetes Production — набор подходов и ресурсов Kubernetes, необходимых для эксплуатации приложений в рабочем окружении: упаковка и выпуск через Helm, внешний доступ через Ingress, разграничение прав с RBAC, хранение состояния, автоматическое масштабирование и контроль готовности контейнеров.

Типичная производственная схема:

Пользователь
    |
    v
Load Balancer / внешний адрес
    |
    v
Ingress Controller
    |
    v
Ingress -> Service -> Pods
                       |
                       +-> ConfigMap / Secret
                       +-> PersistentVolumeClaim -> PersistentVolume

Helm управляет манифестами приложения.
RBAC ограничивает доступ пользователей и ServiceAccount.
HPA изменяет количество Pod в зависимости от нагрузки.
Readiness и liveness probes контролируют доступность контейнеров.

Helm не заменяет Kubernetes API, а формирует обычные Kubernetes-манифесты и применяет их как единый релиз. Ingress сам по себе также не обрабатывает трафик: для этого в кластере должен работать Ingress Controller.

Содержание


Helm

Helm — пакетный менеджер для Kubernetes. Он позволяет описать набор связанных Kubernetes-ресурсов в виде пакета — chart, настроить их через параметры и установить в кластер как единый release.

Основные понятия:

Понятие Назначение
Chart Пакет с шаблонами Kubernetes-ресурсов
Release Установленный в кластер экземпляр chart
Repository Хранилище опубликованных chart
Values Значения, подставляемые в шаблоны
Template Kubernetes-манифест с выражениями Go templates
Dependency Другой chart, используемый текущим chart

Один chart можно установить несколько раз с разными именами и параметрами:

helm install shop-dev ./shop-chart \
  --namespace shop-dev \
  --create-namespace

helm install shop-prod ./shop-chart \
  --namespace shop-prod \
  --create-namespace \
  -f values-prod.yaml

shop-dev и shop-prod — два независимых Helm-релиза одного chart.

Основные команды Helm

# Проверить клиент Helm
helm version

# Создать каркас chart
helm create my-app

# Проверить chart на типичные ошибки
helm lint ./my-app

# Отрендерить манифесты локально без установки
helm template my-release ./my-app

# Установить chart
helm install my-release ./my-app

# Установить или обновить релиз одной командой
helm upgrade --install my-release ./my-app

# Показать релизы
helm list --all-namespaces

# Показать состояние релиза
helm status my-release

# Показать историю ревизий
helm history my-release

# Откатить релиз к указанной ревизии
helm rollback my-release 2

# Удалить релиз
helm uninstall my-release

Для production-развёртывания часто применяют:

helm upgrade --install my-app ./chart \
  --namespace production \
  --create-namespace \
  -f values-production.yaml \
  --atomic \
  --wait \
  --timeout 10m

Структура Helm chart

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

my-app/
├── Chart.yaml
├── Chart.lock
├── values.yaml
├── values-production.yaml
├── charts/
└── templates/
    ├── _helpers.tpl
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── serviceaccount.yaml
    ├── hpa.yaml
    ├── configmap.yaml
    ├── NOTES.txt
    └── tests/
        └── test-connection.yaml

Chart.yaml

Содержит метаданные chart:

apiVersion: v2
name: my-app
description: Helm chart для веб-приложения
type: application
version: 1.2.0
appVersion: "2.5.1"

Версия chart и версия приложения не обязаны совпадать.

Зависимости

Зависимости объявляются в Chart.yaml:

dependencies:
  - name: redis
    version: "20.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled

Загрузка и обновление зависимостей:

helm dependency update ./my-app
helm dependency build ./my-app

Chart.lock фиксирует разрешённые версии зависимостей. Его обычно сохраняют в системе контроля версий.


Values и переопределение настроек

values.yaml содержит значения по умолчанию:

replicaCount: 2

image:
  repository: registry.example.com/my-app
  tag: "2.5.1"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

ingress:
  enabled: false
  className: nginx
  host: app.example.com
  tls:
    enabled: false
    secretName: app-tls

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

Значения для production можно вынести в отдельный файл:

# values-production.yaml
replicaCount: 3

image:
  tag: "2.5.1"

ingress:
  enabled: true
  host: app.example.com
  tls:
    enabled: true
    secretName: app-tls

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: "1"
    memory: 1Gi

Файлы применяются слева направо, более поздние значения имеют приоритет:

helm upgrade --install my-app ./chart \
  -f values.yaml \
  -f values-production.yaml

Отдельное значение можно передать через командную строку:

helm upgrade --install my-app ./chart \
  --set replicaCount=4 \
  --set image.tag=2.5.2

Для строк, которые могут быть ошибочно преобразованы в число или логическое значение, используют --set-string:

helm upgrade --install my-app ./chart \
  --set-string image.tag=2026.09

Секреты не следует хранить в открытом виде в values.yaml. --set также нежелателен для секретов: значение может попасть в историю команд, журналы CI/CD и метаданные релиза. Обычно применяют внешний менеджер секретов, зашифрованные файлы или заранее созданные Kubernetes Secret.

Приоритет значений

В упрощённом виде приоритет возрастает так:

  1. values.yaml внутри chart;
  2. значения родительского chart для зависимостей;
  3. файлы, переданные через -f или --values;
  4. параметры --set, --set-string и аналогичные.

Просмотр итоговых значений установленного релиза:

helm get values my-app
helm get values my-app --all

Шаблоны Helm

Шаблоны Helm основаны на Go templates и библиотеке функций Sprig. Выражения записываются внутри {{ ... }}.

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

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-app.fullname" . }}
  labels:
    {{- include "my-app.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "my-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Основные встроенные объекты:

Объект Содержимое
.Values Значения из values.yaml, файлов -f и --set
.Chart Данные из Chart.yaml
.Release Имя, namespace и данные текущего релиза
.Capabilities Возможности и версии API целевого кластера
.Template Информация о текущем шаблоне
.Files Доступ к дополнительным файлам chart

Условия

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
# ...
{{- end }}

Проверка нескольких условий:

{{- if and .Values.ingress.enabled .Values.ingress.tls.enabled }}
# TLS-настройки
{{- end }}

Циклы

values.yaml:

env:
  - name: LOG_LEVEL
    value: info
  - name: APP_MODE
    value: production

Шаблон:

env:
  {{- range .Values.env }}
  - name: {{ .name }}
    value: {{ .value | quote }}
  {{- end }}

Конвейеры и функции

metadata:
  name: {{ .Values.nameOverride | default .Chart.Name | trunc 63 | trimSuffix "-" }}

Результат передаётся от одной функции к другой через |.

Полезные функции:

# Заключить значение в кавычки
value: {{ .Values.mode | quote }}

# Обязательное значение
host: {{ required "ingress.host обязателен" .Values.ingress.host }}

# Преобразовать структуру в YAML и выровнять отступ
{{- toYaml .Values.resources | nindent 10 }}

# Вычислить SHA-256 — полезно для перезапуска при изменении ConfigMap
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

Именованные шаблоны

Вспомогательные шаблоны обычно размещаются в _helpers.tpl:

{{- define "my-app.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

Подключение:

metadata:
  labels:
    {{- include "my-app.labels" . | nindent 4 }}

Проверка результата

Перед установкой полезно выполнить:

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

helm lint проверяет структуру chart, но не гарантирует, что ресурсы будут приняты конкретным кластером. Для production-процесса дополнительно применяют серверную проверку, схемы значений, policy-as-code и тестовое развёртывание.


Управление Helm-релизами

Установка:

helm install my-app ./chart \
  --namespace production \
  --create-namespace

Обновление:

helm upgrade my-app ./chart \
  --namespace production \
  -f values-production.yaml

Идемпотентный вариант для CI/CD:

helm upgrade --install my-app ./chart \
  --namespace production \
  --create-namespace \
  -f values-production.yaml

Просмотр сгенерированных манифестов релиза:

helm get manifest my-app -n production

Просмотр заметок chart:

helm get notes my-app -n production

История и откат:

helm history my-app -n production
helm rollback my-app 3 -n production --wait

Следует учитывать, что откат Kubernetes-манифестов не всегда откатывает внешние изменения: миграции базы данных, содержимое постоянного тома и операции внешних сервисов требуют отдельной стратегии.


Ingress и Ingress Controller

Ingress — Kubernetes-ресурс с правилами маршрутизации входящего HTTP/HTTPS-трафика к Service внутри кластера.

Ingress Controller — компонент, который наблюдает за объектами Ingress и фактически настраивает прокси или балансировщик нагрузки.

Популярные варианты контроллеров:

Создание только объекта Ingress без установленного и подходящего контроллера не обеспечит внешний доступ.

Базовая цепочка:

Клиент -> Load Balancer -> Ingress Controller -> Service -> Pod

Пример Service:

apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: production
spec:
  type: ClusterIP
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 8080

Пример Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  namespace: production
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

ingressClassName указывает, какой контроллер должен обработать ресурс.

Типы пути

pathType Поведение
Exact Точное совпадение пути
Prefix Совпадение по сегментам префикса URL
ImplementationSpecific Поведение определяет Ingress Controller

Маршрутизация нескольких путей:

spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend
                port:
                  number: 80

Маршрутизация по разным доменам:

spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80
    - host: admin.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: admin
                port:
                  number: 80

Аннотации Ingress зависят от выбранного контроллера. Настройка, работающая с одним контроллером, может не поддерживаться другим. Поэтому аннотации следует проверять по документации конкретного Ingress Controller.


TLS и маршрутизация Ingress

TLS-секрет содержит сертификат и закрытый ключ:

kubectl create secret tls app-tls \
  --cert=tls.crt \
  --key=tls.key \
  -n production

Ingress с TLS:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  namespace: production
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

Обычно TLS завершается на Ingress Controller, после чего трафик к Service передаётся внутри кластера. При повышенных требованиях может использоваться TLS и между прокси и приложением.

Для автоматического выпуска и обновления сертификатов часто применяют отдельный контроллер сертификатов. Сертификаты, закрытые ключи и DNS-записи должны управляться как часть общей инфраструктуры, а не вручную на каждом релизе.

Диагностика Ingress:

kubectl get ingress -A
kubectl describe ingress web -n production
kubectl get ingressclass
kubectl get pods -n ingress-nginx
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller
kubectl get endpointslices -n production -l kubernetes.io/service-name=web

Если Ingress отвечает ошибкой, последовательно проверяют:

  1. DNS домена и внешний адрес контроллера;
  2. выбранный ingressClassName;
  3. правила host и path;
  4. существование Service и правильность его порта;
  5. наличие EndpointSlice с готовыми Pod;
  6. readiness probe приложения;
  7. журналы Ingress Controller;
  8. TLS Secret и соответствие сертификата домену.

RBAC в Kubernetes

RBAC — Role-Based Access Control, модель управления доступом на основе ролей.

RBAC отвечает на вопрос: какой субъект может выполнить какое действие над каким ресурсом.

Основные объекты:

Объект Область действия Назначение
Role Namespace Набор разрешений внутри одного namespace
ClusterRole Весь кластер Кластерные или переиспользуемые разрешения
RoleBinding Namespace Назначает Role или ClusterRole субъекту в namespace
ClusterRoleBinding Весь кластер Назначает ClusterRole на уровне кластера

Субъектом может быть:

Kubernetes RBAC не создаёт обычных пользователей. Аутентификация пользователей выполняется внешним механизмом, а RBAC определяет их полномочия после успешной аутентификации.

Role

Разрешение читать Pod и их журналы в namespace production:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]

Пустая строка в apiGroups обозначает основную API-группу, к которой относятся Pod, Service, ConfigMap и Secret.

Частые значения verbs:

get, list, watch, create, update, patch, delete, deletecollection

RoleBinding

Назначение роли пользователю:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: production
subjects:
  - kind: User
    name: developer@example.com
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

roleRef после создания binding нельзя произвольно заменить на другую роль — обычно объект удаляют и создают заново.

ClusterRole

Чтение узлов кластера:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: node-reader
rules:
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]

Назначение на уровне всего кластера:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: read-nodes
subjects:
  - kind: Group
    name: platform-engineers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: node-reader
  apiGroup: rbac.authorization.k8s.io

ClusterRole можно назначить через RoleBinding. Тогда её разрешения действуют только на ресурсы указанного namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: namespace-view
  namespace: production
subjects:
  - kind: Group
    name: developers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: view
  apiGroup: rbac.authorization.k8s.io

Проверка доступа

# Может ли текущий пользователь читать Pod?
kubectl auth can-i get pods -n production

# Может ли пользователь удалить Deployment?
kubectl auth can-i delete deployments \
  -n production \
  --as=developer@example.com

# Может ли ServiceAccount читать Secret?
kubectl auth can-i get secrets \
  -n production \
  --as=system:serviceaccount:production:my-app

# Показать доступные текущему пользователю действия
kubectl auth can-i --list -n production

Принцип наименьших привилегий

Следует выдавать только необходимые действия и ресурсы. Опасные широкие правила:

rules:
  - apiGroups: ["*"]
    resources: ["*"]
    verbs: ["*"]

Особого внимания требуют разрешения на:

Возможность создать Pod с произвольным ServiceAccount или примонтировать Secret часто фактически означает возможность получить полномочия этих объектов.


ServiceAccount

ServiceAccount — учётная запись для процессов, работающих внутри Pod.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app
  namespace: production

Использование в Deployment:

spec:
  template:
    spec:
      serviceAccountName: my-app
      automountServiceAccountToken: false
      containers:
        - name: app
          image: registry.example.com/my-app:2.5.1

Если приложение не обращается к Kubernetes API, автоматическое подключение токена лучше отключить:

automountServiceAccountToken: false

Если приложению нужен доступ, создаётся минимальная Role и RoleBinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: config-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["my-app-config"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: my-app-config-reader
  namespace: production
subjects:
  - kind: ServiceAccount
    name: my-app
    namespace: production
roleRef:
  kind: Role
  name: config-reader
  apiGroup: rbac.authorization.k8s.io

resourceNames ограничивает правило конкретными именованными объектами, но поддержка и поведение зависят от типа запроса. Например, обычный list нельзя полноценно ограничить списком имён так же, как get.


StatefulSet

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

В отличие от Deployment, StatefulSet предоставляет:

StatefulSet применяют для баз данных, брокеров сообщений, распределённых хранилищ и других stateful-систем. Наличие StatefulSet само по себе не делает приложение отказоустойчивым: репликация, выбор лидера, резервное копирование и восстановление определяются самим приложением или оператором.

Пример headless Service:

apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: production
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - name: postgres
      port: 5432

Пример StatefulSet:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: production
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:17
          ports:
            - name: postgres
              containerPort: 5432
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: password
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          readinessProbe:
            exec:
              command:
                - sh
                - -c
                - pg_isready -U postgres
            initialDelaySeconds: 5
            periodSeconds: 10
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 20Gi

Для Pod postgres-0 будет создан PVC с предсказуемым именем, например data-postgres-0.

Масштабирование StatefulSet:

kubectl scale statefulset postgres --replicas=3 -n production

Увеличивать число реплик безопасно только тогда, когда приложение умеет формировать кластер и реплицировать данные. Простое создание трёх экземпляров базы данных не превращает их в единый кластер.

Удаление StatefulSet обычно не удаляет его PVC автоматически. Это защищает данные, но требует контролируемой очистки неиспользуемых томов.


PersistentVolume и PersistentVolumeClaim

PersistentVolume (PV) — ресурс кластера, представляющий постоянное хранилище.

PersistentVolumeClaim (PVC) — запрос приложения на хранилище с определённым размером, режимом доступа и классом.

Связь:

Pod -> PVC -> PV -> реальный диск или сетевое хранилище

Пример PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
  namespace: production
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast
  resources:
    requests:
      storage: 10Gi

Подключение PVC к Pod:

spec:
  containers:
    - name: app
      image: registry.example.com/my-app:2.5.1
      volumeMounts:
        - name: data
          mountPath: /var/lib/my-app
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: app-data

Режимы доступа

Режим Назначение
ReadWriteOnce (RWO) Чтение и запись с одного узла
ReadOnlyMany (ROX) Чтение с нескольких узлов
ReadWriteMany (RWX) Чтение и запись с нескольких узлов
ReadWriteOncePod (RWOP) Чтение и запись только одним Pod

Фактическая поддержка режимов зависит от CSI-драйвера и типа хранилища. ReadWriteOnce относится прежде всего к подключению на уровне узла и не всегда означает «ровно один Pod».

Фазы PVC и PV

Частые состояния:

Диагностика:

kubectl get pvc -A
kubectl get pv
kubectl describe pvc app-data -n production
kubectl get storageclass

StorageClass и динамическое выделение томов

StorageClass описывает класс хранилища и CSI-провайдер, который динамически создаёт тома для PVC.

Пример:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: csi.example.com
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
  type: ssd

Ключевые поля:

reclaimPolicy

Для критичных данных Retain уменьшает риск случайного удаления, но не заменяет резервное копирование.

volumeBindingMode

Immediate выделяет том сразу после создания PVC.

WaitForFirstConsumer откладывает выделение до планирования первого Pod. Это помогает выбрать хранилище в правильной зоне доступности и избежать ситуации, когда Pod нельзя разместить рядом с томом.

Расширение PVC

Если StorageClass и CSI-драйвер поддерживают расширение:

kubectl patch pvc app-data -n production \
  -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

Уменьшение существующего PVC обычно не поддерживается. Перед изменением критичного тома необходимо проверить возможности драйвера и наличие актуальной резервной копии.

Снимки и резервные копии

PV обеспечивает постоянство данных при перезапуске Pod, но не является резервной копией. Для production необходимы:


Автомасштабирование HPA

HorizontalPodAutoscaler (HPA) автоматически изменяет количество реплик Deployment, StatefulSet или другого масштабируемого workload на основе метрик.

Типичная схема:

Metrics Server / monitoring adapter
              |
              v
             HPA
              |
              v
Deployment replicas: 2 -> 5 -> 3

HPA не масштабирует отдельный Pod по вертикали. Он изменяет количество реплик целевого ресурса.

Пример HPA по CPU:

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

Для метрики CPU типа Utilization контейнеры должны иметь resources.requests.cpu:

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

Упрощённая логика расчёта:

желаемые реплики ≈ текущие реплики × текущая метрика / целевая метрика

Например, если работают 4 реплики, средняя загрузка CPU равна 140%, а целевая — 70%, HPA стремится примерно к 8 репликам. Реальное решение учитывает допуски, отсутствующие метрики, готовность Pod и политики стабилизации.

HPA по нескольким метрикам

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: AverageValue
          averageValue: 400Mi

При нескольких метриках контроллер рассчитывает рекомендуемое число реплик для каждой и выбирает наибольшее допустимое значение.

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

behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
      - type: Percent
        value: 100
        periodSeconds: 60
      - type: Pods
        value: 4
        periodSeconds: 60
    selectPolicy: Max
  scaleDown:
    stabilizationWindowSeconds: 300
    policies:
      - type: Percent
        value: 25
        periodSeconds: 60

Окно стабилизации при уменьшении числа реплик помогает избежать частых колебаний.

Условия работы HPA

Для HPA необходим источник метрик:

Проверка:

kubectl get hpa -A
kubectl describe hpa web -n production
kubectl top pods -n production
kubectl top nodes

Частые причины, по которым HPA не работает:

Если HPA управляет Deployment, значение spec.replicas в GitOps или Helm следует согласовать с автомасштабированием, чтобы разные контроллеры не перезаписывали число реплик друг у друга.


Readiness, liveness и startup probes

Проверки состояния помогают Kubernetes принимать решения о маршрутизации трафика и перезапуске контейнера.

Probe Что проверяет Результат ошибки
readinessProbe Готов ли контейнер принимать трафик Pod исключается из endpoints Service
livenessProbe Работоспособен ли контейнер Контейнер перезапускается
startupProbe Завершился ли запуск медленного приложения До успеха отключает влияние liveness/readiness по правилам запуска

Readiness probe

readinessProbe:
  httpGet:
    path: /ready
    port: http
  initialDelaySeconds: 5
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3
  successThreshold: 1

Если readiness probe не проходит, контейнер продолжает работать, но Pod перестаёт получать трафик через Service. Это полезно при прогреве приложения, временной перегрузке или потере критичной зависимости.

Liveness probe

livenessProbe:
  httpGet:
    path: /health
    port: http
  initialDelaySeconds: 20
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

Liveness должна проверять внутреннюю жизнеспособность процесса. Не следует без необходимости связывать её с доступностью внешней базы данных или стороннего API: сбой общей зависимости может вызвать одновременный перезапуск всех Pod и усугубить проблему.

Startup probe

startupProbe:
  httpGet:
    path: /health
    port: http
  periodSeconds: 5
  failureThreshold: 30

В этом примере приложению даётся до 150 секунд на запуск. Пока startup probe не завершилась успешно, liveness и readiness не начинают влиять на контейнер согласно обычному циклу проверок.

Виды проверок

HTTP GET

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
    scheme: HTTP

HTTP-код от 200 до 399 считается успешным.

TCP socket

livenessProbe:
  tcpSocket:
    port: 5432
  periodSeconds: 10

Проверяет возможность установить TCP-соединение, но не гарантирует корректную работу прикладного протокола.

Exec

readinessProbe:
  exec:
    command:
      - sh
      - -c
      - test -f /tmp/ready
  periodSeconds: 5

Команда с кодом выхода 0 означает успех. Частые и тяжёлые exec-проверки могут создавать лишнюю нагрузку.

gRPC

livenessProbe:
  grpc:
    port: 50051
  periodSeconds: 10

Приложение должно поддерживать стандартный протокол gRPC health checking.

Параметры probes

Поле Значение
initialDelaySeconds Задержка перед первой проверкой
periodSeconds Интервал между проверками
timeoutSeconds Максимальное время одной проверки
failureThreshold Число ошибок до признания проверки неуспешной
successThreshold Число успехов для восстановления состояния
terminationGracePeriodSeconds Время на корректное завершение после ошибки liveness, если поддерживается конфигурацией

Разделение endpoint

Рекомендуется разделять:

/health или /live — процесс жив и не находится в неисправимом состоянии
/ready           — экземпляр готов принимать новый трафик
/startup         — приложение завершило начальную загрузку

Один и тот же endpoint допустим для простого приложения, но отдельные проверки позволяют точнее управлять поведением.

Graceful shutdown

При остановке Pod приложение должно прекратить принимать новый трафик и корректно завершить текущие запросы:

spec:
  terminationGracePeriodSeconds: 30
  containers:
    - name: app
      lifecycle:
        preStop:
          exec:
            command: ["sh", "-c", "sleep 5"]

preStop не заменяет корректную обработку сигнала завершения приложением. Процесс должен обрабатывать SIGTERM, закрывать соединения и завершаться до окончания terminationGracePeriodSeconds.


Рекомендации для production

Helm

Ingress

RBAC

Stateful workloads

HPA

Probes


Общий пример

Ниже приведён упрощённый Helm chart для stateless-приложения с ServiceAccount, Deployment, Service, Ingress и HPA.

values.yaml

replicaCount: 2

image:
  repository: registry.example.com/web
  tag: "1.4.0"
  pullPolicy: IfNotPresent

serviceAccount:
  create: true
  name: ""

service:
  port: 80
  targetPort: 8080

ingress:
  enabled: true
  className: nginx
  host: web.example.com
  tls:
    enabled: true
    secretName: web-tls

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

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

templates/_helpers.tpl

{{- define "web.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "web.fullname" -}}
{{- printf "%s-%s" .Release.Name (include "web.name" .) | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "web.selectorLabels" -}}
app.kubernetes.io/name: {{ include "web.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{- define "web.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "web.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

templates/serviceaccount.yaml

{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ include "web.serviceAccountName" . }}
automountServiceAccountToken: false
{{- end }}

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "web.fullname" . }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      {{- include "web.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "web.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ include "web.serviceAccountName" . }}
      terminationGracePeriodSeconds: 30
      containers:
        - name: web
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          startupProbe:
            httpGet:
              path: /health
              port: http
            periodSeconds: 5
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health
              port: http
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 5"]

Если HPA включён, поле replicas не выводится в шаблон Deployment, чтобы Helm не возвращал количество реплик к значению из values.yaml при каждом обновлении.

templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ include "web.fullname" . }}
spec:
  type: ClusterIP
  selector:
    {{- include "web.selectorLabels" . | nindent 4 }}
  ports:
    - name: http
      port: {{ .Values.service.port }}
      targetPort: http

templates/ingress.yaml

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "web.fullname" . }}
spec:
  ingressClassName: {{ .Values.ingress.className }}
  {{- if .Values.ingress.tls.enabled }}
  tls:
    - hosts:
        - {{ .Values.ingress.host | quote }}
      secretName: {{ .Values.ingress.tls.secretName }}
  {{- end }}
  rules:
    - host: {{ .Values.ingress.host | quote }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ include "web.fullname" . }}
                port:
                  name: http
{{- end }}

templates/hpa.yaml

{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ include "web.fullname" . }}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{ include "web.fullname" . }}
  minReplicas: {{ .Values.autoscaling.minReplicas }}
  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
{{- end }}

Проверка и установка

helm lint ./web-chart

helm template web ./web-chart \
  --namespace production \
  -f values-production.yaml

helm upgrade --install web ./web-chart \
  --namespace production \
  --create-namespace \
  -f values-production.yaml \
  --atomic \
  --wait \
  --timeout 10m

Проверка ресурсов:

kubectl get deploy,pods,svc,ingress,hpa -n production
kubectl rollout status deployment/web-web -n production
kubectl describe hpa web-web -n production
kubectl get endpointslices -n production
kubectl auth can-i --list \
  --as=system:serviceaccount:production:web-web \
  -n production

Полный production-процесс обычно также включает NetworkPolicy, Pod Security Standards, PodDisruptionBudget, распределение по узлам и зонам, мониторинг, централизованные журналы, управление секретами, резервное копирование и проверяемую стратегию восстановления.