Multi-container: ambassador and adapter
Three roles for containers sharing a Pod, and the signal that tells you which one to pick.
Why this matters
A Pod is the smallest unit Kubernetes runs. Several containers can run inside one Pod. They aren't used to pack two unrelated services together: containers of the same app split roles between themselves.
Containers in one Pod always run on the same node, share the network, and can share volumes (a shared folder inside the Pod). Split them across two Pods, and you'll need a Service, a network request, and separate setup for shared storage between them.
There are three established patterns: sidecar, ambassador, adapter. Below is how they differ and what tells you which one to pick.
Three patterns
| Pattern | What it does | At what level | Example |
|---|---|---|---|
| sidecar | extends the main container | the general name for all three | log collector, monitoring agent |
| ambassador | hides the outside network behind 127.0.0.1 | network | nginx, envoy, haproxy |
| adapter | converts data to the format an outside system expects | data in a shared volume | JSON from main turned into plain text |
The test is simple. If the helper proxies the main container's network requests outward, it's an ambassador. If the helper translates the main container's data into someone else's format, it's an adapter. Any other helper is just called a sidecar.
Sidecar
A sidecar is a container next to the main one that extends it. That's the
general name. A sidecar doesn't have to listen on a port. It might read a
file with tail -F or periodically check the main container's state.
Examples: a log collector, a monitoring agent.
Sidecar has a dedicated way to write it: the container goes into
initContainers and gets restartPolicy: Always. This has been enabled by
default since Kubernetes 1.29, and stable since 1.33. A container like that
starts before the main ones, runs for the Pod's whole lifetime, and shuts
down after them.
initContainers:
- name: log-shipper
image: busybox:1.36.1
restartPolicy: Always
command: ["sh", "-c", "tail -F /var/log/app/app.log"]
Writing it as a regular containers entry still works, and you'll see
plenty of that in older manifests. The difference is in the guarantees.
kubelet starts regular containers in list order, but doesn't wait for the
previous one to become ready. When the Pod stops, all regular containers
get SIGTERM at the same time, and a sidecar can exit before the main
container. And in a Job, a sidecar like that keeps the Pod from finishing:
the main container is already done, but the helper keeps running.
Sidecar is covered in detail in the module's previous lesson.
Ambassador
An ambassador is a sidecar that hides the work of talking to the outside
network from the main container. The main container talks to
127.0.0.1:8080 and has no idea what's outside. The ambassador (usually
nginx, envoy, or haproxy) accepts the request on that port and proxies it
to the real Service.
That moves all communication with the outside world out of the main container. The Service's address changes, TLS (channel encryption) gets added, retry logic shows up. You fix that in the ambassador, the main container stays the same.
The request's full path:
main → 127.0.0.1:8080 (inside the Pod) → nginx-ambassador → Service auth-api
The nginx config is usually kept in a ConfigMap and mounted into the ambassador container:
apiVersion: v1
kind: ConfigMap
metadata:
name: ambassador-conf
data:
default.conf: |
server {
listen 8080;
location / {
proxy_pass http://auth-api.default.svc.cluster.local:80;
}
}
containers:
- name: main
image: busybox:1.36.1
command: ["sh", "-c", "while true; do wget -qO- http://127.0.0.1:8080/; sleep 5; done"]
- name: ambassador
image: nginx:1.27.3-alpine
volumeMounts:
- name: conf
mountPath: /etc/nginx/conf.d
volumes:
- name: conf
configMap:
name: ambassador-conf
nginx reads its config at startup. If you change the ConfigMap, the file
in the container updates after some delay (usually up to a minute), but a
running nginx won't reread it. The simplest fix is to recreate the Pod:
kubectl rollout restart.
Adapter
An adapter is a sidecar that converts the main container's data into the format an outside system expects. The main container writes to a file in whatever way is convenient for it, say, JSON. The adapter reads the same file from a shared volume and translates it, say, into plain text.
containers:
- name: main
image: busybox:1.36.1
command: ["sh", "-c", "while true; do echo '{\"level\":\"info\",\"msg\":\"tick\"}' >> /var/log/app/app.json; sleep 2; done"]
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: adapter
image: busybox:1.36.1
command: ["sh", "-c", "tail -F /var/log/app/app.json | awk -F'\"' '{print $4, $8}'"]
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
awk -F'"' splits the line on the double quote: the fourth field is the
value of level, the eighth is the value of msg.
kubectl logs <pod> -c adapter shows lines like info tick instead of
JSON.
Ambassador works at the network level, adapter translates the data format. That's the main difference between them.
What a Pod's containers share
A Pod's containers share the network. 127.0.0.1 is the same for all of
them, so their ports must be different. They also share volumes from
spec.volumes: each container mounts the volume it needs at its own path,
and paths can differ between containers.
Two containers listening on 8080, and there's only one 127.0.0.1 for the
whole Pod. The second one crashes with EADDRINUSE (address already in
use). In nginx's log it looks like this:
bind() to 0.0.0.0:8080 failed (98: Address in use).
Containers don't share their filesystem or their environment variables.
Each one has its own. By default containers also don't see each other's
processes: that's turned on with the field shareProcessNamespace: true
in the Pod spec.
Related commands
kubectl logs <pod> -c <container>
kubectl exec <pod> -c <container> -- sh
kubectl describe pod <pod> # all containers and their states
kubectl get pod <pod> -o jsonpath='{.spec.containers[*].name}'
kubectl get pod <pod> -o jsonpath='{.spec.volumes[*].name}'
kubectl explain pod.spec.shareProcessNamespace
Without -c, logs and exec pick the default container (the first one
in the containers list) and say so with the line
Defaulted container "...".
What usually breaks
The main container starts before the ambassador has opened its port. Then
the main container gets connection refused. Retry logic in its command
helps. Sidecar form is more reliable: an ambassador in initContainers
with restartPolicy: Always is guaranteed to start before the main
container, and with a startupProbe the main container waits until the
ambassador has opened its port.
The adapter can't find the main container's file. tail -F waits for the
file to appear on its own. For other commands, people add a wait at the
top: until [ -f /path ]; do sleep 1; done.
The adapter prints nothing even though the file is growing. A common cause:
output buffering. When output goes not to a terminal but to a pipe or a
container's log, sed batches lines and emits them in a chunk, and
busybox sed has no line-buffering flag. busybox awk emits every line
right away. In other awk implementations, people add fflush() after
print.
What you'll need for the tasks
- Reading logs and running commands in a specific container:
kubectl logs -c,kubectl exec -c. - The shared
127.0.0.1inside a Pod and container ports. - A ConfigMap with the nginx config and
kubectl rollout restartafter editing it. - A shared
emptyDirvolume:volumeson the Pod andvolumeMountson each container. - Sidecar form:
initContainerswithrestartPolicy: Always. - For the mini-project: everything from the module. Environment variables
from a ConfigMap and a Secret,
immutable: true,command/args, a waiting init container.
Homework
Practice the theory on your own cluster: 3 steps with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.
./run.sh start k8s-multi-container-patterns # set up the starting state
./run.sh verify k8s-multi-container-patterns # check the current step
In short
- Several containers in one Pod share the node, the network, and volumes, but not the filesystem or env.
- Sidecar is the general name for a helper container next to the main one.
- Ambassador hides the outside network behind
127.0.0.1, the main container only talks to localhost. - Adapter translates the main container's data into the format an outside system expects.
- Ports inside a Pod are shared, so two containers on the same port conflict.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 3 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson k8s-config-08-multi-container-patterns
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress