Ravindra BagaleCourses & study guides मराठी

Chapter 59: Kubernetes Pods in Depth: Architecture, YAML Anatomy and Pod Types

59.4 Multi-container pods: shared network, shared volumes, adapter and ambassador

Why: the problem

Suppose we run the Swiggy order service. The app writes logs to a file, but our log system only reads JSON from stdout. We do not want to change the app code. Another day, the app must talk to a Redis cluster with three addresses and TLS. The developers want to just call localhost:6379. Both problems have the same answer: put a second, small container in the same pod. It shares the pod's network and can share a folder with the app. In this lesson we build three multi-container pods by hand: a shared-volume pod, an adapter that changes the log format, and an ambassador that proxies to a remote service. Then we break them on purpose: a port clash, a missing volume and one container that crashes the whole pod.

What: every term in plain English

  • Multi-container pod — A pod with two or more containers that must live, move and scale together.
  • Shared network namespace — All containers in a pod share one IP and one localhost. They talk over localhost:port.
  • Shared volume — A volume, often emptyDir, mounted into more than one container so they can share files.
  • emptyDir — An empty folder created when the pod starts and deleted when the pod is removed.
  • Adapter pattern — A helper container that changes the app's output, such as log or metric format, into a standard one.
  • Ambassador pattern — A helper container that acts as a local proxy to an outside service, so the app only calls localhost.
  • Sidecar pattern — A helper that adds a feature beside the app, such as log shipping. Lesson 59.6 covers it in depth.
  • -c flag — kubectl logs and kubectl exec need -c container-name when a pod has more than one container.
  • READY 2/2 — kubectl get pods shows how many containers in the pod are ready out of the total.
  • Port clash — Two containers in one pod cannot listen on the same port, because they share the network.
Ask: must they live together? Pick ports. Add a shared emptyDir. Write each container. Apply and check READY. Read each container. Test failure.
One pod, two or three containers: shared localhost and shared folder

How: step by step

  1. Ask: must they live together? Put containers in one pod only if they share files or localhost and scale as one unit.
  2. Pick ports. Give each container its own port, for example app 8080 and proxy 6379.
  3. Add a shared emptyDir. Declare it under spec.volumes, then mount it in each container.
  4. Write each container. Each gets its own image, command, resources and probes.
  5. Apply and check READY. kubectl get pod should show 2/2 or 3/3.
  6. Read each container. kubectl logs pod -c name, kubectl exec pod -c name.
  7. Test failure. Kill one container and watch the restart count of only that container.

Example 1: Two containers share a folder and localhost

Goal. Two containers share a folder and localhost.

shared-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: web-writer
  namespace: shop
  labels:
    app: web-writer
spec:
  volumes:
    - name: html
      emptyDir: {}
  containers:
    - name: web
      image: nginx:1.27
      ports:
        - containerPort: 80
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      resources:
        requests: {cpu: 50m, memory: 32Mi}
        limits: {memory: 64Mi}
    - name: writer
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - while true; do
            echo "Order count at $(date): $RANDOM" > /data/index.html;
            sleep 5;
          done
      volumeMounts:
        - name: html
          mountPath: /data
      resources:
        requests: {cpu: 10m, memory: 16Mi}
        limits: {memory: 32Mi}

What each line does

  • volumes.html emptyDir — One empty folder for the whole pod. It lives as long as the pod lives.
  • web mountPath /usr/share/nginx/html — nginx serves files from this folder.
  • writer mountPath /data — The same folder, seen at a different path inside the writer.
  • while true ... sleep 5 — The writer rewrites index.html every 5 seconds.
  • resources per container — Each container has its own requests and limits. The pod total is the sum.

Green output

$ kubectl apply -f shared-pod.yaml
pod/web-writer created
$ kubectl get pod web-writer -n shop
NAME         READY   STATUS    RESTARTS   AGE
web-writer   2/2     Running   0          8s
$ kubectl exec -n shop web-writer -c writer -- wget -qO- http://localhost:80
Order count at Sun Oct 25 06:10:05 UTC 2026: 18342
$ kubectl exec -n shop web-writer -c web -- cat /usr/share/nginx/html/index.html
Order count at Sun Oct 25 06:10:10 UTC 2026: 2291

