Multi-container Pods: init, sidecar и роли контейнеров
Обычно в Pod запускают один основной контейнер приложения. Это хорошее правило: один Pod - одна прикладная единица, которую Kubernetes планирует, перезапускает и удаляет целиком.
Multi-container Pod нужен не для того, чтобы положить несколько независимых сервисов в одно место. Он нужен, когда у приложения есть тесно связанный helper: контейнеры должны жить на одной node, делить localhost, общий volume или общий lifecycle.
Простой критерий:
контейнеры масштабируются и обновляются вместе -> можно думать про один Pod
контейнеры масштабируются и обновляются отдельно -> нужны разные workloads
Если backend и database имеют разные релизы, разные ресурсы и разное масштабирование, это разные Pod'ы и связь через Service. Если приложение пишет лог в файл, а соседний контейнер читает этот же файл и отправляет наружу, это уже нормальный кандидат на multi-container Pod.
Что общее внутри Pod
Контейнеры внутри одного Pod не становятся одним процессом. Но они получают несколько общих пространств и поэтому могут работать как одна прикладная единица.
| Общее внутри Pod | Практический смысл |
|---|---|
| IP-адрес Pod | контейнеры могут обращаться друг к другу через localhost |
| network namespace | порты внутри Pod должны не конфликтовать |
| volumes | контейнеры могут обмениваться файлами |
| lifecycle Pod | Pod планируется на одну node целиком |
Что остаётся отдельным:
- filesystem каждого container image;
- process namespace по умолчанию;
- env-переменные контейнера;
- command/args;
- securityContext контейнера;
- логи каждого контейнера.
Из-за этого в multi-container Pod почти всегда важно явно указывать имя контейнера:
kubectl logs app-pod -c app
kubectl logs app-pod -c helper
kubectl exec app-pod -c app -- printenv
Флаг -c выбирает контейнер внутри Pod. Pod один, но потоков логов и командных окружений несколько.
Технические роли контейнеров
В Kubernetes есть несколько технических мест, куда можно положить контейнер:
| Роль | Где описывается | Как живёт |
|---|---|---|
| app container | spec.containers |
основной долгоживущий процесс приложения |
| init container | spec.initContainers |
выполняется до app containers и должен завершиться |
| sidecar container | обычно spec.containers, а в современном Kubernetes может быть init container с restartPolicy: Always |
работает рядом с приложением |
Главная развилка:
нужно сделать один раз до старта -> init container
нужно работать всё время рядом -> sidecar container
Init и sidecar помогают не смешивать бизнес-код приложения с эксплуатационными задачами: подготовкой файлов, ожиданием зависимостей, отправкой логов, proxy, обновлением конфигов.
Архитектурные роли
Важно не смешивать механику Kubernetes и архитектурные названия.
init container и sidecar container - это способы запуска контейнеров внутри Pod. А ambassador, adapter, log shipper, metrics exporter, proxy - это смысловые роли, которые чаще всего реализуются как sidecar.
| Архитектурная роль | Что делает |
|---|---|
| ambassador | проксирует запросы приложения к внешней системе |
| adapter | преобразует формат логов, метрик или API |
| log shipper | читает локальные логи и отправляет их наружу |
| metrics exporter | переводит внутренние метрики в формат Prometheus |
| proxy | принимает или отправляет сетевой трафик рядом с app |
Init container
Init container запускается до основных контейнеров и должен успешно завершиться. Если init container падает, основной контейнер не стартует.
Это строгое поведение полезно: если подготовка не завершилась, запускать приложение опасно. Например, если init не создал конфиг или не выставил права на директорию, основной контейнер может стартовать в неправильном состоянии и начать отдавать ошибки пользователям.
Типичные задачи init:
| Задача | Пример |
|---|---|
| подготовить файл | сгенерировать config в shared volume |
| дождаться зависимости | проверить доступность API или Service |
| миграция/инициализация | создать директории, права, начальные данные |
| загрузить данные | скачать конфиг или шаблоны перед стартом app |
Пример Pod с init container и общим emptyDir:
apiVersion: v1
kind: Pod
metadata:
name: init-demo
spec:
initContainers:
- name: init-data
image: nginx:alpine
command: ["sh", "-c", "echo init done; echo init data > /data/init.txt"]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: app
image: nginx:alpine
command: ["sh", "-c", "cat /data/init.txt; sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
Здесь init container сначала записывает файл, затем завершается. Только после этого стартует контейнер app.
Разберём новые поля в этом манифесте:
| Поле | Что означает |
|---|---|
spec.initContainers |
список контейнеров, которые запускаются до обычных контейнеров |
spec.containers |
список основных контейнеров Pod'а |
name |
имя контейнера или volume; по нему Kubernetes связывает части манифеста |
image |
container image, из которого запускается контейнер |
command |
команда, которая заменяет default command из image |
volumeMounts |
список volume, которые нужно подключить внутрь конкретного контейнера |
mountPath |
путь внутри контейнера, где будет виден volume |
spec.volumes |
список volume, доступных Pod'у |
emptyDir: {} |
временный пустой volume, который создаётся вместе с Pod |
Самая важная связь здесь такая:
spec.volumes[].name: data
|
v
containers.volumeMounts.name: data
volumes объявляет volume на уровне Pod'а, а volumeMounts подключает этот volume в конкретный контейнер. Если два контейнера монтируют volume с одним и тем же name, они видят одну и ту же директорию, хотя путь mountPath внутри контейнеров может быть разным.
emptyDir в этом примере создаёт пустую директорию для Pod'а. Init container пишет туда файл через путь /data, а app container потом читает этот же файл через свой /data. Подробные типы volume и жизненный цикл данных будут в следующей статье, а пока достаточно запомнить: emptyDir живёт вместе с Pod и подходит для временного обмена файлами между контейнерами.
Проверка:
kubectl get pod init-demo
kubectl describe pod init-demo
kubectl logs init-demo -c init-data
kubectl logs init-demo -c app
В describe смотри секцию Init Containers. Если init падает, Pod может быть в статусе Init:CrashLoopBackOff, Init:0/1 или PodInitializing. Это не проблема основного контейнера: приложение ещё не запускалось.
Если init containers несколько, они идут строго в порядке списка:
spec:
initContainers:
- name: step1
image: nginx:alpine
command: ["sh", "-c", "echo step1 > /data/step1"]
volumeMounts:
- name: data
mountPath: /data
- name: step2
image: nginx:alpine
command: ["sh", "-c", "echo step2 > /data/step2"]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: app
image: nginx:alpine
command: ["sh", "-c", "cat /data/step1 /data/step2; sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
step2 не начнёт работу, пока step1 не завершится успешно. Контейнер app не начнёт работу, пока не завершатся оба init containers.
Sidecar container
Sidecar работает рядом с основным контейнером. Он не главный бизнес-процесс, а помощник: собирает логи, обновляет конфиг, проксирует трафик, экспортирует метрики.
Sidecar не должен быть способом спрятать второй самостоятельный сервис. Если контейнер имеет собственный lifecycle, отдельные релизы и масштабируется иначе, ему нужен отдельный workload. Sidecar оправдан, когда helper без app почти не имеет смысла.
Пример с общим volume:
apiVersion: v1
kind: Pod
metadata:
name: sidecar-demo
spec:
containers:
- name: app
image: nginx:alpine
command: ["sh", "-c", "touch /logs/app.log; while true; do echo request >> /logs/app.log; sleep 5; done"]
volumeMounts:
- name: logs
mountPath: /logs
- name: log-reader
image: nginx:alpine
command: ["sh", "-c", "touch /logs/app.log; tail -F /logs/app.log"]
volumeMounts:
- name: logs
mountPath: /logs
volumes:
- name: logs
emptyDir: {}
Контейнер app пишет строки в /logs/app.log, а log-reader читает тот же файл из общего emptyDir.
Логи конкретного контейнера:
kubectl logs sidecar-demo -c app
kubectl logs sidecar-demo -c log-reader
kubectl logs sidecar-demo --all-containers=true
--all-containers=true удобно для быстрой проверки, но для точной диагностики лучше выбирать контейнер через -c.
Exec в конкретный контейнер:
kubectl exec sidecar-demo -c app -- sh
kubectl exec sidecar-demo -c log-reader -- sh
Если забыть -c, kubectl может выбрать контейнер по умолчанию или попросить уточнить имя.
Когда multi-container Pod оправдан
Multi-container Pod хорошо подходит, когда helper без основного приложения почти не имеет смысла.
Примеры:
- init container готовит конфиг в общий volume перед стартом app;
- sidecar читает файл логов из общего volume;
- proxy-контейнер слушает
localhostрядом с приложением; - helper периодически обновляет файл, который читает app;
- exporter переводит внутренний формат метрик в формат Prometheus.
Здесь контейнеры образуют одну operational unit: если переносить приложение на другую node, helper должен ехать вместе с ним.
Когда лучше разные Pod'ы
Не стоит класть в один Pod frontend, backend, worker и database только потому, что они относятся к одному продукту.
Разделяй на разные workloads, если компоненты:
- масштабируются с разной скоростью;
- имеют разные rollout и rollback;
- требуют разных ресурсов;
- могут жить без друг друга;
- должны иметь отдельные Service, probes или policy.
Иначе Pod станет маленькой виртуальной машиной, где сложно обновлять и диагностировать отдельные части.
Диагностика
Сначала смотри общий статус Pod, потом состояние каждого контейнера.
kubectl get pod sidecar-demo
kubectl describe pod sidecar-demo
kubectl get pod sidecar-demo -o jsonpath='{.spec.containers[*].name}'
kubectl logs sidecar-demo -c app
kubectl logs sidecar-demo -c log-reader
Для init-проблем:
kubectl describe pod init-demo
kubectl logs init-demo -c init-data
Для sidecar-проблем Pod может выглядеть Running, но один контейнер может перезапускаться или не выполнять свою роль. Тогда проверяй логи обоих контейнеров, volume mounts и Events в describe.
У multi-container Pod диагностика почти всегда контейнерная: нужно явно понимать, какой контейнер пишет файл, какой читает, кто слушает порт и у кого ошибка.
Как принять решение
Перед тем как добавить второй контейнер в Pod, задай несколько вопросов:
- Нужен ли общий
localhost? - Нужен ли общий volume?
- Должны ли контейнеры всегда быть на одной node?
- Должны ли они масштабироваться одинаково?
- Что произойдёт, если helper упадёт?
- Должен ли helper завершиться до старта app или жить рядом с ним?
Если ответы показывают тесную связь, multi-container Pod оправдан. Если связь слабая, лучше разные workloads и нормальная связь через Service.
Перед лабораторной запомни главное:
init container -> подготовить и завершиться
sidecar container -> помогать во время работы app
ambassador/adapter -> архитектурные роли, обычно реализуются sidecar'ом
