Анатомия Pod
Pod — наименьшая deployable unit в Kubernetes. Это не контейнер, и это важно: Pod — это группа из одного или нескольких контейнеров, которые делят между собой набор Linux-ресурсов и всегда запускаются на одной node. Когда вы говорите «запустить приложение в K8s», на физическом уровне вы говорите «создать Pod на каком-то узле, внутри которого container runtime поднимет контейнеры». Если контейнеров в Pod несколько — они стартуют и убиваются вместе, видят друг друга на localhost и могут общаться через shared memory. Pod нельзя переместить между узлами: Pod либо живёт на узле A, либо удаляется и пересоздаётся на узле B как новый объект с новым IP.
Почему Pod, а не контейнер
Истоки идеи Pod идут от Google Borg. На практике почти всегда у вас один контейнер на Pod, и тогда Pod выглядит как «обёртка над контейнером». Но иногда нужны tightly coupled processes: процессы, которые обязаны жить на одной машине, иметь общий network namespace, общий доступ к диску и общий жизненный цикл. Примеры из реальности:
- Sidecar — рядом с основным приложением крутится прокси (Envoy в service mesh), который перехватывает сетевой трафик. Им нужен общий network namespace, чтобы прокси видел
localhost:8080приложения. - Init контейнеры — отдельные контейнеры, которые запускаются ДО основных, делают какую-то подготовку (миграции БД, генерация конфига, дождаться доступности зависимости) и завершаются.
- Adapter / Ambassador — конвертация форматов логов, прокси к внешнему сервису через shared loopback.
Pod как абстракция позволяет планировать (schedule) такую группу процессов атомарно: scheduler выбирает один узел для всей группы, kubelet запускает их вместе, livenessProbe убивает их вместе.
Pod — это не виртуальная машина и не «маленький сервер». Это группа контейнеров, разделяющих набор Linux namespaces и volumes. Никакого собственного OS, никакого systemd, никакого SSH внутри Pod — только процессы контейнеров.
Linux namespaces и capabilities: как ядро делит root-полномочия
Shared Linux namespaces
Технически Pod в Linux реализован как набор контейнеров, у которых частично shared namespaces. Linux namespace — это механизм изоляции (PID, network, mount, IPC, UTS, user, cgroup), Docker и containerd используют их, чтобы один контейнер не видел процессы и сеть другого. В Pod часть этих namespace специально делается общей:
Ключевой момент: общий network namespace означает, что все контейнеры в Pod видят друг друга на 127.0.0.1. Если контейнер app слушает :8080, а контейнер sidecar хочет до него достучаться — он просто делает curl http://localhost:8080. Никакого Service, никакого DNS — обычный loopback. Это и есть главная техническая причина существования Pod.
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
shareProcessNamespace: true # опционально: общий PID namespace
containers:
- name: app
image: myapp:1.0
ports:
- containerPort: 8080
- name: sidecar
image: envoyproxy/envoy:v1.30
# этот контейнер видит app на localhost:8080
pause container: невидимый держатель namespaces
Когда вы делаете crictl ps на узле, вы увидите больше контейнеров, чем у вас в Pod-spec. На каждый Pod kubelet поднимает дополнительный pause container (он же infra container, sandbox container). Этот контейнер:
- запускается первым в Pod;
- держит все shared namespaces (network, IPC, UTS);
- ничего не делает — буквально вызывает
pause()в C и спит навечно; - весит ~700 KB (registry.k8s.io/pause).
Зачем он нужен. Когда обычный контейнер падает и kubelet его перезапускает, он должен сохранить network namespace — иначе Pod получит новый IP при каждом restart. Pause container держит namespace «живым», пока другие контейнеры пересоздаются вокруг него. Когда pause умирает — умирает весь Pod, namespace освобождается, и kubelet говорит CNI plugin отпустить IP.
Pause container — это причина, по которой kubectl get pods показывает Pod как Running даже если ваш app container в CrashLoopBackOff. Pod считается «живым», пока жив pause. Реальный статус приложения смотрите в status.containerStatuses.
PodIP и CNI
У каждого Pod — собственный IP-адрес, выделенный из cluster pod CIDR (например, 10.244.0.0/16). Этот IP не привязан к IP узла. Pod-ы на разных узлах могут общаться напрямую по IP без NAT — это базовое требование Kubernetes networking model.
Кто выдаёт IP: CNI plugin (Container Network Interface). Это бинарник, который kubelet вызывает в момент создания Pod. Популярные CNI: Calico, Cilium, Flannel, Weave Net, AWS VPC CNI, Azure CNI. Они отличаются по тому, как реализуют сеть (overlay через VXLAN, BGP, eBPF), но интерфейс к kubelet — стандартный.
PodIP — эфемерный: после restart Pod (kubectl delete + Deployment пересоздаёт) IP почти всегда другой. Никогда не пишите код, который полагается на конкретный PodIP. Используйте Services (ClusterIP, headless) или DNS-имена — это стабильные endpoints.
hostname, subdomain и DNS-имя Pod
По умолчанию hostname Pod-а равен имени Pod (metadata.name). Это видно изнутри:
kubectl exec mypod -- hostname
# mypod
Если задать spec.hostname и spec.subdomain, и в том же namespace создать headless Service с тем же именем, что и subdomain, — Pod получит полноценное FQDN в cluster DNS:
apiVersion: v1
kind: Pod
metadata:
name: db-0
spec:
hostname: db-0
subdomain: mydb
containers:
- name: postgres
image: postgres:16
При наличии headless Service mydb в том же namespace, Pod будет резолвиться как db-0.mydb.<namespace>.svc.cluster.local. Это базовый механизм StatefulSet — у каждой реплики стабильное DNS-имя. Подробно — в модуле про Services.
Базовый Pod manifest
Самый минимальный валидный Pod:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
Применяется:
kubectl apply -f nginx.yaml
kubectl get pod nginx
kubectl describe pod nginx
kubectl logs nginx
kubectl exec -it nginx -- /bin/sh
kubectl delete pod nginx
В реальности голые Pod создаются редко — обычно через Deployment, который сам управляет ими. Но для CKAD понимание чистого Pod manifest обязательно: половина задач на экзамене начинается с kubectl run или kubectl create -f pod.yaml.
Killer момент: containerPort — это документация
В spec вы пишете:
ports:
- containerPort: 8080
name: http
protocol: TCP
Логичный вопрос: что произойдёт, если этого блока не указать? Ничего не сломается. Приложение продолжит слушать на 8080, и любой другой Pod (или сам узел) сможет до него достучаться по PodIP:8080.
Дело в том, что containerPort не открывает порт и не настраивает iptables. CNI plugin не читает это поле. Это чисто декларативная документация — для людей, читающих YAML, и для других объектов K8s, которые на него ссылаются. Реальное прослушивание порта определяется тем, какие порты открыло само приложение внутри контейнера.
Тогда зачем containerPort указывать? Три причины: (1) документация — читатель сразу видит, на каких портах слушает Pod; (2) ServiceMonitor от Prometheus может ссылаться на порт по имени; (3) kubectl port-forward и Service.targetPort могут ссылаться на name из этого блока, что удобнее, чем хардкодить число.
hostPort — это уже другая история: он реально пробрасывает порт узла внутрь Pod (CNI это делает). Но hostPort в продакшене почти не используется — он ломает scheduling (только один Pod с этим портом на узле) и обходит абстракцию Service.
QoS class: предисловие
У каждого Pod есть QoS class — Guaranteed, Burstable, BestEffort. Он вычисляется автоматически из resources.requests и resources.limits контейнеров и влияет на то, какой Pod kubelet убьёт первым при нехватке ресурсов на узле. Подробно — в модуле 13 про Resource Management. Сейчас достаточно знать, что QoS — это computed поле, вы его не задаёте напрямую.