Wanneer een pod meer dan een container moet bevatten
Een Kubernetes pod is een groep van een of meer containers die samen op dezelfde node worden gepland en een netwerk-namespace en (optioneel) volumes delen. De meeste workloads zijn een container per pod. De patronen in dit artikel bestaan voor de paar gevallen waarin dat niet zo is.
De vuistregel: zet twee containers in dezelfde pod als ze strak gekoppeld zijn en hun lot delen. Ze starten samen, worden samen gepland, schalen samen en kunnen onderling praten via localhost zonder netwerk-hop. Kun je je voorstellen dat ze op verschillende nodes draaien zonder dat er iets stuk gaat? Dan horen ze in aparte pods.
Vier patronen domineren het multi-container ontwerp:
- Init containers voor opgesomd opstartwerk dat klaar moet zijn voor de app draait.
- Sidecar containers voor hulpprocessen die de hele pod-levensduur naast de app draaien.
- Ambassador containers voor het proxyen van uitgaand netwerkverkeer namens de app.
- Adapter containers voor het normaliseren van de output van de app (logs, metrics) naar een formaat dat andere systemen verwachten.
De rest van dit artikel loopt door elk patroon, geeft productiewaardige YAML, en legt uit hoe de native sidecar feature die stable werd in Kubernetes 1.33 de resource accounting math veranderde.
Wat een pod echt deelt tussen containers
Begrijpen wat een pod deelt is de basis voor elk patroon. Containers in dezelfde pod delen:
- De netwerk-namespace van de pod. Elke container ziet dezelfde loopback-interface en deelt het pod-IP. Twee containers in dezelfde pod praten met elkaar via
127.0.0.1:<poort>, niet via een Service. Ze kunnen niet dezelfde poort binden. Dit staat in de pod-conceptpagina: "Pods natively provide two kinds of shared resources for their constituent containers: networking and storage." - Volumes die op pod-niveau worden gemount. De meeste multi-container patronen geven data tussen containers door via een
emptyDir-volume. Alle containers in de pod kunnen het mounten, vaak op verschillende paden. De emptyDir-documentatie is duidelijk: "All containers in the Pod can read and write the same files in theemptyDirvolume." - Dezelfde node. De pod is de scheduling-eenheid, dus elke container in de pod komt op dezelfde node terecht en overleeft of sterft als groep.
- Optionele process-namespace. Zet je
shareProcessNamespace: trueop de pod spec, dan ziet elke container alle processen van de andere containers. Handig voor debug-sidecars, gevaarlijk als default.
Containers in dezelfde pod delen standaard geen MNT-namespace. Elke container heeft zijn eigen filesystem uit zijn eigen image; het enige sharing op filesystem-niveau is via gemounte volumes.
Het emptyDir-volume is het werkpaard. De levensduur is gelijk aan die van de pod: zodra de pod weg is, is de inhoud weg. Container-crashes vernietigen de inhoud niet. Zet je medium: Memory, dan wordt het volume gebackt door tmpfs in plaats van disk. Dat houdt secrets buiten node-storage, maar telt de inhoud van het volume mee tegen de memory limit van de container.
Patroon 1: Sidecar (klassiek vs native)
Een sidecar draait continu naast de hoofdapp: een log shipper die bestanden naar Loki tailt, een metrics exporter die een Unix socket scrapet, een Envoy-proxy die verkeer onderschept voor een service mesh.
Klassieke sidecar (extra reguliere container)
Het klassieke patroon is de sidecar als tweede entry in containers declareren:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: app # hoofdcontainer, schrijft naar /var/log/app
image: mycompany/web:1.6.0
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: log-shipper # klassieke sidecar, tailt hetzelfde volume
image: grafana/promtail:3.4.0
args:
- "-config.file=/etc/promtail/promtail.yaml"
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: promtail-config
mountPath: /etc/promtail
volumes:
- name: logs
emptyDir: {}
- name: promtail-config
configMap:
name: promtail-config
Beide containers starten parallel. Er is geen ordergarantie bij startup. De hoofdcontainer kan logbestanden produceren voordat de shipper klaar is om te lezen, en bij shutdown kan de shipper gekild worden voordat hij de laatste regels heeft geflusht. Voor een Deployment is dat meestal prima. Voor een Job is het een probleem: een sidecar in containers houdt de pod oneindig in leven, want de Job-controller wacht tot elke container exit, en een tailende log shipper doet dat nooit.
Native sidecar (init container met restartPolicy: Always)
Het native sidecar patroon, stable sinds Kubernetes 1.33, codeert de sidecar als een speciale init container met restartPolicy: Always:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
initContainers:
- name: log-shipper # native sidecar
image: grafana/promtail:3.4.0
restartPolicy: Always # dit maakt het een sidecar
args:
- "-config.file=/etc/promtail/promtail.yaml"
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: promtail-config
mountPath: /etc/promtail
startupProbe:
httpGet:
path: /ready
port: 3101
failureThreshold: 30
periodSeconds: 2
containers:
- name: app
image: mycompany/web:1.6.0
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
- name: promtail-config
configMap:
name: promtail-config
Wat de native vorm verandert:
- Startup-volgorde. De kubelet start eerst de sidecar, wacht tot de
startupProbeslaagt, en start dan pas de app-container. De app ziet het volume nooit half geinitialiseerd. - Probe-support. Native sidecars ondersteunen
startupProbe,readinessProbeenlivenessProbe. Klassieke sidecars-als-extra-container ondersteunen ook probes (het zijn gewone containers), maar bij native sidecars gate de startup-probe ook de start van de hoofdcontainer. - Termination-volgorde. Bij pod-shutdown krijgen hoofdcontainers eerst SIGTERM, daarna sidecars. De log shipper heeft tijd om te flushen nadat de app is gestopt.
- Job-completion. Een native sidecar blokkeert Job-completion niet. De Job-controller behandelt alleen de entries in
containersals completion-signaal, dus een tailende log shipper kan eindelijk in dezelfde pod als een one-shot batch container. - OOM score. Native sidecars krijgen hun kernel OOM-score afgestemd op die van de hoofdcontainer, dus ze worden niet preferent gekild bij memory pressure.
Zit je op Kubernetes 1.29 of nieuwer, gebruik dan native sidecars. De SidecarContainers feature gate staat standaard aan sinds 1.29, en de API is identiek.
Patroon 2: Init container
Init containers draaien voordat de app start, sequentieel, tot voltooiing. Elke moet exit 0 voordat de volgende start, en de hoofdcontainer start pas als alle init containers klaar zijn. Gebruik ze voor eenmalige setup: wachten op een dependency, een databasemigratie draaien, configuratie ophalen in een gedeeld volume, filesysteem-permissies fixen.
spec:
initContainers:
- name: wait-for-postgres
image: busybox:1.37 # getest met busybox 1.37
command:
- sh
- -c
- |
until nc -z postgres-service 5432; do
echo "wachten op postgres..."
sleep 2
done
containers:
- name: api
image: mycompany/api:3.2.0
Init containers ondersteunen geen liveness-, readiness- of startup-probes. Ze zijn geen langlopende processen. Heb je een proces nodig dat blijft draaien maar de hoofdcontainer blokkeert van starten, dan wil je een native sidecar met een startupProbe, geen init container.
De volledige behandeling, inclusief patronen voor databasemigraties, het emptyDir config-fetching patroon, en de concurrency-val als een Deployment opschaalt naar meerdere replicas, staat in Kubernetes init containers.
Patroon 3: Ambassador (uitgaand verkeer proxyen)
Het ambassador-patroon zet een proxy-container naast de app om uitgaande netwerkcomplexiteit te regelen. De app praat plain HTTP naar localhost. De ambassador regelt TLS, retries, circuit breaking, service discovery of sharding naar een downstream-systeem.
Een veelvoorkomend geval is een sharded Redis. De app wil niets weten van hash slots en replica-topologie; hij wil een enkel Redis-endpoint. De ambassador draait twemproxy en de app praat met localhost:6379:
spec:
containers:
- name: app
image: mycompany/api:3.2.0
env:
- name: REDIS_URL
value: "redis://localhost:6379" # altijd lokaal, nooit sharded
- name: redis-ambassador
image: kuiken/twemproxy:0.5
args:
- "-c"
- "/etc/twemproxy/nutcracker.yaml"
ports:
- containerPort: 6379
volumeMounts:
- name: nutcracker-config
mountPath: /etc/twemproxy
volumes:
- name: nutcracker-config
configMap:
name: nutcracker-config
Hetzelfde patroon zie je overal waar een service mesh in het spel is. Istio's Envoy-sidecar, Linkerd's linkerd2-proxy en Consul Connect proxies zijn allemaal ambassadors. De app ziet een platte localhost-wereld; de ambassador regelt mTLS, traffic policy en retries.
Wanneer ambassador de juiste keuze is:
- Het downstream-systeem heeft connectiebeheer-complexiteit (sharding, leader election, mTLS) die je niet in elke taalclient van je hele fleet wilt stoppen.
- Je wilt de proxy onafhankelijk van de app upgraden.
- De app is third-party en je kunt hem niet aanpassen.
Wanneer ambassador de verkeerde keuze is:
- De proxy wordt een per-pod-bottleneck. Een enkele Envoy-sidecar op een pod met een drukke app-instance is prima; een enkele Envoy-sidecar als gedeelde egress-proxy voor veel apps is een andere architectuur (een aparte proxy-Deployment, geen sidecar).
- Het latency-budget kan een extra hop niet hebben. Localhost is snel, maar niet gratis; je betaalt nog steeds TCP-setup, serialisatie en een context switch.
Patroon 4: Adapter (output van de app normaliseren)
Het adapter-patroon is het omgekeerde van ambassador. Waar ambassador uitgaand verkeer herschrijft, herschrijft adapter uitgaande observability-state: logs, metrics, status-endpoints. De app produceert zijn eigen formaat; de adapter vertaalt dat naar wat de rest van het platform verwacht.
Het canonieke geval is een metrics-exporter:
spec:
containers:
- name: legacy-app
image: mycompany/legacy:5.4.0
# exposeert /jolokia, een JMX-over-HTTP endpoint specifiek voor JVM-apps
ports:
- containerPort: 8778
name: jolokia
- name: jmx-exporter
image: bitnami/jmx-exporter:1.0.1
args:
- "5556"
- "/etc/jmx-exporter/config.yaml"
ports:
- containerPort: 5556
name: prom-metrics # wat het platform verwacht te scrapen
volumeMounts:
- name: exporter-config
mountPath: /etc/jmx-exporter
volumes:
- name: exporter-config
configMap:
name: jmx-exporter-config
Prometheus scrapet :5556. De legacy app blijft JMX produceren. De adapter zit ertussen. Upgrade je de app, dan blijft het adapter-contract stabiel; verander je Prometheus-configuratie, dan merkt de app er niets van.
Andere adapter-use cases:
- Een Fluent Bit sidecar die container-logs van
stdoutleest en als gestructureerde JSON wegschrijft. - Een kleine webserver die een uniforme
/healthzen/readyzaanbiedt door meerdere legacy app-endpoints te aggregeren. - Een protocol-vertaler die gRPC exposeert voor een HTTP-only legacy app, of andersom.
Het adapter-patroon is ook waar pods in de problemen komen. Elke adapter is glue code die met beide kanten in sync moet blijven. Verandert de legacy app zijn /jolokia-schema en wordt de JMX-exporter niet bijgewerkt, dan verdwijnen metrics stil. Behandel de adapter als een apart geversionde dependency, geen vergeten implementatiedetail.
Native sidecar containers (Kubernetes 1.29+ beta default-on, 1.33 stable)
Native sidecars verdienen een eigen sectie omdat hun lifecycle en resource accounting subtiel anders zijn.
Lifecycle
De kubelet start containers in deze volgorde:
- Volume mounts en pod-level setup.
- Native sidecars (init containers met
restartPolicy: Always), in declaratievolgorde. De volgende sidecar start zodra destartupProbevan de vorige slaagt (of meteen, als geen probe is gedefinieerd). - Reguliere init containers, in declaratievolgorde, elk volledig draaiend voor de volgende start.
- Hoofdcontainers in
containers, allemaal parallel gestart.
Bij termination draait de volgorde om. Hoofdcontainers krijgen eerst SIGTERM; zodra die klaar zijn, krijgen sidecars SIGTERM, en daarna (na de grace period) SIGKILL. De officiele documentatie merkt op dat andere exit codes dan nul voor sidecars normaal zijn tijdens pod-termination en in het algemeen genegeerd moeten worden, omdat de exit van de sidecar wordt afgedwongen door de kubelet, niet graceful is.
Wanneer de feature gate ertoe doet
De SidecarContainers feature gate heeft de volgende historie, volgens de native sidecar containers blogpost en de v1.33 release announcement:
- Alpha in Kubernetes 1.28 (augustus 2023). Standaard uit; niet voor productie.
- Beta in Kubernetes 1.29. Standaard aan.
- Stable in Kubernetes 1.33 (23 april 2025). De gate staat vast aan en gaat eruit.
Zit je op een managed Kubernetes-service (EKS, GKE, AKS), check dan de gerapporteerde clusterversie voordat je op native sidecars rekent. EKS, GKE en AKS ondersteunen de feature inmiddels op elke supported minor release, maar een self-managed cluster vastgepind op 1.27 of ouder moet upgraden of het klassieke patroon gebruiken.
Resource requests, limits en HPA in multi-container pods
Hier verschillen de patronen op manieren die je in productie pijn doen.
Effective request math
De Kubernetes init container documentatie definieert twee getallen:
- Effective init request voor een resource: de hoogste enkele request van alle init containers (omdat ze sequentieel draaien, telt maar een footprint tegelijk).
- Effective pod request voor een resource: de hoogste van (som van alle hoofdcontainer-requests) en (effective init request).
Voor een pod met klassieke init containers en geen native sidecars betekent dat:
effective_request = max(
max(initContainer[i].request),
sum(container[i].request)
)
Native sidecars veranderen de formule. Omdat een sidecar een init container is die in de hoofdfase blijft draaien, moet de request opgeteld worden bij de hoofdcontainer-som tijdens die fase. De effective request wordt:
# pod met native sidecars
effective_request = max(
max(InitContainerUse(i)), # max enkele request tijdens initfase
sum(sidecar.request) + sum(container.request) # beide fases samen
)
Waarbij InitContainerUse(i) rekening houdt met sidecars die al draaien tegen de tijd dat init container i start (elke volgende init container ziet de sidecars opgeteld bij zijn eigen request). Dit staat in de sidecar containers sectie over resource sharing.
Het praktische gevolg: een pod met drie native sidecars van 100m CPU per stuk plus een hoofdcontainer van 500m CPU heeft een effective request van 800m, niet 500m. De scheduler reserveert 800m op welke node ook de pod accepteert. Vergeet je dit en pak je nodes vol op basis van alleen de hoofdcontainer-request, dan krijg je overcommit.
HPA per-container scaling
Hebben pods asymmetrische containers (een drukke app en een bijna-idle log shipper), dan maskeren pod-niveau CPU-gemiddelden de waarheid. De HPAContainerMetrics feature gate en het ContainerResource metric type laten HPA schalen op de utilization van een specifieke container in plaats van het pod-gemiddelde:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 3
maxReplicas: 30
metrics:
- type: ContainerResource # niet "Resource"
containerResource:
name: cpu
container: app # naam van de container om te volgen
target:
type: Utilization
averageUtilization: 70
De tijdlijn voor HPAContainerMetrics: alpha in 1.20, beta default-on in 1.27, GA in 1.30. Op clusters ouder dan 1.27 moet de feature gate expliciet aangezet. Vanaf 1.30 staat hij vast aan en kan hij van de gate-lijst af.
Voor het volledige verhaal over HPA-configuratie, inclusief custom metrics en scaling behaviour, zie Kubernetes Horizontal Pod Autoscaler.
emptyDir Memory en de limit-accounting val
Gebruikt een patroon emptyDir met medium: Memory (een tmpfs-mount), dan tellen de bytes die naar dat volume worden geschreven mee tegen de memory limit van de container die ze schreef, niet tegen ephemeral storage. Een 200Mi configbestand dat door een init container in een memory-backed emptyDir wordt geschreven en daarna door een hoofdcontainer met 256Mi memory limit wordt gelezen, laat maar 56Mi over voor de heap van de hoofdcontainer. Dit is een veelvoorkomende verrassing.
Voor bredere richtlijnen over requests, limits, QoS-classes en overcommit-strategie, zie Kubernetes resource requests en limits.
Wat multi-container pods NIET zijn
Vijf dingen waarover multi-container pods vaak verkeerd worden begrepen.
Ze zijn geen manier om sidecars na de hoofdcontainer te laten starten. Sidecars zijn parallel, niet sequentieel. Een native sidecar start voor de hoofdcontainer, niet erna. Hangt je sidecar er echt van af dat de hoofdcontainer eerst up is (zeldzaam; meestal wil je het omgekeerde), dan heb je retry-logica op applicatieniveau nodig, geen Kubernetes-feature.
Ze geven de sidecar geen aparte netwerk-namespace. Elke container in de pod deelt de pod's netwerk-namespace. De sidecar ziet dezelfde 127.0.0.1 en dezelfde loopback-interface als de hoofdcontainer. Twee containers kunnen niet dezelfde poort binden. Dacht je dat de sidecar zijn eigen netns had, dan had je de architectuur verkeerd gelezen.
Native sidecars in 1.29+ vervangen het klassieke patroon niet. Het klassieke patroon (sidecar in containers) werkt nog steeds precies hetzelfde. Native sidecars zijn opt-in. Kies native als je startup-ordering, probe-gating of Job-completion semantics nodig hebt; kies klassiek als je dat niet hebt, en bespaar de cognitieve overhead.
Multi-container pods schalen niet onafhankelijk per container. Replicas schalen de hele pod. Doet je sidecar 100x meer werk dan je hoofdcontainer, dan is het antwoord niet de pod opschalen, maar de sidecar uit de pod halen en in een eigen Deployment zetten met een Service ertussen.
Ze zijn geen vervanging voor goede architectuur. Ik heb teams gezien die vier containers in een pod stopten omdat de componenten "gerelateerd" waren. Niets aan gerelateerd zijn rechtvaardigt een multi-container pod. De rechtvaardiging is gedeelde lifecycle, gedeeld filesystem of gedeelde netwerk-namespace. Geldt geen van die drie, dan zijn aparte pods simpeler en ze schalen onafhankelijk.
Beslismatrix
Kies het patroon op basis van de dominante eis.
| Behoefte | Patroon | Waarom |
|---|---|---|
| Een setup-taak draaien die af moet zijn voor de app start | Init container | Sequentieel, run-to-completion semantiek |
| Een langlopende helper naast de app draaien | Sidecar (native als 1.29+) | Parallelle levensduur, probe-support, Job-vriendelijk |
| Uitgaand verkeer proxyen voor TLS, sharding, mesh | Ambassador | App ziet localhost als plat endpoint |
| Output van de app (logs, metrics) naar standaardformaat vertalen | Adapter | Glue tussen app en platformcontracten |
| App-startup blokkeren tot een sidecar klaar is | Native sidecar met startupProbe |
Init containers ondersteunen geen probes |
| Iets eenmalig draaien voor een batch en dan stoppen | Job met init container of Job met native sidecar | Klassieke sidecars blokkeren Job-completion |
| Een fleet-brede proxy met veel verkeer draaien | Aparte Deployment + Service | Sidecars schalen niet onafhankelijk |
| Twee niet-gerelateerde apps draaien die toevallig in hetzelfde team zitten | Aparte pods | "Gerelateerd" is geen multi-container pod-rechtvaardiging |
Multi-container pods debuggen
kubectl describe pod is het startpunt. De secties Init Containers:, Containers: en Events: rapporteren elke state apart.
Logs van een specifieke container:
# hoofdcontainer
kubectl logs <pod> -c app
# init of sidecar (sidecar staat in initContainers in de spec)
kubectl logs <pod> -c log-shipper
# vorige run na een crash
kubectl logs <pod> -c app --previous
Exec naar een specifieke container:
kubectl exec -it <pod> -c app -- /bin/sh
Lijken twee containers het oneens te zijn over de inhoud van een gedeeld volume, kijk dan aan beide kanten:
kubectl exec <pod> -c init-writer -- ls -la /shared
kubectl exec <pod> -c app -- ls -la /shared
Voor containers zonder shell (distroless images) gebruik je ephemeral debug containers. Voor pods die vasthangen voordat een container start, ligt het meestal aan image pull of volume mount; zie pods debuggen die vasthangen in ContainerCreating.
Waar je verder kijkt
- Kubernetes init containers voor het volledige init container playbook met databasemigraties, config ophalen en concurrency-vallen.
- Kubernetes resource requests en limits voor de onderliggende mechanismen van requests, limits en QoS-classes.
- Kubernetes Horizontal Pod Autoscaler voor per-container schalen via het
ContainerResourcemetric type.