The best way to deploy to Kubernetes

If you're anything like me you have made many "new" projects for the web. Perhaps a little website, an API or some little test thing for your friend. 

And then: you need to deploy. The first time it's usually still fine. You've done this before. You build your Dockerfile, recycle an old kubespec / deployment.yaml and kubectl apply -f my-app.yaml It's online; life is good. 

deploy to kubernetes

This article is really about what comes after that

You find a problem. Fix it in your code, docker build the image again, push it. Re-run kubectl apply. Refresh. hmm. the fix doesn't show up!?

The problem is that Kubernetes (and Docker) don't automatically pull a new version of your image when you don't change the name of your image.

In this article I'm going to help you make the best, simplest, reproducible deployments possible.  You may not know about 'Make', and if you mostly do .js or python it may feel a bit oldscool. But it's actually very simple to use, and much more friendly than building your own 'deploy.sh' or 'deploy.py'. Make will be the basis of what I show here.

In this guide I'm going to add the build steps one by one.

Step 1: Building your Dockerfile

I'll assume this is nothing new to you. Instead I will just give you my two best tips:

  • Docker has two build engines, the newer is called Docker Buildkit, and can be called with docker buildx build . it is much smarter for things like caching. Use it like so: docker buildx build -t my_repo/project .
  • Do expensive installs before copying in your code, this will help Docker leverage the caching and be much faster. For web projects this means I'll do:

FROM node:14WORKDIR /app/COPY package.jsonRUN npm install# and only thenCOPY . .

Ok, now we're going to use Make to make our life easier. Just make a file named makefile and put in the following content: 

dockerize: docker buildx build -t my_repo/project .

Now you can do make dockerize and it will save you a lot of typing! -- We're going to do more of that. Note I call it 'dockerize' because you may want to also 'build' for just a local building of the project.

Step 2 - Pushing it to the Docker Hub

Now we'll also add a step in the Makefile to push it to the Docker hub, like so:

image=my_repo/projectdockerize: docker buildx build -t $(image) .push: docker push $(image)

Notice that I'm introducing a variable here. image. If you define them at top of your Makefile they will be are available throughout your file.

Great! To make our life easier we can also chain commands like so:

$ make dockerize push

Step 3 - Deploying it

Now we're going to deploy our freshly built and pushed image to our Kubernetes cluster. I'll assume you have a deployment spec called deployment.yaml which contains something like:

     containers:        - name: cool-website          image: my-repo/project

And we add the following to our Makefile

deployment: kubectl deploy -f deployment.yamldeploy: dockerize push deployment

The deployment part is nothing new. But notice how with deploy we add the verbs after deploy? This is to trigger those things first

So now we can type make deploy, and Make will do everything for us.

Step 4 - RE-deploying

Now, this is where it gets interesting. If you make changes to your code and run make deploy again you'll notice a new image will be built and deployed, but your deployment will not be updated. This is because the image name didn't change. 

A (wrong)  solution is to add 'imagePullPolicy: always' to your deployment. And this may actually work for you. But my experience is that it does not always work, and it's really hard to see which version is running in production.

A better way is to add a version to your image! 

Lets consider the following Makefile

image=my_repo/projectdockerize: docker buildx build -t $(image):$(version) .push: docker push $(image):$(version)deployment: sed -e 's|:latest|:$(version)|g' deployment.yaml | kubectl apply -f -deploy: dockerize push deployment

Here we introduce a variable $(version), but instead of defining it in the Makefile you can just define it at runtime, like so:

make deploy version=2

A special note is to be given to the modification to the deployment step. What I do here is to use the Stream Editor 'sed' to find and replace the word :latest with the version you specify. So essentially we edit the file and then stream it straight into kubectl to deploy it.

For this to work you need to make sure you tag your image name in your deployment file as image: my-repo/project:latest 

Try it! You can now easily deploy new versions.

Step 5 -- Using git

As you are working on a project a little longer it's nice to actually refer to a given version 'running' to code committed. And luckily 'git' allows for showing you which commit you are on. So lets add the following line to the top of our makefile:

version := $(shell git describe --tags --always --abbrev=7 --dirty)

What this line does is call out to the shell and run git describe, which returns information about the current git branch and commit you are on. --tags --always --abbrev=7 and --dirty help to format the returned string into something clear and usable. 

 If you use this you don't need to specify the version, and can just type make deploy. Power tip: You can still use version=22

The complete Makefile

version := $(shell git describe --tags --always --abbrev=7 --dirty)image=my_repo/projectdockerize: docker buildx build -t $(image):$(version) .push: docker push $(image):$(version)deployment: sed -e 's|:latest|:$(version)|g' deployment.yaml | kubectl apply -f -deploy: dockerize push deployment

Bonus

I'm now also adding a bonus configuration piece for you:

Secrets management:

secret_yaml = $(shell kubectl create secret generic $(1) \ --save-config --dry-run \ --from-file=./secrets/$(1).py \ -o json)secrets-prod: echo '$(call secret_yaml,production)' |kubectl apply -f - secrets-dev: echo '$(call secret_yaml,development)' |kubectl apply -f -

Make also allows basic functions. Which means that we can make parameters that are called from different functions. Note the $(1) here. It is the first parameter. In this Makefile snippet I use a parsing the secret out to json, which is then used for kubectl apply.