Raspberry Pi Cluster, Part 5 - Kubernetes

It’s time to join all of these dots together and really get into the depths of it. We have a functional Raspberry Pi Cluster, we have a functional Docker image hosting a website, so let’s install the container orchestration system known as Kubernetes.

I mentioned Kubernetes briefly in the previous post and what it is, but instead of going into the details here, I’ll leave that to The Linux Foundation to do a much better job.

As Kubernetes is an enterprise grade system, it has enterprise grade system requirements and under normal circumstances, we wouldn’t even consider running such a thing on a Raspberry Pi. Thankfully, the nice people at Rancher built a super lightweight stripped back version of Kubernetes called K3S that plays very nicely on Raspberry Pi’s.

Installing K3S

You’d think this part would be complicated, it couldn’t really be much easier. On your master node, run the following:

curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644

Note the --write-kubeconfig-mode 644 argument here. This is important as it installs Kubernetes with the correct permissions. Without it, when the cluster reboots, it won’t be able to access the file system. You won’t need many guesses to know how I found this out.

Give a few minutes and that is it. Kubernetes v1.21 (at the time of writing) is installed and in theory, the cluster is alive! We can check this by running:

kubectl get pods -A

All good! The next task is to install Kubernetes of each of the worker nodes. However, before we can do this, we need to get the cluster’s unique access token that we use to connect the worker nodes to the master. Run this and make a note of the result:

sudo cat /var/lib/rancher/k3s/server/token

Almost there! On each of the worker nodes, run the following:

curl -sfL https://get.k3s.io | K3S_URL=https://<server>:6443 K3S_TOKEN=<token> sh -

Where <server> is the IP address of your master node and <token> is the token we got from the master node in the previous step.

And once complete, the Raspberry Pi based Kubernetes cluster is complete!

Major problem No. 2

Full disclosure, I didn’t realise I had the problem that I’m just about to describe until a bit further down the line, but it makes sense to talk about it here quickly.

Whilst Kubernetes installs without any issues on a Raspberry Pi 3b+, after around 24 hours of use, the master node runs out of steam and locks itself up. After quite a bit research and a fair amount of swearing, I discovered that any versions of K3S later that v1.17 don’t play nicely on the 3b+ variants of the Pi.

The solution was to upgrade the master node to a Raspberry Pi 4 4Gb. I probably should have just bitten the bullet on done this in the first place. Never mind. As the Pi 4 runs host, especially running Kubernetes, I added a fan shim to keep it cool.

Here was additional shopping list:

This added a further £63.90 to the budget.

Configuring our cluster

We now need to tell Kubernetes what to do and we do this via a series of manifest files of different types. Again, this post is long enough already, so I’m not going into the details here, but this will give you an idea of how I got it all working.

Fortunately, there isn’t much in the way of configuration needed, the K3S install pretty much does it all. However, as the Docker image we created is in a private repo on DockerHub, Kubernetes needs the access credentials to pull the image down. Run the following:

kubectl create secret docker-registry regcred --docker-server=<your-registry-server> --docker-username=<your-name> --docker-password=<your-password> --docker-email=<your-email>

Replacing <your-registry-server>, <your-name>, <your-password> and <your-email> with your credentials. Kubernetes can now pull down our private image from DockerHub.

WARNING: Be aware that there is a potential security issue here where you credentials are visible in your bash history. If this is going to be a problem, there are options available to help. Give it a Google. For me this is not a factor - if someone can view my bash history, I have bigger problems to deal with!

Setting up a Deployment

A deployment manifest in basic terms tells the Kubernetes where to find the image that will be used as the base of the application that we want to run on the cluster. Let’s create the manifest file with sudo nano deployment.yml and copy in the following:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: blog-website
spec:
  replicas: 3
  selector:
    matchLabels:
      app: blog-website
  template:
    metadata:
      labels:
        app: blog-website
    spec:
      containers:
      - env:
        image: <username>/<containername>:latest
        imagePullPolicy: Always
        name: blog-website
        ports:
        - containerPort: 80
      imagePullSecrets:
        - name: regcred

Replace blog-website with what ever name you want. Just remember what you change it to as we’ll need to link to this shortly. There are some important lines to consider, the first is replicas. I chose 3 as I have 3 worker nodes in my cluster. You can set this how you like, you can have more that 1 replica per node too. I could have set this to 20, but that is probably a little excessive.

Secondly, replace <username> and <containername> with the image details that you created in the last post.

After you have saved and closed the manifest file, we need to pass it to Kubernetes. Run:

kubectl apply -f deployment.yml

After a few minutes, if you run kubectl get pods, you should see that 3 pods come online all running instances of our Docker container. Nice.

WARNING: This is where you are likely to see issues if you have any problems with your configuration or servers. One to look out for is an **EXEC FORMAT** error. This is most likely caused by you trying to run a container that was built to be run on a different processor architecture. Remember Raspberry Pi's have ARM processors. Images built for x86 based systems will not work on a Pi.

Setting up a Service

The next manifest we want to create is a service, this will give network access to our deployment. There are a few different types of service, however, we don’t need to specify one and this will use the default ClusterIP type. As before, let’s create a new file with sudo nano service.yml and copy in the following:

apiVersion: v1
kind: Service
metadata:
  name: blog-website-service
spec:
  selector:
    app: blog-website
  ports:
  - protocol: TCP
    port: 80

You can change the names to what you wish, just make sure they match between your different manifest files. Apply this service manifest with:

kubectl apply -f service.yml

Again, give it a few minutes and check your service is running with kubectl get service.

Setting up an Ingress

Like everything Kubernetes, there appears to be at least 100 ways of doing anything, so it’s no surprise that setting up Ingress can be confusing. An Ingress essentially routes external traffic to our service (which in turn exposes our deployment). Most people appear to favour the Nginx Ingress in all of the tutorials that I read researching this, but K3S ships with an Ingress controller called Traefik, so we’ll go with that.

As before, create a new file with sudo nano ingress.yml and copy in the following:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: blog-website-ingress
  annotations:
    kubernetes.io/ingress.class: traefik
spec:
  rules:
  - host: <external-domain>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: blog-website-service
            port:
              number: 80

As previously, make sure your names match what you put in the other manifest and replace <external-domain> with your own domain name that you want to use for your website.

Apply this service manifest with:

kubectl apply -f ingress.yml

Congratulations! Once the ingress comes online, you should have a cluster based website that is accessible on the network. There is one last task to make our master piece available to the outside world…

TOP TIP: You can combine all of these manifest into a single file, all you need to do is separate each declaration with ---. I separated it out here to make it easier to explain.

Setup port forwarding on network router

In terms of instructions, you are on your own I’m afraid. Every router is different, but the process is essentially the same. In your network router configuration, find the port forwarding section and forward port TCP 80 (might as well do 443 too if you enable SSL like I do in a future post) to the internal IP of your master node.

This was a pretty epic post, well done if you have made it this far. Now we have the website up and running, let’s help people find it by pointing a domain at it.

Next, Part 6 - Dynamic DNS