Kubefeeds Team A dedicated and highly skilled team at Kubefeeds, driven by a passion for Kubernetes and Cloud-Native technologies, delivering innovative solutions with expertise and enthusiasm.

Running a production-ready Raspbery Pi Kubernetes cluster at home

9 min read

Ambassador post originally published on Gerald on IT by Gerald Venzl

In this guide, I’ll cover how to run a production-ready Raspberry Pi Kubernetes Cluster using K3s.

Background

If you are like me, you probably have a bunch of (older) Raspberry Pi models lying around not doing much because you replaced them with newer models. So, instead of just having them collect dust, why not create your own little Kubernetes cluster and deploy something on them, or just use it to learn Kubernetes?

Setup

Hardware

  • 1 x Raspberry Pi Model 4 B with 4 GB RAM
  • 1 x Raspberry Pi Model 4 B with 2 GB RAM
  • 4 x Raspberry Pi Model 3 B+ with 1 GB of RAM
  • 1 x Netgear ProSafe 8 Port Gigabit unmanaged switch
  • 1 x Corsair Flash Voyager GTX 256GB USB 3.1 Premium Flash Drive

Note: This hardware setup is what I have available. At no point is this the recommendation for building your own cluster. If you have newer, more powerful Raspberry Pi models, you are probably better off using them instead.

image

Software

All Raspberry Pis are running Raspberry Pi OS (with desktop)

  • Release date: November 19th 2024
  • System: 64-bit
  • Kernel version: 6.6
  • Debian version: 12 (bookworm)

Why K3s?

K3s is a lightweight, CNCF-certified, and fully compliant Kubernetes distribution. It ships as a single binary, requires half the memory, supports other data stores, and more. As their website says, it’s:

Great for:

  • Edge
  • Homelab
  • Internet of Things (IoT)
  • Continuous Integration (CI)
  • Development
  • Single board computers (ARM)
  • Air-gapped environments
  • Embedded K8s
  • Situations where a PhD in K8s clusterology is infeasible

K3s Documentation

Another advantage of K3s for Raspberry Pis is that it allows for data stores other than etcd. That’s great because, as their website says, etcd is write-intensive and the SD cards can usually not handle the IO load:

K3s performance depends on the performance of the database. To ensure optimal speed, we recommend using an SSD when possible.

If deploying K3s on a Raspberry Pi or other ARM devices, it is recommended that you use an external SSD. etcd is write intensive; SD cards and eMMC cannot handle the IO load.

K3s Documentation – Requirements -> Hardware -> Disks

In my case, I am using an external MariaDB database running on the Corsair Flash Voyager GTX 256GB USB 3.1 Premium Flash Drive. For comparison, the SanDisk 256GB Extreme microSDXC UHS-I card offers a write rate of 130MB/s and a read rate of 190MB/s, while the Voyager GTX USB 3.1 provides a read and write rate of 440MB/s. However, it comes at 2.5 times the price of a microSD card.

Installation

cgroups

Kubernetes requires the cgroups (control groups) Linux kernel feature. Unfortunately, the memory subsystem of this feature is not enabled by default in the latest Raspberry Pi OS image. To verify whether it is, you can do a cat /proc/cgroups and see whether there is a 1 in the enabled column for the memory row:

gvenzl@gvenzl-rbp-0:~ $ cat /proc/cgroups
#subsys_name    hierarchy    num_cgroups    enabled
cpuset                  0             58          1
cpu                     0             58          1
cpuacct                 0             58          1
blkio                   0             58          1
memory                  0             58          0
devices                 0             58          1
freezer                 0             58          1
net_cls                 0             58          1
perf_event              0             58          1
net_prio                0             58          1
pids                    0             58          1

If you see a 0 like in the output above, you have to enable the memory subsystem. This is done by adding cgroup_enable=memory to the /boot/firmware/cmdline.txt file and then reboot the system. The quickest way to do this is via these commands (notesudo reboot will reboot your Raspberry Pi):

sudo sh -c 'echo " cgroup_enable=memory" >> /boot/firmware/cmd

Note: cgroup_enable=cpuset and cgroup_memory=1 are no longer required.

Once the system is up again, doublecheck the entry for the memory subsystem, which should now show a 1:

