Kube/Pythonproduction course
Курс · RU02-pods-and-workload-resources.md

02. Pod и ресурсы рабочих нагрузок

Незнакомый термин? Откройте словарь Kubernetes и терминов курса.

Назначение, предварительные знания и результаты обучения

Нужны главы 00–01, загруженный образ kube-python-course:1.0.0 и namespace kube-course. Цель — выбирать контроллер по контракту жизненного цикла, а не по знакомому YAML.

После главы вы сможете:

Ментальная модель: Pod — единица планирования с общей судьбой

Pod — минимальная единица планирования: один или несколько контейнеров совместно получают узел, IP-адрес Pod и сетевой namespace, а также могут делить тома. Это не долговечная виртуальная машина. Контейнеры внутри Pod связаны судьбой сильнее, чем разные Pods: их нельзя масштабировать независимо; localhost у них общий.

Уровни жизненного цикла:

Контроллер нагрузки → объект Pod → назначенный узел → песочница → контейнеры
Желаемые реплики        phase       PodScheduled      состояния/перезапуски

Поле phase Pod (Pending, Running, Succeeded, Failed, Unknown) даёт грубую картину. CrashLoopBackOff и ImagePullBackOff — причины состояния ожидания, а не фазы Pod. Читайте .status.containerStatuses[*].state, .lastState, restartCount, conditions и Events.

restartPolicy применяется kubelet к контейнерам приложения всего Pod:

  • Always — значение по умолчанию и обязательная семантика для Deployment;
  • OnFailure — удобна для Job;
  • Never — код завершения остаётся видимым.

Перезапуск контейнера сохраняет UID, IP и тома Pod; заменяющий Pod получает новый UID и обычно новый IP. Задержка повторов увеличивает интервал между повторными запусками.

Полный манифест Deployment

Реальный манифест хранится в manifests/base/deployment.yaml. Сокращённый, полный манифест рабочей нагрузки:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: python-api
  namespace: kube-course
  labels:
    app.kubernetes.io/name: python-api
    app.kubernetes.io/instance: course
    app.kubernetes.io/part-of: kube-python-course
spec:
  replicas: 3
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: python-api
      app.kubernetes.io/instance: course
  template:
    metadata:
      labels:
        app.kubernetes.io/name: python-api
        app.kubernetes.io/instance: course
        app.kubernetes.io/part-of: kube-python-course
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: api
          image: kube-python-course:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8000
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL

Deployment управляет ReplicaSets, ReplicaSet — Pods. Не задавайте restartPolicy: Never: Deployment API отклонит его. replicas меняет число Pods, а не процессов внутри одного Pod.

Как выбирать тип рабочей нагрузки

КонтрактРесурсИдентичность и хранение
Долгоживущие взаимозаменяемые HTTP-репликиDeploymentPods заменяемы
Выполнение до завершения с повторамиJobcompletions, задержка повторов
Периодический JobCronJobрасписание, история Job и конкурентность
Стабильные порядковый номер, сеть и хранилище каждой репликиStatefulSetname-0, Service без ClusterIP, шаблоны PVC
По одному Pod на подходящий узелDaemonSetагент, привязанный к узлу

StatefulSet не «для любой БД»: он даёт идентичность и упорядоченные операции, но не гарантирует корректность репликации, кворум, резервное копирование или логику переключения после отказа. DaemonSet нужен агентам на узлах, а не обычному API. Deployment — стандартный выбор для FastAPI без состояния.

Init-контейнеры, сайдкар и эфемерные контейнеры

Обычные init-контейнеры выполняются последовательно до контейнеров приложения. Используйте их для ограниченной инициализации, а не бесконечного ожидания зависимости.

Нативный сайдкар со стабильным статусом в Kubernetes v1.33 — контейнер в initContainers с restartPolicy: Always на уровне контейнера; он остаётся на всё время жизни Pod. Сайдкар разделяет жизненный цикл и ресурсы Pod и увеличивает радиус поражения. Если телеметрию можно собирать на уровне узла или через SDK, отдельный сайдкар не всегда оправдан.

