02. Pod и ресурсы рабочих нагрузок
Незнакомый термин? Откройте словарь Kubernetes и терминов курса.
Назначение, предварительные знания и результаты обучения
Нужны главы 00–01, загруженный образ kube-python-course:1.0.0 и namespace
kube-course. Цель — выбирать контроллер по контракту жизненного цикла, а не
по знакомому YAML.
После главы вы сможете:
- объяснить песочницу Pod,
phase, состояние контейнера и задержку перезапусков; - использовать init-контейнеры, нативные сайдкары и эфемерные контейнеры;
- управлять Deployment/ReplicaSet и отличать Job/CronJob;
- выбрать Deployment, StatefulSet или DaemonSet;
- диагностировать CrashLoopBackOff по текущему и предыдущему состоянию;
- объяснить неизменяемые поля Pod и семантику замены.
Ментальная модель: 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-реплики | Deployment | Pods заменяемы |
| Выполнение до завершения с повторами | Job | completions, задержка повторов |
| Периодический Job | CronJob | расписание, история Job и конкурентность |
| Стабильные порядковый номер, сеть и хранилище каждой реплики | StatefulSet | name-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 с несколькими контейнерами подходит только при строгом контракте совместного
размещения и жизненного цикла.
Самопроверка
- Является ли
CrashLoopBackOffфазой? - Что переживает перезапуск контейнера, но не замену Pod?
- Кто назначает узел и кто запускает контейнер?
- Когда нужен StatefulSet?
- Чем нативный сайдкар отличается от обычного init-контейнера?
- Почему повтор Job не обеспечивает выполнение ровно один раз?
Ответы: 1) нет, это причина ожидания; 2) UID, IP и тома Pod вроде emptyDir
переживают перезапуск, но не замену; 3) scheduler назначает узел, kubelet и
среда исполнения запускают контейнер; 4) контракт стабильных порядкового
номера, сети и хранилища; 5) restartPolicy: Always, и сайдкар продолжает
работать; 6) внешний эффект может завершиться до записи успешного результата
Job, поэтому повтор выполнит его снова.
Резюме и источники
Выбирайте контроллер рабочей нагрузки по желаемому жизненному циклу. Диагностируйте состояние контейнера внутри Pod, а не по одной колонке STATUS. Далее: конфигурация и секреты.