Using initContainers in Kubernetes

I’ve been considering writing a post about Kubernetes initContainers for a while, but for some reason I just haven’t gotten around to doing it… That’s about to change!

Let’s start by having a look at what initContainers are, and why they are useful.

What are initContainers?

Sometimes you have tasks that need to be performed before your Pod starts up. It could be that you need to load some configuration data from a central location, or maybe register the new Pod for service location. Whatever it is, it is a task that needs to be performed before the “main” containers start up.

Sure, you could make it part of the “main” container if you only have one, but that feels…well…a bit icky. And complicated when you have more than one container. And it could also be that it is a somewhat slow operation that could causes problems with the routing during startup, or the “synchronization” between multiple containers. Either way, having a separate container do the work before the “main” one(s) start, sounds like pretty nice solutions. And that’s where initContainers come into play.

initContainers are just containers that run before the main contianer is started.

The example

To demonstrate the use of an initContainer, I’ve decided to create a Pod that uses an initContainer to clone a GitHub repo and run Jekyll to generate a static site. The main container is an nginx container that hosts the website that the initContainer generated.

So let’s get started!

Creating the initContainer image

The initContainer needs to do two things. It needs to clone the GitHub repo, and it needs to generate a static website using Jekyll. To do this, I’ve created a shell script that looks like this

git clone "https://$PAT:x-oauth-basic@github.com/ChrisKlug/chrisklug.github.io.git" ./source --depth 1

chmod -R 0777 .

jekyll build --source ./source --destination ./web/

It uses git clone to clone the repo. However, as the repo is private, I’m using this weird authentication scheme $PAT:x-oauth-basic. This allows me to use a GitHub Personal Access Token for authentication. And even if my PAT only gives private repo read access, it is still a credential, so I want to store it somewhere safe and not add it hard coded to my script. Because of that I’m using an environment variable to store the value, and $PAT to add it to the URL.

Next, the script sets full access to the current directory, and everything inside it, to make sure that Jekyll is allowed to read and write files, as well as create directories.

Finally, the script uses Jekyll to generate a static website from the newly cloned source code. And the result is placed in a directory called web.

That’s all there is to it!

The next step is to create a Dockerfile for the initContainer. It should look something like this

FROM jekyll/jekyll

ADD ./init.sh ./

ENTRYPOINT ["/bin/bash", "./init.sh"]

As you can see, all it really does, is to copy the script to an image that uses jekyll/jekyll as the base, and then runs the init.sh script on startup.

Once the Dockerfile is in place, I can create an image by running

> docker build -t zerokoll/fearofoblivioninit .

And to verify that it works, I’ll just run

> docker run --rm -e PAT=$PAT -v "$PWD/web:/srv/jekyll/web" zerokoll/fearofoblivioninit

As you can see, I’m providing my GitHub Personal Access Token as an environment variable, and map a local path (./web) as a volume located at /srv/jekyll/web inside the container.

Since the script will tell Jekyll to put the generated site at /srv/jekyll/web, this will cause the generated site to be written to the host machine so that we can verify that it works.

And since I end up with a static version of my blog in a folder called web on my host after running this command, I know that the container is working.

The next step is to push it to Docker Hub by running

> docker push zerokoll/fearofoblivioninit

Once my image has been pushed to Docker Hub, it is time to create the Pod.

Crating a Pod that uses an initContainer

The first step in creating the actual Pod, is to create the Pod definition. And it looks like this

apiVersion: v1
kind: Pod
metadata:
  name: fear-of-oblivion
  labels:
    app: web
spec:
  containers:
  - name: web
    image: nginx
    volumeMounts:
    - mountPath: /usr/share/nginx/html
      name: web
  initContainers:
  - name: init
    image: zerokoll/fearofoblivioninit
    env:
    - name: PAT
      value: "ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    volumeMounts:
    - mountPath: /srv/jekyll/web
      name: web
  volumes:
  - name: web
    emptyDir: {}

Ok, so let’s have a look at what this definition contains.

First of all, and quite obviously, it is a Pod definition for a Pod called fear-of-oblivion. It consists of 3 parts.

The first part is the “main” container, which is running the official nginx image. It also mounts a volume called web at /usr/share/nginx/html, which is the default path served by nginx. So anything put at that path will be served.

The second part is the initContainer, which is placed “inside” the initContainers list, which makes it pretty obvious that you can have multiple initContainers for your Pod.

The initContainer is obviously configured to use the zerokoll/fearofoblivioninit image. But it also needs to set up an environment variable for the GitHub PAT, and mount a volume at srv/jekyll/web.

Note: A better solution for storing the PAT would be to add it as a Secret to the K8s cluster, and then use that to set the environment variable. However, as this is a demo, it’s much simpler to put it in the YAML…

Finally, the YAML defines a single volume called web. It is defined as an emptyDir, which is basically a temporary storage area that has the same lifetime as the Pod itself. This volume is the one mounted in both containers, which means that the initContainer will generate the static website inside this volume, and nginx will serve the generated content since it is mounted at the default nginx content path.

Once the YAML is done, I can create a new Pod by running

> kubectl apply -f web.yml

and then start a port-forwarding session to the Pod like this

> kubectl port-forward fear-of-oblivion 8080:80

Once the port-forwarding is up and running, I can open my browser at http://localhost:8080 and get my blog

Fear Of Oblivion

Cool! It seems like it is working!

However, it’s worth noting that it can take a little while from the time you deploy the Pod until it is actually running since the initContainer needs to run to completion first. During that time, you can check the progress by running

> kubectl get pods

NAME               READY   STATUS     RESTARTS   AGE
fear-of-oblivion   0/1     Init:0/1   0          1s

As you can see in the STATUS column, Init:0/1 tells you that it is currently busy running the initContainers for the Pod. As soon as that goes away and the STATUS column says Running, the Pod is up and running, and ready to handle request.

You can also run

> kubectl describe pod/fear-of-oblivion

...
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  10s   default-scheduler  Successfully assigned default/fear-of-oblivion to docker-desktop
  Normal  Pulling    9s    kubelet            Pulling image "zerokoll/fearofoblivioninit"
  Normal  Pulled     8s    kubelet            Successfully pulled image "zerokoll/fearofoblivioninit" in 1.4260244s
  Normal  Created    8s    kubelet            Created container init
  Normal  Started    7s    kubelet            Started container init
  Normal  Pulling    3s    kubelet            Pulling image "nginx"
  Normal  Pulled     2s    kubelet            Successfully pulled image "nginx" in 1.4235151s
  Normal  Created    2s    kubelet            Created container web
  Normal  Started    2s    kubelet            Started container web

This gives you a bit more information about the startup of the Pod if you look at the Events section.

Note: Yes, I removed a ton of information from the output above as the only interesting part was the Events section.

That’s all there is to it! It’s pretty simple to work with, and still very powerful. Just like most of the best features in software development!

Conclusion

initContainers are a really nice way to handle any actions that need to be performed before the “main” containers in a Pod is started. It gives a very clean separation of concerns and can simplify your images quite a bit!

Obviously, this is a somewhat contrived example, but it does work! And I can definitely see a lot of real world scenarios where initContainers could perform useful tasks for us before Pod start up.

On another note, I finally managed to create a post that wasn’t a 2 hour read! Hopefully I still managed to cover everything that needed to be covered! If not, feel free to reach out and ask questions! I’m always available at @ZeroKoll!

zerokoll

Chris

Developer-Badass-as-a-Service at your service