initContainers:
  - name: log-sidecar
    image: busybox:1.37.0
    restartPolicy: Always
    command: ["sh", "-c", "tail -F /shared/access.log"]
    volumeMounts:
      - name: shared
        mountPath: /shared

Эфемерный контейнер (стабильный с v1.25) добавляется через специальный подресурс для диагностики, например командой kubectl debug; у него нет проверок и ресурсов, он не перезапускается. Это доступ во время инцидента, а не декларативный сайдкар приложения.

Практическое упражнение: Pod → Deployment

Подготовка и отдельный Pod

manifests/labs/pod.yaml содержит полный Pod.

kubectl apply --dry-run=server -f manifests/labs/pod.yaml
kubectl apply -f manifests/labs/pod.yaml
kubectl wait pod/python-api-pod -n kube-course \
  --for=condition=Ready --timeout=120s
kubectl get pod python-api-pod -n kube-course -o wide
kubectl port-forward -n kube-course pod/python-api-pod 8000:8000

В другом терминале:

curl --fail --silent http://127.0.0.1:8000/ | python3 -m json.tool
curl --fail --silent http://127.0.0.1:8000/health/ready

Ожидаются HTTP 200, "service": "python-api" и код завершения 0. Остановите port-forward сочетанием Ctrl-C, затем удалите Pod. Ничто его не восстановит:

kubectl delete pod python-api-pod -n kube-course
kubectl get pod python-api-pod -n kube-course

Последняя команда завершается кодом 1 и сообщает NotFound.

Deployment и владение ресурсами

До полного базового набора нужны объекты конфигурации, поэтому применим их и Deployment:

kubectl apply -f manifests/base/configmap.yaml
kubectl apply -f manifests/base/secret.example.yaml
kubectl apply -f manifests/base/serviceaccount.yaml
kubectl apply -f manifests/base/deployment.yaml
kubectl rollout status deployment/python-api -n kube-course --timeout=180s
kubectl get deploy,rs,pod -n kube-course \
  -l app.kubernetes.io/name=python-api

Ожидается по три значения desired, current и ready. Удалите один управляемый Pod и наблюдайте:

kubectl get pods -n kube-course -l app.kubernetes.io/instance=course -w

Заменяющий Pod появится с новым суффиксом и UID. Ctrl-C завершает наблюдение кодом 130, что ожидаемо для прерывания пользователем.

Job и CronJob

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

apiVersion: batch/v1
kind: Job
metadata:
  name: python-api-smoke
  namespace: kube-course
  labels:
    app.kubernetes.io/name: python-api
    app.kubernetes.io/part-of: kube-python-course
spec:
  backoffLimit: 2
  ttlSecondsAfterFinished: 300
  template:
    metadata:
      labels:
        app.kubernetes.io/name: python-api-smoke
    spec:
      restartPolicy: Never
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: smoke
          image: kube-python-course:1.0.0
          command: ["python", "-c", "print('smoke ok')"]
          resources:
            requests:
              cpu: 10m
              memory: 32Mi
            limits:
              cpu: 100m
              memory: 64Mi
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
kubectl apply -f /tmp/python-api-smoke.yaml
kubectl wait job/python-api-smoke -n kube-course \
  --for=condition=Complete --timeout=60s
kubectl logs -n kube-course job/python-api-smoke

Ожидается smoke ok. Для CronJob всегда задавайте concurrencyPolicy, startingDeadlineSeconds, ограничения истории и контракт часового пояса; расписание исполняет плоскость управления, а идемпотентность бизнес-операции остаётся вашей задачей.

Очистка

kubectl delete job python-api-smoke -n kube-course --ignore-not-found
kubectl delete deployment python-api -n kube-course --ignore-not-found

Объекты конфигурации оставьте для следующей главы.

Самостоятельная задача

