Certified Kubernetes Application Developer (CKAD) #3 Multi-container Patterns: Init container, sidecar, ambassador, adapter
In #2 Pod and Container Lifecycle you got comfortable with the lifecycle and restart behavior of a single-container Pod. But in real work and on the exam, the case of multiple containers inside one Pod comes up often. This post organizes the representative collaboration patterns that emerge when several containers gather into a single Pod.
In Kubernetes’ Pod model, the containers inside a Pod share the same network namespace and the same storage volumes. In other words, containers talk to each other over localhost and can exchange files in the same directory. This sharing property is exactly what makes the four patterns — init container, sidecar, ambassador, and adapter — possible.
Init container: the setup work that must finish before the main container #
An init container is a container that runs before the main container starts and must complete to the end. If you define several, they run one at a time in the defined order, and each one must finish successfully before the next begins. Only after all init containers have completed do the regular containers start up.
The typical uses are as follows.
- Wait until a service the main app depends on (a database, a backend Service) is ready
- Run database schema migrations before the app starts
- Pre-download config files or static assets into a shared volume
- Perform privileged initialization separately from the main container
When an init container fails, the kubelet retries according to restartPolicy. Unless restartPolicy is Never, it keeps restarting until it succeeds, so the Pod stalls at the init stage and stays in an Init:Error or Init:CrashLoopBackOff state. To check an init container’s logs separately, specify the container name as in k logs myapp -c wait-for-db, and check the progress stage with k describe pod myapp.
Here is an init container example that waits until a backend Service comes up before the main container.
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- "until nslookup db-service; do echo waiting for db; sleep 2; done"
containers:
- name: app
image: nginx:1.27
ports:
- containerPort: 80initContainers is a separate field placed at the same spec level as containers. On the exam, the fastest flow is to build the Pod skeleton with k run myapp --image=nginx:1.27 $do > myapp.yaml and then add this field by hand.
Sidecar: the helper container that runs alongside the main one #
A sidecar is a helper container that keeps running side by side with the main container inside the same Pod. Unlike an init container, which finishes and disappears before the main one, a sidecar operates together with the main container for as long as it runs. The representative uses are log collection, metrics exposure, proxying, and config synchronization.
The most common form is the log-collection sidecar. When the main container writes log files to a shared volume, the sidecar reads the same volume and streams them to standard output or sends them to an external collector. The core of two containers sharing the same file is the emptyDir volume, which we cover later.
1.28+ native sidecar #
Starting with Kubernetes 1.28, a native way to declare a sidecar as an init container whose restartPolicy is Always was introduced (enabled by default in 1.29). An init container defined this way starts before the main container like an ordinary init container, but instead of terminating, it keeps running together with the main container and is cleaned up after the main one when the Pod shuts down. In other words, it suits cases where the sidecar must become ready before the main container. If the exam version is 1.28 or higher, it’s safer to know this form too.
apiVersion: v1
kind: Pod
metadata:
name: native-sidecar
spec:
initContainers:
- name: log-agent
image: busybox:1.36
restartPolicy: Always # this line turns the init container into a sidecar
command: ["sh", "-c", "tail -F /var/log/app/access.log"]
volumeMounts:
- name: logs
mountPath: /var/log/app
containers:
- name: app
image: nginx:1.27
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}Ambassador: abstracting outbound connections behind a local proxy #
The ambassador pattern is a structure in which the main container does not connect directly to external services, but instead communicates only over localhost through a proxy container in the same Pod. The main app always looks at nothing but localhost:port, and the ambassador container handles the complex connection logic — actual routing, retries, sharding, authentication.
Its purpose is to keep the main app’s connection code simple. For example, you can handle database read/write splitting or per-environment endpoint switching with ambassador configuration alone, without changing app code. From the app’s perspective, the outside always appears to be in one place.
Adapter: standardizing output format #
The adapter pattern is the reverse direction of the ambassador. If the ambassador abstracts the outbound connection, the adapter converts the main container’s output into the standard format an external system expects. The main app emits logs or metrics in its own way, and the adapter container processes them into a form the monitoring system can read.
The representative use is metrics exposure. When the app records its state in its own format, the adapter converts it into a format Prometheus can scrape and exposes it. Aligning each app’s idiosyncratic output to a single cluster standard is the adapter’s role.
Sharing between containers #
The foundation that makes these four patterns work is that the containers inside a Pod share resources. The two sharing mechanisms you use directly on the hands-on exam are these.
emptyDir volume sharing #
An emptyDir is a temporary volume that is created when the Pod is scheduled onto a node and exists until the Pod disappears. When several containers of the same Pod each mount this volume with their own volumeMounts, they share the same directory. A file written by one container can be read immediately by another, so both the log-collection sidecar and the init container’s file handoff rely on this.
shared process namespace #
Setting spec.shareProcessNamespace to true lets the containers inside a Pod also share the process namespace, so one container can see another container’s processes or send signals to them. It’s used in sidecars meant for debugging or process management.
YAML examples #
Init container and main container #
Here is an example where an init container drops a static page into a shared volume and the main nginx serves that volume.
apiVersion: v1
kind: Pod
metadata:
name: web-with-init
spec:
initContainers:
- name: fetch-content
image: busybox:1.36
command:
- sh
- -c
- "echo '<h1>ready</h1>' > /usr/share/nginx/html/index.html"
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
emptyDir: {}Sharing logs with a sidecar #
Here is an example where the main container writes logs to an emptyDir and the sidecar reads the same volume and streams them to standard output.
apiVersion: v1
kind: Pod
metadata:
name: app-with-logging
spec:
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- "while true; do echo \"$(date) request\" >> /var/log/app/access.log; sleep 3; done"
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: log-shipper
image: busybox:1.36
command:
- sh
- -c
- "tail -F /var/log/app/access.log"
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}Because the two containers mount the same logs volume, log-shipper reads and prints exactly the logs app leaves behind. You check the sidecar’s logs by container name, as in k logs app-with-logging -c log-shipper.
Exam points #
- Init containers go in
initContainers, notcontainers. Don’t get confused about the fact that it’s a separate field at the samespeclevel. - Init containers run in order, until they succeed. If a Pod is stuck in a state like
Init:0/2, check which init container is blocked withk describe pod. - For logs of a multi-container Pod, you must append
-c container-nameto see the container you want. Omit the name and it’s ambiguous which container’s logs you get. - Remember the two axes for sidecars and init containers: file sharing is
emptyDir, communication sharing islocalhost. - On 1.28 and higher, there is a native way to declare a sidecar as an init container with
restartPolicy: Always. Check the version in the question stem. - Specify the container when running a command inside it to verify behavior, too.
# Run a command inside a specific container
k exec app-with-logging -c app -- cat /var/log/app/access.logWrap-up #
What this post locked in:
- init container. A setup container that runs in order before the main one and must succeed before moving on. Used for waiting, migrations, and asset downloads
- sidecar. A helper container that keeps running together with the main one. Log collection and proxying are the representative uses. A 1.28+ native sidecar is an init container with
restartPolicy: Always - ambassador. Abstracts outbound external connections behind a local proxy. The app looks at nothing but
localhost - adapter. Converts the main container’s output into an external system’s standard format
- sharing mechanisms. Share files with an
emptyDirvolume, share the process namespace withshareProcessNamespace - checking logs. For a multi-container Pod,
k logs pod -c container-name
Next — Container Images #
You’ve gotten comfortable with how to arrange containers inside a Pod. The next question, then, is how the images those containers carry are built.
In #4 Container Images: Dockerfile, multi-stage, building by hand on the exam we’ll walk through the core Dockerfile instructions, the multi-stage build that shrinks image size, and the work of building an image by hand on CKAD, tagging it, and pushing it to a registry — following along step by step.