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
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!