Sidecar pattern: классический и native (GA v1.33)
Sidecar — это контейнер, дополняющий main приложение функциональностью, которая полезна, но не входит в core ответственность app. Канонические примеры: log shipper (fluent-bit, fluentd), metrics exporter (node_exporter), service mesh proxy (Envoy, Linkerd-proxy), secret rotator (Vault Agent). Долгое время sidecar в Kubernetes не имел отдельной формы — это был просто container в spec.containers. Эволюция нативной поддержки: alpha v1.28, beta v1.29 (default on), GA v1.33 — через spec.initContainers с restartPolicy: Always. Это разрешило целый класс старых проблем.
Свой systemd-сервис: пишем .service unit для Python ETL daemon
Классический sidecar: до native sidecar API
# Старый, "классический" способ
apiVersion: v1
kind: Pod
metadata:
name: app-classic
spec:
containers:
- name: app
image: my-app:v1
volumeMounts:
- mountPath: /var/log/app
name: logs
- name: log-shipper # sidecar — просто рядом с main
image: fluent/fluent-bit:3.0
volumeMounts:
- mountPath: /var/log/app
name: logs
volumes:
- name: logs
emptyDir: {}
И app, и log-shipper — это spec.containers. kubelet их стартует одновременно, никакого ordering. Они оба должны быть Ready чтобы Pod был Ready. Если log-shipper падает — он рестартится по restartPolicy Pod (как обычный контейнер).
Звучит просто. Но в этом подходе есть три фундаментальных проблемы.
Проблема 1: Race conditions на старте
main и sidecar стартуют параллельно. Если main критически зависит от sidecar (например, не может работать без Envoy proxy), это race:
- main стартует, пытается резолвить
localhost:15001→ proxy ещё не bind на порт → connect refused → main падает → restart loop. - Envoy в service mesh — главный пострадавший. Istio до v1.18 решал это через
holdApplicationUntilProxyStarts(специальная аннотация и init container), но это workaround.
Проблема 2: Job не завершается
Самая болезненная проблема классического sidecar. Возьмём Job, который выполняет batch-задачу и имеет sidecar для отправки метрик/логов:
apiVersion: batch/v1
kind: Job
metadata:
name: nightly-etl
spec:
template:
spec:
restartPolicy: Never
containers:
- name: etl
image: my-etl:v1
command: ['python', 'etl.py']
- name: log-shipper
image: fluent/fluent-bit
# этот процесс никогда не завершается сам
main etl завершается успешно (exit 0). Но log-shipper — это long-running процесс. Он не знает, что main закончил. Он продолжает работать. И Pod никогда не переходит в Completed, потому что в spec.containers есть running контейнер. Job висит в фазе Running вечно.
Workarounds в старой эре: pre-stop hook на sidecar, который читает sentinel-файл от main; shared process namespace + kill из main; periodic checks через cron. Все они хрупкие.
Это главная причина, по которой native sidecar важен. Если у вас есть Job с классическим sidecar (логи, метрики, mesh proxy) — он зависает. С native sidecar — main exits → kubelet shuts down sidecars → Pod Completed. На CKAD точно ожидайте вопрос про это.
Проблема 3: Graceful shutdown без ordering
При delete Pod kubelet шлёт SIGTERM всем containers одновременно. Что происходит:
- main app получает SIGTERM, начинает доплоштать запросы и логи в файл;
- log-shipper получает SIGTERM одновременно и решает graceful exit — flush buffers и stop;
- main пишет последний батч логов в файл → но log-shipper уже остановился → последние логи теряются.
В service mesh симметрично: Envoy умирает раньше, чем main, и последние запросы не уходят через mesh.
Native sidecar containers: решение
История фичи: alpha в v1.28 (декабрь 2023), beta v1.29 — default on (январь 2024), GA v1.33 (апрель 2025). Механизм: контейнер объявляется в spec.initContainers с дополнительным полем restartPolicy: Always. kubelet распознаёт его как sidecar и применяет специальную логику.
apiVersion: v1
kind: Pod
metadata:
name: app-native-sidecar
spec:
initContainers:
- name: log-shipper
image: fluent/fluent-bit:3.0
restartPolicy: Always # это и делает sidecar
volumeMounts:
- mountPath: /var/log/app
name: logs
startupProbe:
httpGet:
path: /healthz
port: 2020
containers:
- name: app
image: my-app:v1
volumeMounts:
- mountPath: /var/log/app
name: logs
volumes:
- name: logs
emptyDir: {}
Что kubelet делает с native sidecar
Job problem решён
Native sidecar разрешает Job problem напрямую:
apiVersion: batch/v1
kind: Job
metadata:
name: nightly-etl
spec:
template:
spec:
restartPolicy: Never
initContainers:
- name: log-shipper
image: fluent/fluent-bit
restartPolicy: Always # native sidecar
containers:
- name: etl
image: my-etl:v1
command: ['python', 'etl.py']
etl завершается → kubelet видит «все main containers Completed» → отправляет SIGTERM в log-shipper → log-shipper flushes и exits → Pod Completed → Job Completed.
YAML differences: классический vs GA
# Классический sidecar (работает, но с lifecycle проблемами)
spec:
containers:
- name: app
image: my-app
- name: log-shipper # просто рядом
image: fluent-bit
# Native sidecar (GA v1.33, рекомендованный способ)
spec:
initContainers:
- name: log-shipper # в initContainers
image: fluent-bit
restartPolicy: Always # ключевое поле
containers:
- name: app
image: my-app
Внешне разница в одном поле. По поведению — это разные категории контейнеров с разным lifecycle.
Для CKAD v1.35 знайте оба способа и их отличия. На реальных production кластерах используйте native sidecar везде, где возможно, особенно для Job и CronJob.
Канонические sidecar use cases
| Use case | Sidecar | Что делает |
|---|---|---|
| Логи | fluent-bit, fluentd | читает app log file, шлёт в Loki/ES |
| Метрики | prometheus exporter | собирает app metrics, exposes /metrics |
| Service mesh | Envoy, Linkerd-proxy | проксирует весь in/out трафик, mTLS |
| Secrets | Vault Agent | refresh секретов из Vault в emptyDir |
| Backup | DB dumper | periodic dump БД в S3 |
| Health/Lifecycle | termination handler | catch shutdown signals, drain connections |
Resources в sidecar GA
В отличие от обычных init containers, native sidecar считается «работающим параллельно с main». Поэтому его resource requests суммируются с main containers в effective Pod request:
effective = max(
max(regular_init_i.request),
sum(sidecar_i.request) + sum(main_container_j.request)
)
Это логично: sidecar реально жрёт ресурсы всё время жизни Pod, scheduler должен это учесть.