Pods, init containers and sidecars

How to write a Pod fast, how command and args decide what a container runs, and how init containers and sidecars work beside your app.

35 min 2 hands-on tasksWorkloads · 15% of the exam

Key ideas in this lesson · tap one for a simple explanation

What you will be able to do

  • Make a Pod manifest with kubectl run --dry-run=client, then add command, args, env, ports and restartPolicy to it.
  • Say what a container will run when you set command, args, or both.
  • Build a Pod whose init container prepares a shared volume, and turn a helper container into a native sidecar.
  • Read the logs of, and open a shell in, one chosen container of a Pod.

A is the smallest thing Kubernetes runs. It holds one or more that share one network address and can share folders. Every , and in this module ends up as Pods, so you need to write one quickly.

Exam tasks rarely want a plain Pod with one container. They want a second container, a setup step, a shared folder or an exact command. The grader then reads each container's name and image, and also where it sits in the spec. If the task wanted initContainers and you used containers, you score zero, even if the Pod runs.

Start from kubectl run, never from a blank file

Typing from memory is slow and easy to get wrong. Let kubectl run write the first version for you, then edit it.

kubectl run patterns worth memorizing
# A manifest to edit, not a running Pod
kubectl run web --image=nginx:1.27 --port=80 --labels=app=web,tier=front --env=MODE=production --dry-run=client -o yaml > web.yaml

# A one-shot Pod that is not restarted when its command exits
kubectl run once --image=busybox:1.36 --restart=Never -- sh -c 'echo hello; sleep 5'

# Everything after -- becomes args ...
kubectl run box1 --image=busybox:1.36 -- sleep 3600
# ... unless you add --command, then it becomes command
kubectl run box2 --image=busybox:1.36 --command -- sleep 3600

kubectl run always makes exactly one Pod. For other objects, use kubectl create deployment, kubectl create job or kubectl create cronjob. It has no --requests or --limits flag either. You add CPU and memory settings in the YAML, as the requests and limits lesson shows.

The generated file is only a start. These are the fields you will most often add by hand:

The fields you will add by hand
apiVersion: v1
kind: Pod
metadata:
  name: web
  namespace: wl-pods
  labels:
    app: web
spec:
  restartPolicy: Always          # Always (default) | OnFailure | Never; applies to the whole Pod
  containers:
    - name: nginx
      image: nginx:1.27
      ports:
        - containerPort: 80      # documents the port and lets probes/Services use the name
          name: http
      env:
        - name: MODE
          value: production
    - name: clock
      image: busybox:1.36
      command: ["sh", "-c"]                         # replaces the image ENTRYPOINT
      args: ["while true; do date; sleep 10; done"] # replaces the image CMD

command and args versus ENTRYPOINT and CMD

Every container image has a default program to run. The image's Dockerfile names that program in ENTRYPOINT and its default arguments in CMD. A Pod can replace either one. The names differ, so keep the pairs apart:

In the image (Dockerfile)In the Pod specWhat it holds
ENTRYPOINTcommandThe program to run
CMDargsThe arguments passed to that program

So command replaces ENTRYPOINT, and args replaces CMD. Here is what runs in each case:

You setThe container runsExample with nginx:1.27
NeitherThe image's ENTRYPOINT with the image's CMD/docker-entrypoint.sh nginx -g 'daemon off;'
args onlyThe image's ENTRYPOINT with your args/docker-entrypoint.sh sleep 3600
command onlyYour command. The image's CMD is ignoredsleep 3600
BothYour command with your argssh -c 'nginx -t'
  • busybox has no ENTRYPOINT, and its CMD is sh. That is why args alone happens to work with busybox.
  • Many app images are different: their ENTRYPOINT is the app itself. Say your Django image has ENTRYPOINT gunicorn. With only args: ["sleep", "3600"], the container runs gunicorn sleep 3600, which is not what you wanted.
  • So when a task says the container must run something, set command.
  • Both fields are lists. To run a shell one-liner, use ["sh", "-c", "…"].

Init containers

An is a setup step. It runs to the end and exits before your app starts. For example, before nginx starts, an init container can write the web page that nginx will serve.

  • Init containers run one at a time, in the order you list them.
  • Each one must succeed (exit with code 0) before the next one starts.
  • Your app containers start only after the last init container succeeds.
  • If one fails, the retries it according to the Pod's restartPolicy. With Never, the whole Pod fails instead.

While they run, the STATUS column of kubectl get pods shows their progress. Init:0/2 means none of two has finished yet. Init:Error and Init:CrashLoopBackOff mean one of them keeps failing.

How does an init container hand its work to the app? Through a shared volume. An emptyDir is an empty folder that Kubernetes creates when the Pod starts. Every container in the Pod can mount it, each at its own path, and they all see the same files. It lives as long as the Pod: it survives a container restart, but it is deleted with the Pod.

An init container prepares a file for nginx

Init container

runs first, writes index.html, exits

emptyDir volume

a folder the Pod's containers share

nginx container

starts next and serves the file

The init container runs first and exits. Its file stays in the emptyDir, where nginx finds it and serves it.

An init container rendering content into a shared emptyDir
apiVersion: v1
kind: Pod
metadata:
  name: site
  namespace: wl-pods
