{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreic5d7uho7uxwok7p77hewvyskfgpl7io2u6liq7hnawtslcmbxdzy",
"uri": "at://did:plc:5y2ps7xhcqmc2d63b73ui72s/app.bsky.feed.post/3mmu3m63xl6y2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreigoeti3h5pauowt6jbjpvxxv22sjiqzwoquovccntpm4ictzalxl4"
},
"mimeType": "image/jpeg",
"size": 148418
},
"description": "Learn how to deploy Kubernetes on a Raspberry Pi ClusterHAT cluster using K3s and k3sup, configure shared NFS storage, label nodes, and deploy your first container workloads.",
"path": "/setting-up-a-raspberry-pi-cluster-with-clusterhat-part-3-kubernetes/",
"publishedAt": "2026-05-27T18:00:34.000Z",
"site": "https://blog.php-systems.com",
"tags": [
"Part 1",
"Part 2",
"blinkt-cpu.zip",
"blinkt-cpu.tar",
"https://github.com/alexellis/k3sup"
],
"textContent": "In Part 1, we built the Raspberry Pi ClusterHAT hardware and configured networking and SSH access.\n\nIn Part 2, we automated cluster management with Ansible and created a scalable management workflow.\n\nNow it’s time for the fun part: deploying Kubernetes.\n\nFor this cluster, I chose:\n\n * **K3s** for lightweight Kubernetes\n * **k3sup** for simplified installation and node joining\n * **NFS shared storage** hosted from the controller node\n\n\n\nBy the end of this guide, you’ll have:\n\n * A functioning Kubernetes cluster\n * Shared persistent storage\n * Worker node labels\n * The ability to deploy your own container workloads\n\n\n\n* * *\n\n# Step 1: Configure Shared Storage with NFS\n\nBefore deploying Kubernetes, it’s useful to create shared storage accessible by all nodes.\n\nThis is especially useful for:\n\n * Shared container images\n * Persistent application data\n * Logs\n * Lightweight development workflows\n\n\n\n* * *\n\n## Install the NFS Server on the Controller\n\nOn the controller node:\n\n\n sudo apt-get install -y nfs-kernel-server\n\nCreate the storage directory:\n\n\n sudo mkdir -p /media/Storage\n sudo chown nobody:nogroup /media/Storage\n sudo chmod -R 777 /media/Storage\n\n* * *\n\n## Configure Exports\n\nEdit `/etc/exports`:\n\n\n sudo nano /etc/exports\n\nAdd:\n\n\n /media/Storage 172.19.181.0/24(rw,sync,no_root_squash,no_subtree_check)\n\nApply the export configuration:\n\n\n sudo exportfs -a\n\n* * *\n\n# Step 2: Mount NFS Storage on Worker Nodes\n\nOn every worker node (`p1` - `p4`), install the NFS client:\n\n\n sudo apt-get install -y nfs-common\n\nCreate the mount directory:\n\n\n sudo mkdir -p /media/Storage\n sudo chown nobody:nogroup /media/Storage\n sudo chmod -R 777 /media/Storage\n\n* * *\n\n## Configure Automatic Mounting\n\nEdit `/etc/fstab`:\n\n\n sudo nano /etc/fstab\n\nAdd:\n\n\n 172.19.181.254:/media/Storage /media/Storage nfs defaults 0 0\n\nMount everything:\n\n\n sudo mount -a\n\nVerify the mount works by creating a test file and ensuring it appears across all nodes.\n\nIf you hit errors:\n\n * Double-check `/etc/fstab`\n * Verify `/etc/exports`\n * Confirm the controller firewall allows NFS traffic\n\n\n\n* * *\n\n# Alternatively: Setup NFS Setup via Ansible\n\nOnce manual testing succeeds, automate it.\n\nHere’s a simplified Ansible approach.\n\n* * *\n\n## Controller Play\n\n\n - name: Configure NFS server\n hosts: controllers\n become: yes\n\n tasks:\n - name: Install NFS server\n apt:\n name: nfs-kernel-server\n state: present\n\n - name: Create storage directory\n file:\n path: /media/Storage\n state: directory\n mode: '0777'\n\n - name: Configure exports\n lineinfile:\n path: /etc/exports\n line: '/media/Storage 172.19.181.0/24(rw,sync,no_root_squash,no_subtree_check)'\n\n - name: Reload exports\n command: exportfs -a\n\n* * *\n\n## Worker Play\n\n\n - name: Configure NFS clients\n hosts: workers\n become: yes\n\n tasks:\n - name: Install NFS client\n apt:\n name: nfs-common\n state: present\n\n - name: Create mount directory\n file:\n path: /media/Storage\n state: directory\n mode: '0777'\n\n - name: Configure fstab\n lineinfile:\n path: /etc/fstab\n line: '172.19.181.254:/media/Storage /media/Storage nfs defaults 0 0'\n\n - name: Mount storage\n command: mount -a\n\nAt this point, your cluster has shared persistent storage available everywhere.\n\n* * *\n\n# Misstep: Installing K3s with k3sup\n\nI initially attempted to automate K3s installation using the existing Ansible role.\n\nInstall the collection:\n\n\n ansible-galaxy collection install vandot.k3sup\n\nI also had to modify the Python interpreter inside:\n\n\n ~/.ansible/collections/ansible_collections/vandot/k3sup/plugins/modules/k3sup.py\n\nThe module was trying to use:\n\n\n /usr/bin/env python\n\nRunning manually worked fine, but the role itself continued to cause issues during installation.\n\nIn the end, using `k3sup` directly was significantly simpler.\n\n* * *\n\n# Step 3: Install the Kubernetes Controller\n\nOn the controller node, run:\n\n\n k3sup install --local\n\nOnce complete:\n\n\n export KUBECONFIG=`pwd`/kubeconfig\n kubectl get node\n\nYou should now see the controller node in the cluster.\n\n* * *\n\n# Step 4: Join the Worker Nodes\n\nJoin each worker node:\n\n\n for i in $(seq 1 1 4); do\n k3sup join \\\n --ip 172.19.181.$i \\\n --server-ip 172.19.181.254 \\\n --user pi\n done\n\nVerify the cluster again:\n\n\n export KUBECONFIG=`pwd`/kubeconfig\n kubectl get node\n\nYou should now see:\n\n * Controller node\n * `p1`\n * `p2`\n * `p3`\n * `p4`\n\n\n\nAll reporting as `Ready`.\n\n* * *\n\n# Step 5: Label GPIO-Capable Nodes\n\nBecause these nodes interact with physical GPIO hardware, adding labels makes workload scheduling easier.\n\nExample:\n\n\n kubectl label nodes p1 gpio=true\n kubectl label nodes p2 gpio=true\n kubectl label nodes p3 gpio=true\n kubectl label nodes p4 gpio=true\n\nYou can later target workloads using node selectors.\n\n* * *\n\n# Step 6: Deploy a Custom Workload\n\nOne of the first workloads I deployed was a custom **blinkt** container for Raspberry Pi LEDs. The entire project can be found here: blinkt-cpu.zip\n\n* * *\n\n## Build the Image\n\nBuild your container image locally:\n\n\n docker build -t blinkt-cpu .\n\nExport the image:\n\n\n docker save blinkt-cpu > blinkt-cpu.tar\n\nIf you don't have docker installed, you will need to build the image on a device with the same architecture and copy it over to the control node. Alternatively, if you trust me, the image can be found here: blinkt-cpu.tar\n\nCopy the image to the shared NFS mount:\n\n\n cp blinkt-cpu.tar /media/Storage/\n\n* * *\n\n## Import the Image into K3s\n\nOn each node:\n\n\n sudo k3s ctr images import /media/Storage/blinkt-cpu.tar\n\nOnce imported, your Kubernetes deployments can reference the image directly.\n\n* * *\n\n# Example GPIO Node Selector\n\nExample deployment snippet:\n\n\n nodeSelector:\n gpio: \"true\"\n\nThis ensures GPIO-dependent workloads only run on appropriate hardware.\n\n* * *\n\n# Final Thoughts\n\nAt this point, you now have:\n\n * A Raspberry Pi Kubernetes cluster\n * Shared NFS-backed storage\n * Automated configuration with Ansible\n * Worker node scheduling labels\n * A platform for experimenting with distributed systems\n\n\n\nThis setup is surprisingly capable for:\n\n * Home labs\n * CI/CD experimentation\n * Edge computing\n * IoT orchestration\n * Learning Kubernetes safely and cheaply\n\n\n\nAnd perhaps most importantly—it’s fun.\n\n* * *\n\n# Additional Resources\n\n * k3sup GitHub Repository\nhttps://github.com/alexellis/k3sup\n\n\n\n* * *\n\nThat wraps up this three-part Raspberry Pi ClusterHAT Kubernetes series. From bare SD cards to a functioning Kubernetes cluster, you now have a powerful miniature platform ready for experimentation.",
"title": "Setting Up a Raspberry Pi Cluster with ClusterHAT (Part 3: Kubernetes)",
"updatedAt": "2026-05-27T18:00:37.720Z"
}