Expected result. The writer reaches nginx on localhost, because both share one network. Both see the same file, because both mount the same emptyDir.

Example 2: Adapter: turn plain log lines into JSON without touching the app

Goal. Adapter: turn plain log lines into JSON without touching the app.

adapter-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: orders-adapter
  namespace: shop
spec:
  volumes:
    - name: logs
      emptyDir: {}
  containers:
    - name: orders
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - i=0; while true; do
            i=$((i+1));
            echo "ORDER $i PUNE 250" >> /var/log/app/orders.log;
            sleep 3;
          done
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
    - name: json-adapter
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - touch /logs/orders.log;
          tail -F /logs/orders.log | while read word id city amount; do
            echo "{\"event\":\"order\",\"id\":$id,\"city\":\"$city\",\"amount\":$amount}";
          done
      volumeMounts:
        - name: logs
          mountPath: /logs
          readOnly: true

What each line does

  • orders container — Stands in for an old app that writes plain lines to a file.
  • echo ... >> orders.log — Appends a line like ORDER 7 PUNE 250.
  • tail -F — Follows the file, even if it is rotated or created later.
  • while read word id city amount — Splits each line into four words.
  • echo JSON — Prints one JSON object per line to stdout, which kubectl logs and Fluent Bit can read.
  • readOnly: true — The adapter can read the logs but never change them.

Green output

$ kubectl apply -f adapter-pod.yaml
$ kubectl logs -n shop orders-adapter -c json-adapter --tail=3
{"event":"order","id":5,"city":"PUNE","amount":250}
{"event":"order","id":6,"city":"PUNE","amount":250}
{"event":"order","id":7,"city":"PUNE","amount":250}
$ kubectl logs -n shop orders-adapter
Defaulted container "orders" out of: orders, json-adapter

Expected result. The app is unchanged, but the outside world now gets clean JSON. Without -c, kubectl picks the first container and warns you.

Example 3: Ambassador: the app calls localhost, the proxy talks to the real service

Goal. Ambassador: the app calls localhost, the proxy talks to the real service.

redis.yaml

apiVersion: v1
kind: Pod
metadata:
  name: redis-main
  namespace: shop
  labels:
    app: redis-main
spec:
  containers:
    - name: redis
      image: redis:7.2
      ports:
        - containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
  name: redis-main
  namespace: shop
spec:
  selector:
    app: redis-main
  ports:
    - port: 6379

ambassador-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: cart
  namespace: shop
spec:
  containers:
    - name: cart
      image: redis:7.2
      command: ["sh", "-c", "sleep infinity"]
      env:
        - name: REDIS_HOST
          value: "127.0.0.1"
        - name: REDIS_PORT
          value: "6380"
    - name: redis-ambassador
      image: alpine/socat:1.8.0.0
      args:
        - TCP-LISTEN:6380,fork,reuseaddr
        - TCP:redis-main.shop.svc.cluster.local:6379
      ports:
        - containerPort: 6380

What each line does

  • Service redis-main — Gives the Redis pod a stable DNS name inside the cluster (lesson 62.2).
  • REDIS_HOST 127.0.0.1 — The app only knows localhost. It never sees the real address.
  • socat TCP-LISTEN:6380,fork — Listens on 6380 inside the pod and opens a new link for each client.
  • TCP:redis-main...:6379 — Forwards every byte to the real Redis service.
  • Why 6380 and not 6379 — The cart image also contains Redis tools; using a different port avoids any clash.

Green output

$ kubectl apply -f redis.yaml -f ambassador-pod.yaml
$ kubectl exec -n shop cart -c cart -- redis-cli -h 127.0.0.1 -p 6380 SET cart:42 "2 dosa"
OK
$ kubectl exec -n shop redis-main -- redis-cli GET cart:42
"2 dosa"

Expected result. The app wrote to localhost, and the data landed in the real Redis. To move Redis to ElastiCache later, change only the ambassador args.