spec:
  initContainers:
    - name: render
      image: busybox:1.36
      command: ["sh", "-c", "echo \"built on $(hostname) at $(date)\" > /data/index.html"]
      volumeMounts:
        - name: html
          mountPath: /data
  containers:
    - name: nginx
      image: nginx:1.27
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
  volumes:
    - name: html
      emptyDir: {}

Native sidecar containers

A is a helper container that runs next to your app for the app's whole life. Examples are a log shipper, a proxy, or a tool that reloads config files.

The old way was to add the helper as a second entry under containers. That causes two problems:

  • There is no start order, so the helper may start after your app.
  • In a Job, the helper never exits. So the Pod never finishes, and the Job never completes.

A native sidecar fixes both. It is an entry under initContainers with one extra line: restartPolicy: Always. The kubelet then handles it like this:

  • It starts the sidecar in init order, but it waits only for the sidecar to start, not to exit.
  • It keeps the sidecar running beside your app, and restarts it if it stops.
  • It stops the sidecar after your app containers finish.
The life of a Pod with a native sidecar

1. Sidecar starts

listed under initContainers

2. App runs

the sidecar runs beside it

3. App exits

its work is done

4. Sidecar is stopped

so the Pod can finish

This matters most in a Job, because a Job's Pod must finish before the Job can complete.

Init containerNative sidecarApp container
Where it goesinitContainersinitContainers, with restartPolicy: Alwayscontainers
When it runsFirst, then it exitsStarts first, then runs beside the appAfter the init containers finish and the sidecars start
ExampleWrites index.htmlShips the app's logsnginx, or your Django app
A log tailer as a native sidecar
apiVersion: v1
kind: Pod
metadata:
  name: app
  namespace: wl-pods
spec:
  initContainers:
    - name: log-tail
      image: busybox:1.36
      restartPolicy: Always      # this one line makes it a sidecar
      command: ["sh", "-c", "touch /var/log/app/app.log; tail -F /var/log/app/app.log"]
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "while true; do date >> /var/log/app/app.log; sleep 2; done"]
      volumeMounts:
        - name: logs
          mountPath: /var/log/app
  volumes:
    - name: logs
      emptyDir: {}

Task 2 below puts a sidecar inside a Job, an object that runs a Pod until its work is done. Jobs have their own lesson: DaemonSets, Jobs and CronJobs.

Working with one container

In a Pod with several containers, needs to know which one you mean. Give the container's name with -c, for example -c log-tail.

Working with one container at a time
kubectl -n wl-pods get pod app                      # READY 2/2: the sidecar is counted
kubectl -n wl-pods logs app -c log-tail             # one container
kubectl -n wl-pods exec app -c app -- cat /var/log/app/app.log
kubectl -n wl-pods exec -it app -c app -- sh
kubectl -n wl-pods describe pod app | sed -n '/Init Containers/,/Conditions/p'

Without -c, kubectl picks a container for you and prints a Defaulted container note. It picks the one named in the Pod annotation kubectl.kubernetes.io/default-container, or else the first entry under containers. Read that note. It is how people end up reading the wrong container's logs.

More ways to read logs, such as the logs of a container that crashed, are in Logs, events and container output streams.

In the official docs· open on exam day too

Hands-on task 1 of 2wl-pods-1
~6 min4% weightWorkloads

Serve a file prepared by an init container

In namespace wl-pods create a Pod named web. An init container named prepare (image busybox:1.36) writes the single line prepared by init to a file index.html on an emptyDir volume named content. A container named nginx (image nginx:1.27) mounts the same volume at /usr/share/nginx/html and serves that file on port 80.

Cluster:kubectl config use-context kind-cka

  1. 1

    Set up the scenario

    curl -fsSL https://codewithmuh.com/labs/cka/t/wl-pods-1/setup | bash
  2. 2

    Solve it in your terminal

    Use kubectl and the Kubernetes docs, like on exam day. Save any answer files under /tmp/cka/.

  3. 3

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/wl-pods-1/verify | bash
    Got FAIL? Read which check failed, fix it, run it again.
  4. 4

    Clean up (optional)

    curl -fsSL https://codewithmuh.com/labs/cka/t/wl-pods-1/cleanup | bash
Hands-on task 2 of 2wl-pods-2
~8 min6% weightWorkloads

Let a Job finish despite its log shipper

The Job report in namespace wl-pods never completes. Its main container finishes its work, but the log-shipper container keeps the Pod running. Change the Job so that log-shipper runs as a native sidecar: it starts before main, runs while main works, and is stopped automatically when main exits. Keep both containers' names, images and commands and the shared volume. The Job must reach the Complete condition.

Cluster:kubectl config use-context kind-cka

  1. 1

    Set up the scenario

    curl -fsSL https://codewithmuh.com/labs/cka/t/wl-pods-2/setup | bash
  2. 2

    Solve it in your terminal

    Use kubectl and the Kubernetes docs, like on exam day. Save any answer files under /tmp/cka/.

  3. 3

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/wl-pods-2/verify | bash
    Got FAIL? Read which check failed, fix it, run it again.
  4. 4

    Clean up (optional)

    curl -fsSL https://codewithmuh.com/labs/cka/t/wl-pods-2/cleanup | bash

Finished this lesson?

0 of 2 tasks passed. Your readiness counts tasks, so run them before you move on.