Multi-container Pod: зачем и когда
Базовая единица планирования в Kubernetes — не контейнер, а Pod. Pod может содержать один контейнер (типичный случай), а может несколько. И когда вы вынуждены упаковать несколько процессов в один Pod, начинаются нетривиальные решения: какой паттерн использовать, что про lifecycle, как ведёт себя scheduler. С v1.33 окончательно стабилизировалась ещё одна категория — native Sidecar containers через initContainers с restartPolicy: Always (alpha v1.28, beta v1.29 с default on, GA v1.33) — которая решает целый класс старых проблем.
Airflow стек: webserver + scheduler + Postgres в compose
Pod = atomic unit
Pod — это группа контейнеров, которые планируются вместе на один Node, делят набор Linux namespaces и одну lifecycle границу. Это значит:
- Все контейнеры в Pod на одном Node — scheduler не разнесёт их по разным машинам.
- Контейнеры делят network namespace — у Pod один IP, контейнеры видят друг друга по
localhost, конфликтуют по портам. - Контейнеры делят IPC namespace — могут использовать SystemV/POSIX shared memory.
- Контейнеры могут делить PID namespace — через
shareProcessNamespace: true(по умолчанию выключено). - Через volumes (особенно
emptyDir) контейнеры обмениваются файлами. - Pod создаётся целиком и удаляется целиком. Нельзя «передеплоить один контейнер в Pod» — это всегда новый Pod.
Когда несколько контейнеров в одном Pod — это правильно
Главный критерий — tight coupling. Контейнеры должны:
- разделять lifecycle (если один умирает — другой не имеет смысла без него);
- разделять данные через volume или localhost (а не через сеть к другому Pod);
- быть симбиотическими — основной процесс плюс «помощник», который без него бесполезен.
Канонические сценарии:
- log-shipper, читающий логи приложения с того же emptyDir;
- service mesh proxy (Envoy), через который ходит весь трафик приложения;
- init container, готовящий конфиг до старта приложения;
- adapter, transforming формат метрик приложения для Prometheus.
Когда несколько контейнеров — это антипаттерн
Если два процесса:
- независимы по lifecycle (можно перезапустить один без другого);
- общаются через HTTP/gRPC, а не localhost/volume;
- могут масштабироваться независимо;
— это разные Deployments + Services, а не один Pod.
«Положу backend и frontend в один Pod, чтобы они общались через localhost» — классическая ошибка. Backend и frontend должны масштабироваться независимо, иметь отдельные образы и rolling updates. Это разные Deployments. Один Pod — это про процессы, которые не имеют смысла друг без друга.
4 классических паттерна
Литература по Kubernetes (от Brendan Burns и далее) определяет 4 канонических паттерна multi-container Pod:
В реальности граница между sidecar/adapter/ambassador часто размывается. Envoy в service mesh — это технически ambassador (proxy для outbound), но в реализации это sidecar container. Fluent-bit, который читает логи и парсит их в JSON — это и sidecar (дополнение), и adapter (transform). Разделение полезно для проектирования, но имена паттернов в манифесте Kubernetes не существует — есть только два механизма: initContainers и containers.
Killer момент: что такое Sidecar containers GA (v1.33)
Долгое время в Kubernetes sidecar не был отдельной сущностью — это был просто container в spec.containers рядом с main. И это порождало известные проблемы:
- При завершении main контейнер sidecar мог умереть раньше, чем main допишет логи → последние логи теряются.
- В Job sidecar (например, Envoy proxy) продолжает работать после завершения main → Pod зависает в Running, Job не Completed.
- Нет порядка старта: main может стартовать до того, как sidecar готов принять трафик.
Эволюция: alpha в v1.28, beta в v1.29 (default on), GA в v1.33 — новая форма sidecar: container в spec.initContainers с полем restartPolicy: Always. kubelet распознаёт его как sidecar и:
- стартует его до main containers (как init, но не ждёт его completion);
- считает sidecar Ready (через startup/readiness probe) перед стартом main containers;
- при завершении main containers останавливает sidecars — это решает Job problem;
- перезапускает sidecar по
restartPolicy: Alwaysпри падении в течение жизни Pod.
spec:
initContainers:
- name: log-shipper
image: fluent/fluent-bit
restartPolicy: Always # это и делает его native sidecar (GA с v1.33)
volumeMounts:
- mountPath: /var/log
name: app-logs
containers:
- name: app
image: my-app:1.0
volumeMounts:
- mountPath: /var/log
name: app-logs
volumes:
- name: app-logs
emptyDir: {}
С v1.33 GA (beta с v1.29, default on) — sidecar containers через initContainers + restartPolicy: Always — это рекомендованный способ для log shippers, service mesh proxies, secret rotators. Старый способ (просто container в spec.containers) работает, но имеет известные lifecycle проблемы. На CKAD v1.35 ожидайте вопросы про оба способа и их отличия.
Простой пример: app + log-shipper
Минимальный multi-container Pod с shared volume — приложение пишет логи в файл, sidecar их вычитывает и отправляет наружу.
apiVersion: v1
kind: Pod
metadata:
name: app-with-logs
spec:
initContainers:
- name: log-shipper
image: fluent/fluent-bit:3.0
restartPolicy: Always
volumeMounts:
- mountPath: /var/log/app
name: logs
containers:
- name: app
image: my-app:1.0
volumeMounts:
- mountPath: /var/log/app
name: logs
volumes:
- name: logs
emptyDir: {}
Здесь:
emptyDirсоздаётся при старте Pod, mount-ится в оба контейнера на/var/log/app;- log-shipper стартует первым (как sidecar в init), main app стартует после Ready log-shipper’а;
- main пишет логи → log-shipper их вычитывает и шлёт наружу;
- при terminate Pod main завершается → kubelet завершает sidecars → Pod Terminated.