Добавьте к отдельному Pod init-контейнер busybox:1.37.0, который записывает initialized в общий emptyDir; основной контейнер должен прочитать файл через kubectl exec ... -- cat. Затем сломайте команду init-контейнера (exit 7), докажите, что контейнер приложения не запускается, и восстановите его. Критерий приёмки: status.initContainerStatuses, Events, журналы -c <init-name> и код завершения служат доказательствами.

Сломанный сценарий: CrashLoopBackOff

kubectl apply -f manifests/broken/02-crashloop.yaml
kubectl get pods -n kube-course -l app.kubernetes.io/name=broken-crashloop -w

Ожидаются CrashLoopBackOff и растущий RESTARTS. Не удаляйте Pod до сбора:

POD="$(kubectl get pod -n kube-course \
  -l app.kubernetes.io/name=broken-crashloop \
  -o jsonpath='{.items[0].metadata.name}')"
kubectl describe pod "$POD" -n kube-course
kubectl logs "$POD" -n kube-course -c crash
kubectl logs "$POD" -n kube-course -c crash --previous
kubectl get pod "$POD" -n kube-course \
  -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'

Ожидаются сообщение intentional crash и код завершения 42. Исправление — изменить команду в Deployment или удалить сломанный Deployment; kubectl delete pod только создаст такой же сломанный Pod.

kubectl delete -f manifests/broken/02-crashloop.yaml

Дерево диагностических решений

Pod не Ready
├─ `PodScheduled=False` → события планировщика: запросы ресурсов, affinity, taint, PVC
├─ состояние init `waiting`/`running` → журналы/status init; приложение ещё не обязано стартовать
├─ контейнер в состоянии waiting
│  ├─ ImagePullBackOff → имя образа/тег/аутентификация/сеть
│  ├─ CreateContainerConfigError → отсутствует ConfigMap/Secret/ключ
│  └─ CrashLoopBackOff → текущие журналы + --previous, lastState/exitCode
├─ Running, restartCount растёт → liveness/OOM/сигнал процесса
└─ Running, но не Ready → endpoint readiness/зависимость/порт

Типичные ошибки: один Pod вместо контроллера; несколько несвязанных процессов в одном Pod; бесконечное ожидание в init-контейнере; сайдкар без ресурсов; restartPolicy как политика повторов Deployment; Job без идемпотентности; kubectl exec в уже завершившийся контейнер вместо logs --previous; правка ReplicaSet.

Эксплуатационные компромиссы, безопасность и стоимость

Каждый сайдкар добавляет свои requests/limits к Pod и обновляется вместе с ним; учитывайте его при планировании ресурсов. Большой revisionHistoryLimit хранит ReplicaSets, а не данные приложения. Jobs без TTL и ограничения истории засоряют API. Доступ к эфемерным контейнерам для диагностики — сильное разрешение (pods/ephemeralcontainers), аудитируйте его. automountServiceAccountToken: false уменьшает раскрытие учётных данных. Pod с несколькими контейнерами подходит только при строгом контракте совместного размещения и жизненного цикла.

Самопроверка

  1. Является ли CrashLoopBackOff фазой?
  2. Что переживает перезапуск контейнера, но не замену Pod?
  3. Кто назначает узел и кто запускает контейнер?
  4. Когда нужен StatefulSet?
  5. Чем нативный сайдкар отличается от обычного init-контейнера?
  6. Почему повтор Job не обеспечивает выполнение ровно один раз?

Ответы: 1) нет, это причина ожидания; 2) UID, IP и тома Pod вроде emptyDir переживают перезапуск, но не замену; 3) scheduler назначает узел, kubelet и среда исполнения запускают контейнер; 4) контракт стабильных порядкового номера, сети и хранилища; 5) restartPolicy: Always, и сайдкар продолжает работать; 6) внешний эффект может завершиться до записи успешного результата Job, поэтому повтор выполнит его снова.

Резюме и источники

Выбирайте контроллер рабочей нагрузки по желаемому жизненному циклу. Диагностируйте состояние контейнера внутри Pod, а не по одной колонке STATUS. Далее: конфигурация и секреты.