Every command, step by step (kind lab with kubectl)

  1. Create the kind cluster

    kind create cluster --name ch59 --config kind-3node.yaml
    kubectl create namespace shop
    

    What it does: One control plane and two workers, as in lesson 59.1.

    Creating cluster "ch59" ...
    namespace/shop created
    
  2. Apply the shared pod

    kubectl apply -f shared-pod.yaml
    kubectl get pod web-writer -n shop -o wide
    

    What it does: Both containers start on the same node, always.

    NAME         READY   STATUS    RESTARTS   AGE   IP           NODE
    web-writer   2/2     Running   0          6s    10.244.1.7   ch59-worker
    
  3. List container names

    kubectl get pod web-writer -n shop -o jsonpath='{.spec.containers[*].name}{"\n"}'
    

    What it does: You need these names for -c.

    web writer
    
  4. Shell into one container

    kubectl exec -it -n shop web-writer -c writer -- sh
    ls /data
    exit
    

    What it does: Each container has its own filesystem, but the shared folder is the same.

    index.html
    
  5. Check one IP for both

    kubectl exec -n shop web-writer -c writer -- hostname -i
    kubectl exec -n shop web-writer -c web -- hostname -i
    

    What it does: Same IP, because there is one network namespace per pod.

    10.244.1.7
    10.244.1.7
    
  6. Logs from every container

    kubectl logs -n shop orders-adapter --all-containers --prefix --tail=2
    

    What it does: --prefix shows which container wrote each line.

    [pod/orders-adapter/json-adapter] {"event":"order","id":12,"city":"PUNE","amount":250}
    
  7. Kill one container

    kubectl exec -n shop web-writer -c web -- sh -c 'kill 1'
    kubectl get pod web-writer -n shop
    

    What it does: Only the nginx container restarts. The pod and its IP stay.

    NAME         READY   STATUS    RESTARTS     AGE
    web-writer   2/2     Running   1 (3s ago)   4m
    
  8. See per-container restarts

    kubectl get pod web-writer -n shop -o jsonpath='{range .status.containerStatuses[*]}{.name}={.restartCount} {end}{"\n"}'
    

    What it does: Shows exactly which container restarted.

    web=1 writer=0
    
  9. Clean up

    kubectl delete pod -n shop web-writer orders-adapter cart redis-main
    kubectl delete svc -n shop redis-main
    

    What it does: Keep the cluster for lesson 59.5.

    pod "web-writer" deleted
    

When it is red 1

NAME       READY   STATUS             RESTARTS
two-webs   1/2     CrashLoopBackOff   4
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)

Cause. Two containers in one pod tried to listen on port 80. They share one network, so the second fails.

Fix, step by step

  1. Give each container a different port.
  2. For nginx, change listen in its config, for example to 8081.
  3. Apply again and check READY 2/2.

When it is red 2

The Pod "web-writer" is invalid: spec.containers[1].volumeMounts[0].name: Not found: "html"

Cause. A container mounts a volume that is not declared under spec.volumes, or the name has a typo.

Fix, step by step

  1. Check spec.volumes names.
  2. Make volumeMounts.name match exactly.
  3. Apply again.

When it is red 3

error: a container name must be specified for pod orders-adapter, choose one of: [orders json-adapter]

Cause. kubectl exec on a multi-container pod without -c in older kubectl, or when no default is set.

Fix, step by step

  1. Add -c json-adapter.
  2. Or set the annotation kubectl.kubernetes.io/default-container: orders.

When it is red 4

READY 1/2   Running
Readiness probe failed: dial tcp 127.0.0.1:6380: connect: connection refused

Cause. The helper container is not ready, so the whole pod is not ready and gets no Service traffic.

Fix, step by step

  1. Read logs of the helper with -c.
  2. Fix its target address or port.
  3. Remember: pod Ready means every container is Ready.

Practice tasks

Easy Task 1: Shared pod

Why: See sharing.

What to do: Apply web-writer and read the page from the writer.

How:
  1. wget localhost.
Solution
kubectl exec -n shop web-writer -c writer -- wget -qO- http://localhost

Easy Task 2: Names

Why: Use -c.

What to do: Print the container names of web-writer.

How:
  1. jsonpath on spec.containers.
Solution
kubectl get pod web-writer -n shop -o jsonpath='{.spec.containers[*].name}'

Easy Task 3: One IP

Why: Prove shared network.

What to do: Print hostname -i from both containers.

How:
  1. exec twice with -c.
Solution
kubectl exec -n shop web-writer -c web -- hostname -i

Medium Task 4: Adapter

