Sign in
← Kubernetes: configuration
6 min · medium · Lesson 8 of 8

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

PatternWhat it doesAt what levelExample
sidecarextends the main containerthe general name for all threelog collector, monitoring agent
ambassadorhides the outside network behind 127.0.0.1networknginx, envoy, haproxy
adapterconverts data to the format an outside system expectsdata in a shared volumeJSON 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.

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

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

Done reading?

Mark the lesson as read to move your track progress forward.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-config-08-multi-container-patterns