gvenzl@gvenzl-rbp-0:~ $ cat /proc/cgroups
#subsys_name    hierarchy    num_cgroups    enabled
cpuset                  0             94          1
cpu                     0             94          1
cpuacct                 0             94          1
blkio                   0             94          1
memory                  0             94          1
devices                 0             94          1
freezer                 0             94          1
net_cls                 0             94          1
perf_event              0             94          1
net_prio                0             94          1
pids                    0             94          1

Repeat the above step on every Raspberry Pi before continuing.

Static IP address configuration

Static IP addresses make things easy for cluster management and communication. It ensures that devices always have the same IP address, which makes it easier to identify a given node and prevent communication disruption between nodes due to changing IP addresses. The latest Raspberry Pi OS has a new NetworkManager and associated command line utilities. For an interactive, text-based UI, use the nmtui (network manager text user interface) command. For scripting purposes, you can use the nmcli (network manager command line interface) to assign static IP addresses for the Raspberry Pis. You should find an already preconfigured Wired connection 1 on the ethernet device eth0. You can verify that via nmcli con show:

gvenzl@gvenzl-rbp-0:~ $ nmcli con show
NAME                UUID                                  TYPE      DEVICE
preconfigured       999a68d9-b3e1-4437-bf86-1c9a5f775159  wifi      wlan0
lo                  db18dd9c-94fe-4fac-8c66-d6ddc3406900  loopback  lo
Wired connection 1  68f30a89-ef57-3ee9-8238-9310f0829f21  ethernet  eth0

To change the configuration for the ethernet connection to have a static IP address, use sudo nmcli con mod. The NN reflects the digits you want to use for the Raspberry Pi. In my case, it’s going to be 1011121314, and 15 on the given node:

sudo nmcli c mod "Wired connection 1" ipv4.addresses "192.168.0.NN/24" ipv4.method manual

If you also want to set a Gateway address to reach the outside network and/or internet and DNS entries for name resolution, you can do so with the following commands:

sudo nmcli con mod "Wired connection 1" ipv4.gateway 192.168.0.1
sudo nmcli con mod "Wired connection 1" ipv4.dns "192.168.0.1, 1.1.1.1, 8.8.8.8"

Note: In my case, I will reach the outside world via the WiFi connection. The ethernet connection is purely for cluster communication

Repeat the above step on every Raspberry Pi before continuing.

Installing K3s

The simplest way to install K3s is by running curl -sfL https://get.k3s.io | sh -. However, because I’m using an external MariaDB database as the cluster data store, things are a bit different. Instead of using etcd, the installation needs to connect to the MariaDB database. This can be done by supplying the --datastore-endpoint parameter or K3S_DATASTORE_ENDPOINT environment variable during the installation. For more details, see Cluster Datastore in the K3s documentation.

Creating the MariaDB database and user for Kubernetes

K3s is capable of connecting to the MariaDB socket at /var/run/mysqld/mysqld.sock using the root user if just mysql:// is provided as the datastore-endpoint. That means that the database needs to run on the same host as the control plane and socket connectivity for root has to be enabled in the MariaDB configuration. Alternatively, one can create a user and database manually, which is what I will do. The user and database will both be called kubernetes. Here are the four SQL statements you will need for that:

CREATE DATABASE kubernetes;
CREATE USER 'kubernetes'@'<your IP address range>' IDENTIFIED BY '<your password>';
GRANT ALL PRIVILEGES ON kubernetes.* TO 'kubernetes'@'<your IP address range>';
FLUSH PRIVILEGES;
gvenzl@gvenzl-rbp-0:~ $ sudo mysql
Welcome to the MariaDB monitor.  Commands end with ; or g.
Your MariaDB connection id is 36
Server version: 10.11.6-MariaDB-0+deb12u1 Debian 12
 
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
 
Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.
 
MariaDB [(none)]> CREATE DATABASE kubernetes;
Query OK, 1 row affected (0.001 sec)
 
MariaDB [(none)]> CREATE USER 'kubernetes'@'192.168.10.%' IDENTIFIED BY '*********';
Query OK, 0 rows affected (0.005 sec)
 
MariaDB [(none)]> GRANT ALL PRIVILEGES ON kubernetes.* TO 'kubernetes'@'192.168.10.%';
Query OK, 0 rows affected (0.002 sec)
 
MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.002 sec)
 
