Что такое под в Kubernetes и почему это не контейнер
Под (Pod) в Kubernetes - минимальная вычислительная единица, которую можно создать и которой можно управлять через API. Это не контейнер, а обёртка над одним или несколькими контейнерами, которые совместно используют сетевое пространство и тома. Планировщик, kubelet и контроллеры работают именно с подами: ставят их на узлы, перезапускают, удаляют и учитывают в квотах.
Аналогия, которая помогает не путать уровни абстракции: контейнер - это процесс с изолированной файловой системой, а под - песочница для одного или нескольких таких процессов с общей сетью и общими дисками. Контейнеры одного пода всегда оказываются на одном узле, общаются друг с другом через localhost и видят смонтированные в под тома.
Минимальный манифест пода с одним контейнером nginx выглядит так:
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
Применяем и смотрим результат:
kubectl apply -f pod.yaml kubectl get pods kubectl get pods -o wide
В выводе kubectl get pods колонки означают: NAME - имя пода, READY - сколько контейнеров готовы из общего числа, STATUS - отображаемое поле для удобства пользователя (фаза пода или причина сбоя), RESTARTS - число перезапусков, AGE - время с момента создания. Расширенный вывод добавляет IP пода и NODE, на который его поставил планировщик.
Помимо прикладных контейнеров под может содержать init-контейнеры, которые отрабатывают последовательно до старта основных, и sidecar-контейнеры, работающие параллельно с приложением. Все они описаны в одном манифесте и делят ресурсы пода.
Один или несколько контейнеров: зачем нужны multi-container поды
Сценарии с несколькими контейнерами в одном поде сводятся к трём паттернам:
- Sidecar - вспомогательный контейнер расширяет функциональность основного. Типичный пример: приложение пишет логи в файл, а sidecar с fluentd читает этот файл и отправляет записи в централизованное хранилище.
- Ambassador - контейнер-прокси, через который основной контейнер ходит во внешний мир: например, подключение к базе через прокси с шифрованием и балансировкой.
- Adapter - контейнер приводит вывод приложения к единому формату. Так экспортёр метрик превращает локальные счётчики приложения в формат, понятный системе мониторинга.
Общие у контейнеров пода только сеть и тома. Файловые системы у них отдельные: каждый контейнер запускается из своего образа и не видит файлы соседа, если их не положить в общий том. Поэтому обмен строят либо через localhost, либо через смонтированный volume.
Multi-container поды - продвинутый сценарий. Для веб-сервера, очереди или API в большинстве случаев достаточно одного контейнера, а лишние sidecar добавляют потребление памяти и усложняют отладку.
Общее сетевое пространство и тома: что это даёт на практике
У пода один IP-адрес и один набор портов на все контейнеры. Практическое следствие: два контейнера в одном поде не могут слушать один и тот же порт - второй не стартует, потому что порт занят. Основной контейнер открывает 80, sidecar с метриками - 9090, и оба доступны по адресу пода.
Для обмена файлами внутри пода используют том emptyDir. Он создаётся вместе с подом и живёт ровно столько же: при перезапуске контейнера файлы в нём остаются на месте, а при удалении и пересоздании пода том очищается. Пример пода, где два контейнера делят каталог /data:
apiVersion: v1
kind: Pod
metadata:
name: shared-volume-pod
spec:
containers:
- name: writer
image: busybox
command: ["sh", "-c", "while true; do date >> /data/log.txt; sleep 5; done"]
volumeMounts:
- name: shared-data
mountPath: /data
- name: reader
image: busybox
command: ["sh", "-c", "tail -f /data/log.txt"]
volumeMounts:
- name: shared-data
mountPath: /data
volumes:
- name: shared-data
emptyDir: {}
Кроме emptyDir в под монтируют configMap и secret с конфигурацией, а также persistentVolumeClaim для постоянного хранения: этот том переживает пересоздание пода. Тома описываются на уровне пода и подключаются в контейнеры через volumeMounts.
Архитектурная роль пода: место в иерархии объектов Kubernetes
Под - низкоуровневая единица. Напрямую (bare pods) их создают для отладки и разовых проверок, потому что такой под не пересоздаётся: удалили - и он исчез, а при падении узла не переедет на другой. В продакшене подами управляют контроллеры: Deployment, ReplicaSet, StatefulSet, DaemonSet. Они следят за нужным количеством реплик, обновляют шаблон пода и восстанавливают его после сбоев.
Иерархия для stateless-приложений выглядит так: Deployment описывает желаемое состояние, создаёт ReplicaSet, а ReplicaSet держит нужное число подов. Deployment с replicas: 3 в итоге даст три пода на узлах кластера. Проверить всю цепочку можно тремя командами:
kubectl get deployments kubectl get replicasets kubectl get pods
Deployment и ReplicaSet: как работает масштабирование и обновление
ReplicaSet отвечает за количество подов, Deployment - за версию шаблона и историю обновлений. Когда вы меняете образ или параметры в шаблоне пода, Deployment создаёт новый ReplicaSet и постепенно переносит реплики из старого в новый. Так получается обновление без простоя.
Есть две стратегии. RollingUpdate (по умолчанию) заменяет поды партиями, ориентируясь на параметры maxSurge и maxUnavailable. Recreate сначала удаляет все старые поды и только потом поднимает новые: короткий простой в обмен на простую логику, подходит приложениям без поддержки параллельных версий.
kubectl set image deployment/nginx nginx=nginx:1.25 kubectl rollout status deployment/nginx kubectl rollout undo deployment/nginx
Первая команда меняет образ, вторая показывает ход обновления, третья откатывает релиз к предыдущему ReplicaSet. Настройку probes, requests и limits в шаблоне пода разбираем отдельно в руководстве по Deployment.
StatefulSet и DaemonSet: когда нужны особые контроллеры
StatefulSet нужен приложениям, которым важны стабильные идентификаторы и свои данные: базам данных, брокерам сообщений, etcd, ZooKeeper. Каждый под получает предсказуемое имя (например, db-0, db-1), собственный PVC и запускается по порядку - следующий только после готовности предыдущего.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
DaemonSet ставит по одному поду на каждый подходящий узел. Так запускают сборщики логов (fluentd, filebeat), агенты мониторинга (node-exporter) и сетевые плагины. Новый узел появляется в кластере - DaemonSet сразу создаёт на нём свой под.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
containers:
- name: node-exporter
image: prom/node-exporter:v1.7.0
ports:
- containerPort: 9100
Сравнение всех контроллеров, включая Job и CronJob, с таблицей различий собрано в материале о выборе контроллера Kubernetes.
Жизненный цикл пода: от создания до завершения
Путь пода начинается с отправки манифеста в API-сервер, продолжается планированием на узел и запуском контейнеров, а заканчивается удалением или падением. Состояние пода описывают пять фаз:
- Pending - под принят кластером, но контейнеры ещё не запущены: ждёт планирования, скачивания образа или монтирования тома.
- Running - под назначен на узел, все контейнеры созданы, и хотя бы один из них работает.
- Succeeded - все контейнеры в поде завершились успешно и не будут перезапущены. Типично для Job.
- Failed - все контейнеры завершились, и как минимум один завершился с ошибкой (ненулевой код выхода или остановка системой) и не настроен на автоматический перезапуск.
- Unknown - состояние пода получить не удалось, обычно из-за ошибки связи с узлом, на котором под должен выполняться.
Фаза - высокоуровневое состояние. Детали лежат в статусах контейнеров и условиях пода, а поведение после завершения контейнера определяет политика перезапуска (restart policy): она задаёт, как Kubernetes реагирует на завершение контейнеров из-за ошибок или по другим причинам. Под существует до момента завершения в соответствии с этой политикой. Полный статус пода в YAML смотрите командой kubectl get pod <pod-name> -o yaml.
Формулировки фаз и статусов стоит сверять с документацией вашей версии Kubernetes: между релизами добавляются новые причины и уточняются переходы между состояниями.
Фазы пода и статусы контейнеров: как читать вывод kubectl
Колонка STATUS - это отображаемое поле kubectl для удобства пользователя, и его не следует путать с фазой пода, которая является явной частью модели данных Kubernetes. В колонке могут появляться и фазы, и причины сбоя:
| Значение STATUS | Что означает |
|---|---|
| Running | Фаза пода: контейнеры запущены, под работает |
| Pending | Фаза пода: контейнеры ещё не стартовали |
| Succeeded | Фаза пода: все контейнеры завершились успешно и не будут перезапущены |
| Failed | Фаза пода: контейнеры завершились, минимум один - с ошибкой |
| CrashLoopBackOff | Отображаемое поле kubectl: контейнер падает и перезапускается с растущей задержкой |
| ImagePullBackOff | Отображаемое поле kubectl: образ не удалось скачать |
У контейнера внутри пода есть собственное состояние: Waiting (ждёт запуска), Running (работает) и Terminated (завершён с кодом выхода). Условия пода (PodConditions) дают ещё один срез: PodScheduled - под привязан к узлу, Initialized - init-контейнеры успешно отработали, ContainersReady - все контейнеры готовы, Ready - под может принимать трафик.
kubectl describe pod nginx-pod kubectl get pod nginx-pod -o yaml kubectl logs nginx-pod -c nginx
В выводе describe ключевой блок - Events в самом низу: там видно, что именно происходило с подом: попытки скачать образ, ошибки монтирования тома, отказ в привязке к узлу.
Probes: как Kubernetes проверяет здоровье пода
Три типа проб решают разные задачи. livenessProbe перезапускает контейнер, если он завис. readinessProbe держит под вне балансировки, пока приложение не готово принимать трафик. startupProbe даёт медленно стартующим приложениям время на инициализацию, отключая остальные проверки до первого успеха.
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: web
image: nginx:1.25
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
Без readinessProbe трафик через Service пойдёт на под, который ещё читает конфигурацию или прогревает кэш, и часть запросов закончится ошибкой. Без livenessProbe зависший процесс останется в статусе Running, хотя реально не отвечает.
Практика: создаём под и проверяем его состояние
Соберём последовательность шагов, по которой видно жизненный цикл пода целиком.
- Сохраните манифест nginx-pod в файл pod.yaml и примените его: kubectl apply -f pod.yaml.
- Проверьте статус: kubectl get pods. Через несколько секунд после Pending под перейдёт в Running.
- Посмотрите детали: kubectl describe pod nginx-pod, блок Events покажет планирование, скачивание образа и старт контейнера.
- Проверьте логи: kubectl logs nginx-pod.
- Зайдите внутрь контейнера: kubectl exec -it nginx-pod -- /bin/sh.
- Удалите под: kubectl delete pod nginx-pod. Он больше не появится - bare pod никто не пересоздаёт.
Теперь тот же сценарий для пода под управлением Deployment: удалите под командой kubectl delete pod, и ReplicaSet через несколько секунд поднимет новый. В этом главное практическое отличие контроллера от одиночного пода. Пошаговый разбор первого запуска с minikube есть в гайде по Kubernetes для начинающих.
Диагностика проблем: CrashLoopBackOff, ImagePullBackOff и другие
Набор команд для разбора почти любого сбоя одинаков:
kubectl describe pod <pod-name> kubectl logs <pod-name> --previous kubectl get events --sort-by=.metadata.creationTimestamp kubectl get nodes
- CrashLoopBackOff - контейнер стартует и падает, kubelet перезапускает его с растущей паузой. Смотрите kubectl logs --previous: причина в выводе упавшего процесса, часто это отсутствующая переменная окружения или непримонтированный конфиг.
- ImagePullBackOff и ErrImagePull - образ не скачивается. Проверьте имя и тег, наличие imagePullSecrets для приватного реестра и доступность реестра из кластера; детали будут в Events у describe.
- Pending - под не может найти узел. Частые причины: запрошено больше CPU или памяти, чем есть на узлах; nodeSelector или taints не совпадают с метками узлов; PVC остался в статусе Pending из-за неподходящего класса хранилища.
- Ошибки монтирования томов - в Events видно сообщение вида FailedMount. Проверьте, существует ли configMap или secret с указанным именем и в том же namespace, что и под.
Связь пода с сетью и сервисами: как трафик попадает в под
Каждый под получает собственный IP-адрес, но этот адрес не постоянен: после пересоздания он изменится. Поэтому обращаться к подам напрямую по IP в приложениях нельзя. Service - абстрактный способ предоставить доступ к приложению, которое работает на наборе подов (документация Kubernetes). Тип ClusterIP даёт сервису виртуальный IP в масштабе кластера, и Kubernetes распределяет трафик к этому адресу между нижележащими подами.
Кроме ClusterIP используют NodePort (открывает порт на каждом узле) и LoadBalancer (получает внешний адрес у облачного провайдера). Внутри кластера чаще всего достаточно ClusterIP: другие поды обращаются к сервису по DNS-имени. Посмотреть список сервисов и их адреса: kubectl get svc. Как сервисы связаны с подами и остальными объектами API, разбираем в материале об архитектуре кластера.
ClusterIP: автоматическое и ручное выделение, конфликты
Control plane либо сам выбирает свободный адрес из настроенного диапазона, либо берёт тот, который вы указали в манифесте вручную. Каждый ClusterIP в кластере уникален, поэтому попытка создать Service с уже занятым адресом возвращает ошибку (Allocation des ClusterIP des Services). Пример сервиса с заданным адресом:
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
spec:
clusterIP: 10.96.0.20
selector:
app: nginx
ports:
- port: 80
targetPort: 80
Чтобы не ловить конфликты, статические адреса резервируют в нижней части диапазона. Официальная документация приводит такие примеры полос: для диапазона 10.96.0.0/24 статическая полоса идёт с 10.96.0.1 по 10.96.0.16 при конце диапазона 10.96.0.254; для 10.96.0.0/20 - с 10.96.0.1 по 10.96.1.0 при конце диапазона 10.96.15.254; для 10.96.0.0/16 - с 10.96.0.1 по 10.96.1.0 при конце 10.96.255.254. Динамическое выделение по умолчанию использует верхнюю полосу, а после её исчерпания переходит к нижнему диапазону. Показательный случай с DNS Service: при диапазоне 10.96.0.0/16 адрес 10.96.0.10 не зарезервирован, поэтому при динамическом выделении его может занять другой сервис, и создание DNS Service закончится ошибкой конфликта.
Безопасность подов: PodSecurityPolicy и Pod Security Standards
Права пода ограничивают на уровне admission-контроллеров. Исторический механизм PodSecurityPolicy (PSP) сопоставляется с Pod Security Standards, а поля PodSecurityPolicySpec описывают, какие изменения нужно внести в под и какие проверки он обязан пройти (таблица сопоставления PSP и Pod Security Standards). Дословный разбор соответствий по полям здесь не приводим: значения зависят от версии, сверять их нужно по первоисточнику.
Pod Security Standards задают три профиля с чёткой линейной прогрессией от наиболее безопасного (Restricted) к наименее безопасному (Privileged). Privileged почти не ограничивает под. Baseline запрещает известные опасные возможности: привилегированный доступ к хосту, совместное использование хостовых пространств имён, привилегированные поды, добавление capabilities сверх разрешённого списка, а также тома HostPath. Restricted идёт дальше: требует запуск не от root, запрещает повышение привилегий и сужает набор допустимых capabilities. Restricted - разумная цель для большинства приложений, но он часто ломает старые образы, которые стартуют от root.
Проверить, какое именно правило нарушает ваш под, проще всего в тестовом namespace: под будет отклонён с сообщением о нарушении, а конкретный параметр окажется в Events.
Миграция с PodSecurityPolicy на PodSecurity admission controller
PodSecurityPolicy был объявлен устаревшим в Kubernetes v1.21 и удалён из Kubernetes в v1.25. Его место занимает встроенный PodSecurity admission controller: вместо PSP можно применять аналогичные ограничения на поды с помощью встроенного контроллера или стороннего admission-плагина, который вы разворачиваете и настраиваете сами (миграция описана в документации). Встроенный контроллер работает без отдельного API-объекта: политика задаётся метками на namespace, а режимы enforce, audit и warn позволяют сначала наблюдать нарушения, а потом блокировать их.
kubectl label namespace prod \ pod-security.kubernetes.io/enforce=baseline \ pod-security.kubernetes.io/audit=restricted \ pod-security.kubernetes.io/warn=restricted
Порядок миграции такой: включите warn и audit, соберите список нарушений по существующим подам, исправьте манифесты, и только после этого ставьте enforce. Резкая смена профиля на prod-кластере заблокирует создание подов, которые раньше стартовали без замечаний.
Типичные ошибки при работе с подами и как их избежать
- Bare pods в продакшене. Такой под не пересоздаётся после удаления и не переезжает при падении узла. Для stateless-приложений нужен Deployment, для stateful - StatefulSet.
- Нет readinessProbe. Service начинает слать трафик сразу после старта контейнера, и часть запросов падает. Добавьте проверку готовности для любого приложения, которое тратит время на инициализацию.
- Некорректные requests и limits. Приводят к Pending (запросили больше, чем есть на узлах) или OOMKilled (превысили лимит памяти).
- Конфликты ClusterIP. Указанный вручную адрес уже занят другим сервисом, и создание завершается ошибкой. Держите статические адреса в зарезервированной полосе.
- Игнорирование Pod Security Standards. Под с privileged: true и доступом к хостовым путям проходит в кластер без ограничений и превращает уязвимость в компрометацию узла.
- Состояние в emptyDir. Том очищается вместе с подом, поэтому данные базы или очереди там хранить нельзя.
Ресурсы и лимиты: как не получить OOMKilled или Pending
requests - сколько ресурсов планировщик гарантирует поду и учитывает при выборе узла. limits - верхняя граница, которую контейнер не может превысить. По CPU превышение лимита даёт троттлинг, по памяти - убийство контейнера с причиной OOMKilled.
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 128Mi
Если под висит в Pending, проверьте запросы: сумма requests всех подов на узле не может превышать его доступные ресурсы. Если контейнер убивают по памяти, смотрите kubectl describe pod и реальное потребление, а не только пиковые значения. Для критичных сервисов разумно ставить requests равными limits: тогда под получает класс качества Guaranteed и не вытесняется другими подами.
Когда использовать поды напрямую, а когда контроллеры
Прямое создание пода оправдано в двух случаях: отладка манифеста и разовая проверка сети или тома в кластере. Всё, что должно жить дольше пяти минут, ставьте под контроллер. Stateless-приложение (веб-сервер, API, воркер очереди) - Deployment. Stateful-приложение с собственными данными и стабильным именем - StatefulSet. Агент, который нужен на каждом узле, - DaemonSet.
Контроллеры дают самовосстановление, горизонтальное масштабирование и обновление по стратегии RollingUpdate. Повторяемо разворачивать и обновлять приложения помогает скрипт с параметрами namespace и тега образа, пример собран в разборе скрипта развертывания.
Чек-лист перед деплоем в кластер:
- Поды управляются контроллером (Deployment, StatefulSet или DaemonSet), а не созданы напрямую.
- Настроены readinessProbe и livenessProbe, для медленного старта добавлена startupProbe.
- Заданы requests и limits, для критичных сервисов requests равны limits.
- Профиль Pod Security Standards выбран осознанно: сначала warn и audit, потом enforce.
- Для доступа к подам создан Service, статические ClusterIP вынесены в нижнюю полосу диапазона.
- Настроены алерты на CrashLoopBackOff, Pending и рост RESTARTS.