Why: Change log format.

What to do: Run orders-adapter and read JSON logs.

How:
  1. Apply adapter-pod.yaml.
  2. logs -c json-adapter.
Solution
kubectl logs -n shop orders-adapter -c json-adapter -f

Medium Task 5: Default container

Why: Less typing.

What to do: Add the default-container annotation to orders-adapter.

How:
  1. metadata.annotations.
Solution
annotations:
  kubectl.kubernetes.io/default-container: orders

Medium Task 6: Ambassador

Why: Proxy pattern.

What to do: Write cart:1 through the ambassador and read it from redis-main.

How:
  1. redis-cli -p 6380.
Solution
kubectl exec -n shop cart -c cart -- redis-cli -h 127.0.0.1 -p 6380 SET cart:1 idli

Medium Task 7: Port clash

Why: Learn the error.

What to do: Make a pod with two nginx containers and read the error.

How:
  1. Same port 80.
Solution
kubectl logs two-webs -c web2

Medium Task 8: Fix clash

Why: Fix it.

What to do: Move the second nginx to port 8081 using a ConfigMap config.

How:
  1. Mount default.conf.
Solution
listen 8081;

Hard Task 9: Restarts

Why: Per-container health.

What to do: Kill nginx in web-writer and show only it restarted.

How:
  1. kill 1.
  2. jsonpath restartCount.
Solution
kubectl get pod web-writer -n shop -o jsonpath='{range .status.containerStatuses[*]}{.name}={.restartCount} {end}'

Hard Task 10: Metrics adapter

Why: Real-world adapter.

What to do: Add an nginx-prometheus-exporter container that reads nginx stub_status on localhost.

How:
  1. Enable stub_status.
  2. Exporter on port 9113.
Solution
image: nginx/nginx-prometheus-exporter:1.3.0
args: ["--nginx.scrape-uri=http://localhost:8080/stub_status"]

Homework: try at home

Take one app you know that writes logs to a file and design a two-container pod with an adapter that prints JSON to stdout. Draw it and write the YAML.

Recap

  • Containers in one pod share one IP and localhost, and can share volumes.
  • Use a multi-container pod only when containers must live and scale together.
  • Adapter changes output format; ambassador proxies outside calls; sidecar adds a feature.
  • Use -c, --all-containers and --prefix with logs and exec.
  • One container crashing restarts only that container, but the pod is not Ready until all are.
  • Chapter 18.3 showed single pods; next, 59.5 covers init containers.

FAQ

Q. Should I put my app and database in one pod?

No. They scale differently and the database needs its own storage. Use two Deployments or a StatefulSet.

Q. Can containers in a pod see each other's processes?

Not by default. Set shareProcessNamespace: true if you need it, for example to debug.

Q. Is there a limit on containers per pod?

No hard limit, but keep it small: one app plus one to three helpers.

Q. Do helpers count for resources?

Yes. The scheduler adds up the requests of all containers.

Q. Is ambassador still used?

Service meshes like Istio and Linkerd do the same job with an automatic proxy.

Q. How do I scale only the helper?

You cannot. That is a sign the helper should be its own Deployment.

Interview questions

  1. When do you use a multi-container pod?

    Model answer: When helpers must share files or localhost with the app and share its life cycle, like log adapters, proxies and config reloaders.

  2. Explain sidecar, adapter and ambassador.

    Model answer: Sidecar adds a feature beside the app, adapter normalises output, ambassador proxies outbound connections.

  3. How do containers in one pod communicate?

    Model answer: Over localhost on different ports, or through a shared volume.

  4. A pod shows 1/2 Ready. What do you check?

    Model answer: kubectl describe pod for which container is not ready, then kubectl logs -c that container and its probe.

  5. Why not put two services in one pod?

    Model answer: They cannot scale or roll out separately, and one bad release affects both.

Quick quiz

Q1. How do two containers in one pod talk?

Over localhost, because they share one network namespace.

Q2. Which volume type is usually shared between containers?

emptyDir.

Q3. What does an ambassador do?

Proxies the app's localhost calls to an outside service.

Q4. What does an adapter do?

Changes the app's output into a standard format.

Q5. Which flag picks a container in kubectl logs?

-c container-name.

Samajla ka?