MariaDB [(none)]> exit;
Bye
gvenzl@gvenzl-rbp-0:~ $

Running the K3s setup script on the control plane Raspberry Pi

To start the K3s installation, a slightly different variation from the above setup script needs to be run to include the --datastore-endpoint parameter:

curl -sfL https://get.k3s.io | sh -s - --datastore-endpoint mysql://<username>:<password>@tcp(<hostname>:3306)/<database-name>

In my case, this is going to look like this:

curl -sfL https://get.k3s.io | sh -s - --datastore-endpoint "mysql://kubernetes:*******@tcp(192.168.10.10:3306)/kubernetes"
gvenzl@gvenzl-rbp-0:~ $ curl -sfL https://get.k3s.io | sh -s - --datastore-endpoint "mysql://kubernetes:*********@tcp(192.168.10.10:3306)/kubernetes"
[INFO]  Finding release for channel stable
[INFO]  Using v1.30.6+k3s1 as release
[INFO]  Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.30.6+k3s1/sha256sum-arm64.txt
[INFO]  Downloading binary https://github.com/k3s-io/k3s/releases/download/v1.30.6+k3s1/k3s-arm64
[INFO]  Verifying binary download
[INFO]  Installing k3s to /usr/local/bin/k3s
[INFO]  Finding available k3s-selinux versions
sh: 416: [: k3s-selinux-1.6-1.el9.noarch.rpm: unexpected operator
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Creating /usr/local/bin/ctr symlink to k3s
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s.service
[INFO]  systemd: Enabling k3s unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
[INFO]  Host iptables-save/iptables-restore tools not found
[INFO]  Host ip6tables-save/ip6tables-restore tools not found
[INFO]  systemd: Starting k3s
gvenzl@gvenzl-rbp-0:~ $

Once the script has finished, verify the control plane setup via sudo kubectl get nodes:

gvenzl@gvenzl-rbp-0:~ $ sudo kubectl get nodes
NAME           STATUS   ROLES                  AGE     VERSION
gvenzl-rbp-0   Ready    control-plane,master   2m48s   v1.30.6+k3s1

Adding nodes to the cluster

To add the additional Pis to the cluster, you must first retrieve the cluster token in the /var/lib/rancher/k3s/server/token file, which is needed for the agent installation. You can do that via the following command:

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

And will get a token that looks something like this:

gvenzl@gvenzl-rbp-0:~ $ sudo cat /var/lib/rancher/k3s/server/token
K103bf5abb471fc2f7bcda85fa95a60c0f934a22a858c6ae943f4d7e0ee4091bc11::server:f3d376e3274a174a38b3b97d224aac6d

Once you have retrieved the token, connect to the other Raspberry Pis and execute the following command:

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

For example:

curl -sfL https://get.k3s.io | K3S_URL=https://192.168.10.10:6443 K3S_TOKEN=K103bf5abb471fc2f7bcda85fa95a60c0f934a22a858c6ae943f4d7e0ee4091bc11::server:f3d376e3274a174a38b3b97d224aac6d sh -
gvenzl@gvenzl-rbp-1:~ $ curl -sfL https://get.k3s.io | K3S_URL=https://192.168.10.10:6443 K3S_TOKEN=K103bf5abb471fc2f7bcda85fa95a60c0f934a22a858c6ae943f4d7e0ee4091bc11::server:f3d376e3274a174a38b3b97d224aac6d sh -
[INFO]  Finding release for channel stable
[INFO]  Using v1.30.6+k3s1 as release
[INFO]  Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.30.6+k3s1/sha256sum-arm64.txt
[INFO]  Downloading binary https://github.com/k3s-io/k3s/releases/download/v1.30.6+k3s1/k3s-arm64
[INFO]  Verifying binary download
[INFO]  Installing k3s to /usr/local/bin/k3s
[INFO]  Finding available k3s-selinux versions
sh: 416: [: k3s-selinux-1.6-1.el9.noarch.rpm: unexpected operator
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Creating /usr/local/bin/ctr symlink to k3s
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-agent-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s-agent.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s-agent.service
[INFO]  systemd: Enabling k3s-agent unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s-agent.service → /etc/systemd/system/k3s-agent.service.
[INFO]  Host iptables-save/iptables-restore tools not found
[INFO]  Host ip6tables-save/ip6tables-restore tools not found
[INFO]  systemd: Starting k3s-agent

Once the installation has finished on all nodes, you have your K3s cluster up and running. You can verify that by running sudo kubectl get nodes on the control plane one more time:

gvenzl@gvenzl-rbp-0:~ $ sudo kubectl get nodes
NAME           STATUS   ROLES                  AGE     VERSION
gvenzl-rbp-0   Ready    control-plane,master   16m     v1.30.6+k3s1
gvenzl-rbp-1   Ready    <none>                 5m42s   v1.30.6+k3s1
gvenzl-rbp-2   Ready    <none>                 3m14s   v1.30.6+k3s1
gvenzl-rbp-3   Ready    <none>                 2m36s   v1.30.6+k3s1
gvenzl-rbp-4   Ready    <none>                 2m7s    v1.30.6+k3s1
gvenzl-rbp-5   Ready    <none>                 42s     v1.30.6+k3s1
gvenzl@gvenzl-rbp-0:~ $

Congratulations, you now have a K3s cluster ready for action!

Bonus: Access your cluster from the Outside with kubectl

If you want to access the cluster from, e.g., your local MacBook with kubectl, you will need to save a copy of the /etc/rancher/k3s/k3s.yaml file locally as ~/.kube/config (the k3s.yaml file needs to be called config) and replace the value of the server field with the IP address or name of the K3s server.

kubectl itself can be installed via Homebrew:

gvenzl@gvenzl-mac ~ % brew install kubectl
==> Downloading https://ghcr.io/v2/homebrew/core/kubernetes-cli/manifests/1.31.3
Already downloaded: /Users/gvenzl/Library/Caches/Homebrew/downloads/f8fd19d10e239038f339af3c9b47978cb154932f089fbf6b7d67ea223df378de--kubernetes-cli-1.31.3.bottle_manifest.json
==> Fetching kubernetes-cli
==> Downloading https://ghcr.io/v2/homebrew/core/kubernetes-cli/blobs/sha256:fd154ae205719c58f90bdb2a51c63e428c3bf941013557908ccd322d7488fb67
Already downloaded: /Users/gvenzl/Library/Caches/Homebrew/downloads/ec1af5c100c16e5e4dc51cff36ce98eb1e257a312ce5a501fae7a07724e59bf9--kubernetes-cli--1.31.3.sonoma.bottle.tar.gz
==> Pouring kubernetes-cli--1.31.3.sonoma.bottle.tar.gz
==> Caveats
zsh completions have been installed to:
  /usr/local/share/zsh/site-functions
==> Summary
🍺  /usr/local/Cellar/kubernetes-cli/1.31.3: 237 files, 61.3MB
==> Running `brew cleanup kubernetes-cli`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).

Next, create the ~/.kube folder and save a copy of k3s.yaml as config:

gvenzl@gvenzl-mac ~ % mkdir ~/.kube
gvenzl@gvenzl-mac ~ % scp root@gvenzl-rbp-0:k3s.yaml ~/.kube/config
root@gvenzl-rbp-0's password:
k3s.yaml                               100% 2965   221.2KB/s   00:00

And replace the server parameter with the IP address or hostname of your control plane node:

gvenzl@gvenzl-mac ~ % sed -i '' 's|server: .*|server: https://gvenzl-rbp-0:6443|g' ~/.kube/config

Once you have done that, you can control your cluster locally too:

gvenzl@gvenzl-mac ~ % kubectl get nodes
NAME           STATUS   ROLES                  AGE   VERSION
gvenzl-rbp-0   Ready    control-plane,master   39m   v1.30.6+k3s1
gvenzl-rbp-1   Ready    <none>                 28m   v1.30.6+k3s1
gvenzl-rbp-2   Ready    <none>                 25m   v1.30.6+k3s1
gvenzl-rbp-3   Ready    <none>                 25m   v1.30.6+k3s1
gvenzl-rbp-4   Ready    <none>                 24m   v1.30.6+k3s1
gvenzl-rbp-5   Ready    <none>                 23m   v1.30.6+k3s1
Kubefeeds Team A dedicated and highly skilled team at Kubefeeds, driven by a passion for Kubernetes and Cloud-Native technologies, delivering innovative solutions with expertise and enthusiasm.