Init containers and sidecars
The first lesson left a sentence hanging: a Pod can have more than one container. Time to go into it. The containers of a Pod share the network (same localhost) and can share volumes, and that enables two patterns you will see in any real cluster.
- An init container runs before the main containers, one after another, and must finish successfully for the Pod to continue. It is for preparing the ground: waiting for a dependency, migrating a database, downloading configuration.
- A sidecar accompanies the main container for its whole life: it collects its logs, acts as a proxy, refreshes a certificate.
And here is an important novelty that changes how sidecars are written: since recent versions of Kubernetes, a sidecar is an init container with restartPolicy: Always. It sounds odd and it makes all the sense in the world, as you will see in a moment.
Step 1: A Pod that waits for its dependency
Create app.yaml. It is a Pod with two init containers and one main container:
cat << 'EOF' > app.yaml
apiVersion: v1
kind: Pod
metadata:
name: app
labels:
app: app
spec:
initContainers:
- name: espera-backend
image: ghcr.io/iximiuz/labs/nginx:alpine
command: ["sh", "-c"]
args:
- |
until wget -q -T 2 -O /dev/null http://backend; do
echo "el backend todavía no responde, esperando..."
sleep 2
done
echo "backend disponible"
- name: sidecar-logs
image: ghcr.io/iximiuz/labs/nginx:alpine
restartPolicy: Always
command: ["sh", "-c"]
args:
- |
while true; do
echo "$(date) latido del sidecar" >> /var/log/app/app.log
sleep 5
done
volumeMounts:
- name: logs
mountPath: /var/log/app
containers:
- name: app
image: ghcr.io/iximiuz/labs/nginx:alpine
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}
EOF
The messages the containers print are in Spanish, as in the book: espera-backend (wait-for-backend) says "the backend isn't answering yet, waiting..." and then "backend available"; sidecar-logs writes "sidecar heartbeat" with the date.
The YAML, explained in questions and answers
Why is the sidecar in initContainers and not in containers?
Because a sidecar needs two guarantees a normal container does not give: starting before the application (a log collector that arrives late is of little use) and finishing after it (it is of little use if it leaves the last logs behind). Init containers already start in order and before anyone else; all that was missing was telling Kubernetes that this one must not finish. That is exactly what restartPolicy: Always means inside an init container: "don't wait for it to finish, this one stays".
And what is the real difference from putting it in containers, as was done before?
That a normal container has no startup order, and that in Jobs it prevented finishing (the Pod never completed because the sidecar kept running). The native sidecar solves both problems: it starts in order, it shuts down when the main container finishes, and its lifecycle does not count toward declaring a Job done.
What happens if an init container fails?
It is retried according to the Pod's restartPolicy, and the Pod does not move forward. You will see the phase Init:0/2 or Init:Error. It is a deliberate block: if the dependency is not there, starting the application makes no sense.
Do init containers run in parallel?
No, in order and one at a time, in the order they appear in the list. The containers in containers, on the other hand, all start at once.
How do the sidecar and the application talk to each other?
Through the two channels a Pod shares: the emptyDir volume both mount (that is how the logs of this example travel) and localhost, because they share the network namespace. A classic sidecar proxy intercepts traffic precisely because of that.
Apply it and watch the block, which is the interesting part:
kubectl apply -f app.yaml
kubectl get pod app
kubectl logs app -c espera-backend
The Pod stays in Init:0/2 and the init container repeats its refrain: nobody answers at http://backend. Of course, it does not exist yet.
Step 2: Bring up the backend and unblock the Pod
Create the Deployment and the Service the init container is waiting for. By now you can do it with your eyes closed:
kubectl create deployment backend --image=ghcr.io/iximiuz/labs/nginx:alpine --port=80
kubectl expose deployment backend --port=80 --target-port=80
And look at the Pod app again, without touching it:
kubectl get pod app --watch
As soon as the Service answers, the init container finishes successfully, the sidecar starts and after it the main container. Init:0/2, Init:1/2, Running. Nobody has recreated anything: the Pod was waiting, and that is all.
Step 3: Check that the sidecar is working
The sidecar has been writing its heartbeat since before the application existed. Read it from both sides of the Pod:
kubectl logs app -c sidecar-logs
kubectl exec app -c app -- cat /var/log/app/app.log
The second command is the one that matters: the main container sees the file another container writes, because both mount the same emptyDir. In the real world the direction is usually the opposite (the application writes and the sidecar ships to Loki, Elastic or Datadog), but the mechanics are exactly this.
Check also how it shows up in the Pod's bookkeeping:
kubectl get pod app
kubectl get pod app -o jsonpath='{.status.initContainerStatuses}' | jq .
The READY column says 2/2: the sidecar counts as a live container, not as a finished initialization step.
💡 And what if the sidecar dies? Since it carries restartPolicy: Always, the kubelet restarts it without touching the main container. Check it if you feel like it: kubectl exec app -c sidecar-logs -- pkill -f sleep and watch the RESTARTS column.
Summary
- An init container prepares the ground and blocks the startup until it finishes successfully. They run in order.
- A sidecar is an init container with
restartPolicy: Always: it starts before the application, finishes after it and does not prevent a Job from finishing. - The containers of a Pod talk to each other through shared volumes and through
localhost.
- Previous lesson
- DaemonSet
- Next lesson
- Jobs and CronJobs