<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged kubernetescluster</title>
<link>https://coderlegion.com/tag/kubernetescluster</link>
<description>Powered by Question2Answer</description>
<item>
<title>How a Kubernetes Cluster Works | Control Plane &amp; Worker Nodes</title>
<link>https://coderlegion.com/28959/how-a-kubernetes-cluster-works-control-plane-worker-nodes</link>
<description>&lt;h3 id=&quot;how-does-a-kubernetes-cluster-actually-work-&quot;&gt;How does a Kubernetes cluster actually work?&lt;/h3&gt;
&lt;p&gt;I’ve been learning Kubernetes and realized that it’s easy to memorize terms like &lt;strong&gt;Control Plane, Worker Node, API Server, Scheduler,&lt;/strong&gt; and &lt;strong&gt;kubelet&lt;/strong&gt; without really understanding how they fit together.&lt;/p&gt;
&lt;p&gt;So I put together a video explaining &lt;strong&gt;how a Kubernetes cluster works&lt;/strong&gt;, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Control Plane and Worker Nodes&lt;/li&gt;
&lt;li&gt;API Server&lt;/li&gt;
&lt;li&gt;etcd&lt;/li&gt;
&lt;li&gt;Scheduler&lt;/li&gt;
&lt;li&gt;Controller Manager&lt;/li&gt;
&lt;li&gt;Pods&lt;/li&gt;
&lt;li&gt;kubelet&lt;/li&gt;
&lt;li&gt;Container Runtime&lt;/li&gt;
&lt;li&gt;kube-proxy&lt;/li&gt;
&lt;li&gt;What happens when you run a &lt;code&gt;kubectl&lt;/code&gt; command?&lt;/li&gt;
&lt;li&gt;Different ways to create and operate a Kubernetes cluster&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My goal was to understand the &lt;strong&gt;flow&lt;/strong&gt;, not just memorize component definitions.&lt;/p&gt;
&lt;p&gt;🎥 &lt;strong&gt;Watch here:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=v-Zh2sIPm7s&quot;&gt;How a Kubernetes Cluster Works | Control Plane &amp;amp; Worker Nodes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I’d love to hear from other Kubernetes learners like me: &lt;strong&gt;which part of the cluster architecture took you the longest to understand?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/28959/how-a-kubernetes-cluster-works-control-plane-worker-nodes</guid>
<pubDate>Tue, 29 Sep 2026 12:00:00 +0000</pubDate>
</item>
<item>
<title>How a Football Match Helped Me Understand Kubernetes: Desired State vs Actual State</title>
<link>https://coderlegion.com/22896/how-a-football-match-helped-me-understand-kubernetes-desired-state-vs-actual-state</link>
<description>&lt;p&gt;Have you ever wondered what Desired State and Actual State mean in Kubernetes?&lt;/p&gt;
&lt;p&gt;I used to hear those terms all the time, but they didn't truly make sense until I watched a football match.&lt;/p&gt;
&lt;p&gt;In this &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=X7LAFxM1Yt4&quot;&gt;video&lt;/a&gt;, Ada explains one of Kubernetes' most important concepts using a simple football analogy. Instead of complicated diagrams and technical jargon, you'll see how a football coach, substitutions, and an unexpected injury perfectly explain how Kubernetes constantly works to keep your applications running.&lt;/p&gt;
&lt;p&gt;Whether you're preparing for the KCNA, CKA, or CKAD, learning Kubernetes for DevOps, or just getting started with cloud-native technologies, this real-world story will help you understand one of Kubernetes' core ideas.&lt;/p&gt;
&lt;h4 id=&quot;what-you-ll-learn&quot;&gt;What you'll learn&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;What Desired State means in Kubernetes&lt;/li&gt;
&lt;li&gt;What Actual State means&lt;/li&gt;
&lt;li&gt;How Kubernetes compares both states&lt;/li&gt;
&lt;li&gt;What reconciliation is&lt;/li&gt;
&lt;li&gt;Why Kubernetes automatically creates new Pods&lt;/li&gt;
&lt;li&gt;Why this makes Kubernetes reliable&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;-watch-next&quot;&gt;🎯 Watch next&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/21v-aDkHNOI&quot;&gt;What a Busy Restaurant Can Teach You About Kubernetes Services&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;-want-to-practice-kubernetes-yourself-&quot;&gt;🚀 Want to practice Kubernetes yourself?&lt;/h4&gt;
&lt;p&gt;Spin up a managed Kubernetes cluster on &lt;a rel=&quot;nofollow&quot; href=&quot;http://%20https://www.awin1.com/cread.php?awinmid=123996&amp;amp;awinaffid=2852319&amp;amp;clickref=kubernetes&amp;amp;ued=https%3A%2F%2Fwww.digitalocean.com%2Fproducts%2Fkubernetes&quot;&gt;DigitalOcean&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can follow my work and projects on &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ijaydimples&quot;&gt;Twitter&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/ijeoma-igboagu/&quot;&gt;LinkedIn&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ijayhub&quot;&gt;GitHub&lt;/a&gt;, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/@cloudinreallife&quot;&gt;YouTube&lt;/a&gt;, where I share what I am building and learning along the way.&lt;/p&gt;
&lt;p&gt;If this video helped you understand Kubernetes better, consider subscribing for more &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/@cloudinreallife&quot;&gt;real-life cloud and DevOps stories.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thank you for reading.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22896/how-a-football-match-helped-me-understand-kubernetes-desired-state-vs-actual-state</guid>
<pubDate>Sat, 18 Jul 2026 10:00:00 +0000</pubDate>
</item>
<item>
<title>ReRute: Treating Physical Freight Like Packets on a Network. Kubernetes and Docker on Logistics</title>
<link>https://coderlegion.com/21508/rerute-treating-physical-freight-like-packets-network-kubernetes-and-docker-logistics</link>
<description>&lt;p&gt;&lt;strong&gt;How we combined Go, Elixir, ScyllaDB, and Kotlin to replace manual dispatching with autonomous reconciliation loops and instant smart-contract settlements.&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2102812375019543864&quot; alt=&quot;&quot;&gt;&lt;br&gt;
If you have ever peered behind the curtain of modern logistics and freight forwarding, you know a dark secret: global supply chains run on spreadsheets, phone calls, static whiteboards, and net-60 invoicing.&lt;/p&gt;
&lt;p&gt;When a shipper needs to move 500 kilograms of temperature-sensitive pharmaceuticals across a city or country, they typically negotiate with a middleman broker. A specific driver and vehicle are manually assigned to a static route. But the physical world is chaotic. Traffic jams happen. Refrigeration units fail. Trucks break down. When an anomaly occurs today, human dispatchers scramble to make frantic phone calls while cargo spoils and drivers wait months for payment.&lt;/p&gt;
&lt;p&gt;We built ReRute to solve this foundational friction. Instead of treating logistics as a series of rigid, manual bookings, ReRute introduces a Declarative Physical Asset Orchestration Protocol. We treat physical freight like packets routing dynamically across a digital network.&lt;/p&gt;
&lt;p&gt; The Paradigm Shift: Declarative Orchestration&lt;br&gt;
In software engineering, Kubernetes revolutionized infrastructure by popularizing declarative orchestration. You don't tell the server how to start processes step-by-step; you declare a Desired State (&quot;I want 3 replicas of this container running&quot;), and an autonomous controller continuously monitors the Actual State, reconciling the two until they match.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ReRute applies this exact philosophy to physical atoms:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cargo Pods (Desired State)&lt;/strong&gt;: Shippers use the Client Portal to declare a Cargo Pod Manifest. They don't book a specific driver. They declare requirements: Origin, Destination, Weight ($kg$), Volume ($m^3$), Quote Amount, and Environmental Constraints (e.g., refrigerated: true).&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=690062542037878521&quot; alt=&quot;&quot;&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5621325827215555904&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;strong&gt;Transport Nodes (Actual State):&lt;/strong&gt; Drivers operate vehicles equipped with our headless Android Node Agent. The node registers its physical capabilities (max load capacity, refrigeration status) and continuously streams real-time physical telemetry (GPS coordinates, vector speed, heading).&lt;br&gt;
&lt;strong&gt;Continuous Reconciliation:&lt;/strong&gt; A centralized control plane continuously compares pending Cargo Pods against active Transport Nodes, autonomously assigning, routing, and rerouting cargo based on proximity, capacity, and environmental compliance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Architecture: Bottom-Up Design&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=18075814246538785289&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Storage Layer: Sub-Millisecond Telemetry with ScyllaDB&lt;/strong&gt;&lt;br&gt;
Tracking thousands of moving transport nodes generating GPS pings every 10 seconds creates a massive, high-frequency write load. Relational databases choke on this time-series ingestion.&lt;/p&gt;
&lt;p&gt;We built our storage foundation on ScyllaDB, a hyper-fast C++ NoSQL database compatible with Apache Cassandra. Using partitioned time-window compaction strategies (TimeWindowCompactionStrategy), ScyllaDB absorbs millions of spatial-temporal coordinate pings (latitude, longitude, speed_kmh, heading_degrees) with sub-millisecond latency while maintaining absolute cluster state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The Orchestration Brain: Go Control Plane&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17654428696066075405&quot; alt=&quot;&quot;&gt;&lt;br&gt;
At the heart of ReRute sits the Control Plane, written in Go for maximum concurrency and minimal memory footprint.&lt;/p&gt;
&lt;p&gt;Every five seconds, the Go API runs a background reconciliation loop. It queries ScyllaDB for all unassigned Cargo Pods (Pending_Pickup) and scans the spatial registry for available Transport Nodes (Available) that satisfy all physical constraints. When a match is found, state is atomic-locked and pushed to the driver's device.&lt;/p&gt;
&lt;p&gt;More importantly, the Control Plane is self-healing. If an active driver experiences a mechanical failure and taps &quot;Report Breakdown&quot; on their device, the Go reconciliation engine immediately cordons that node (Cordoned), releases the Cargo Pod back into the routing pool, and autonomously re-dispatches the nearest available transport node to perform a mid-route pickup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Financial Rails: Elixir &amp;amp; The Daraja Smart Escrow Engine&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12955264538064514095&quot; alt=&quot;&quot;&gt;&lt;br&gt;
In logistics, trust is tied directly to cash flow. Drivers often wait 30 to 90 days to get paid by brokers. ReRute eliminates net-terms entirely using automated digital escrow.&lt;/p&gt;
&lt;p&gt;Our Escrow Engine is built in Elixir, leveraging the Erlang BEAM VM for unparalleled fault tolerance and concurrency. When a client initiates a shipment, upfront payment is locked in a stateful liability machine: PENDING_PICKUP ➔ IN_TRANSIT ➔ NODE_LOCKED ➔ SETTLED.&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16802845357674606287&quot; alt=&quot;&quot;&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7637997795610468080&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;To handle mobile money payments across East Africa, our Elixir engine integrates directly with Safaricom’s Daraja API. Because cellular network latency can cause payment webhooks to duplicate or lag, the engine enforces strict idempotency using Redis and Elixir GenServer supervisors.&lt;/p&gt;
&lt;p&gt;The exact second a driver confirms package handover on their mobile device, the physical state matches the desired state. The Elixir state machine instantly transitions to SETTLED, calculates platform margins, and fires an automated B2C payout directly to the driver's M-Pesa mobile wallet. Settlement drops from 60 days to 600 milliseconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. The Physical Edge: Kotlin Node Agent&lt;/strong&gt;&lt;br&gt;
To ensure reliable telemetry without draining battery life, the mobile edge is built natively in Kotlin. Running as a headless background daemon on Android devices, the Node Agent wakes up on a precise ticker, reads physical sensors (ACCESS_FINE_LOCATION), computes vector trajectories, and transmits lightweight JSON payloads over secure Cloudflare tunnels.&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15830158321941833809&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5454356562915482937&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=37118543913243071&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Future of Autonomous Supply Chains&lt;/strong&gt;&lt;br&gt;
ReRute demonstrates what is possible when we stop viewing physical logistics as a human-coordination problem and start treating it as a distributed software protocol. By combining ultra-fast time-series databases, concurrent Go reconciliation engines, and fault-tolerant Elixir financial state machines, we create a supply chain that is transparent, autonomous, and self-healing.&lt;/p&gt;
&lt;p&gt;Whether moving pharmaceuticals across Nairobi or managing heavy freight across continents, declarative physical orchestration ensures that cargo always finds its optimal route home.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.loom.com/share/38a71ab290094b169dbc8d2ca904c311&quot;&gt;WATCH DEMO VIDEO&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.loom.com/share/4cf67e31b3f944a587cb7cbe78ce5e11&quot;&gt;WATCH DEMO VIDEO 2&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;THIS IS STILL A WORK IN PROGRESS BUT IF YOU ARE WILLING TO WORK WITH&lt;br&gt;
ME THEN WE CAN TURN IT INTO A WORKING PRODUCT&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/21508/rerute-treating-physical-freight-like-packets-network-kubernetes-and-docker-logistics</guid>
<pubDate>Sun, 28 Jun 2026 14:51:44 +0000</pubDate>
</item>
<item>
<title>Deploying a Simple App on K3S in AWS EC2 with GitHub Actions &amp; ECR</title>
<link>https://coderlegion.com/13420/deploying-a-simple-app-on-k3s-in-aws-ec2-with-github-actions-ecr</link>
<description>&lt;hr&gt;
&lt;p&gt;title: Deploying a Simple App on K3S in AWS EC2 with GitHub Actions &amp;amp; ECR&lt;br&gt;
published: true&lt;br&gt;
date: 2025-09-07 12:15:31 UTC&lt;br&gt;
tags: github,kubernetes,kubernetescluster,githubactions&lt;/p&gt;
&lt;h3 id=&quot;canonical-url-https-medium-com-mahinshanazeer-deploying-a-simple-app-on-k3s-in-aws-ec2-with-github-actions-ecr-58e0ea29eb2a&quot;&gt;canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@mahinshanazeer/deploying-a-simple-app-on-k3s-in-aws-ec2-with-github-actions-ecr-58e0ea29eb2a&quot;&gt;https://medium.com/@mahinshanazeer/deploying-a-simple-app-on-k3s-in-aws-ec2-with-github-actions-ecr-58e0ea29eb2a&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*_sZvnHHzeRX_BM1ww8GK-A.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;In this session, we’ll walk through the configuration of K3S on an EC2 instance and deploy a multi-container application with a frontend, backend, and database. The application will run inside a Kubernetes cluster using Deployments and StatefulSets in headless mode. For the setup, we’ll use EC2 to host the cluster, GitHub as our code repository, and GitHub Actions to implement CI/CD.&lt;/p&gt;
&lt;p&gt;If you’re an absolute beginner and not familiar with configuring EC2, I recommend checking out my blog here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/mahinshanazeer/step-by-step-guide-to-launching-an-ec2-instance-on-aws-for-beginners-1ak8&quot;&gt;Step-by-Step Guide to Launching an EC2 Instance on AWS : For Beginners&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This will be an end-to-end project deployment designed for those learning K3S, CI/CD, and Docker. You’ll gain hands-on experience in setting up CI/CD pipelines, writing Dockerfiles, and using Docker Compose. We’ll then move on to deploying the application in K3S, working with Kubernetes manifests, and exploring key components such as Deployments, Services (NodePort and ClusterIP), ConfigMaps, Persistent Volumes (PV), Persistent Volume Claims (PVC), and StatefulSets.&lt;/p&gt;
&lt;p&gt;K3S is a lightweight Kubernetes distribution developed by Rancher (now SUSE). It’s designed to be:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Lightweight — small binary, minimal dependencies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Easy to install — single command installation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Optimized for edge, IoT, and small clusters — runs well on low-resource machines like Raspberry Pi or small EC2 instances.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;Fully compliant — supports all standard Kubernetes APIs and workloads.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In short, K3S simplifies Kubernetes and makes it resource-efficient, making it ideal for single-node clusters, test environments, and learning purposes.&lt;/p&gt;
&lt;p&gt;Log login to the EC2 machine and install k3s first:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://k3s.io/&quot;&gt;K3s&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can install K3S on your machine using the following single command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update -y &amp;amp;&amp;amp; sudo apt upgrade -y
curl -sfL https://get.k3s.io | sh - 
# Check for Ready node, takes ~30 seconds 
sudo k3s kubectl get node 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*WthGdHM5my4mFViKNuItYg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Installation of k3s&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once the installation is completed, the output should be similar to this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*u_KORCKe0I_kU2uL1SSUJw.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Kubectl node status&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once the cluster is up and running, we can move on to the application. You can refer to the following repository for the demo To-Do List app. Before cloning the repository, make sure Docker is installed on the machine to build and test the application. For installing Docker, refer to the following URL:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.docker.com/engine/install/ubuntu/&quot;&gt;Ubuntu&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#run the following command first to remove conficting packages

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release &amp;amp;&amp;amp; echo &quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&quot;) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null

sudo apt-get update

#Installing Docker
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

#Now verify the installtion.
sudo docker run hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let’s dive into the demo application. The Application Stack:  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frontend: React.js  &lt;/li&gt;
&lt;li&gt;Backend API: Node.js + Express  &lt;/li&gt;
&lt;li&gt;Database: MongoDB  &lt;/li&gt;
&lt;li&gt;Containerization &amp;amp; Registry: Docker + AWS ECR  &lt;/li&gt;
&lt;li&gt;Orchestration &amp;amp; Service Management: Kubernetes (K3s)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, let’s clone the application repository to your local machine.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mahinshanazeer/docker-frontend-backend-db-to_do_app.git&quot;&gt;GitHub - mahinshanazeer/docker-frontend-backend-db-to_do_app: Simple Application with Frontend + Backened + DB&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/mahinshanazeer/docker-frontend-backend-db-to_do_app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*sMBBrqHHy6RH0rEo4CxHAg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Clone the github application&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once the repository is cloned, switch to the application directory and check for the Docker Compose file.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*3SK0wgqcLyIlq2EApUyKpA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Directory structure&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  web:
    build:
      context: ./frontend
      args:
        REACT_APP_API_URL: ${REACT_APP_API_URL}
    depends_on:
      - api
    ports:
      - &quot;3000:80&quot;
    networks:
      - network-backend
    env_file:
      - ./frontend/.env

  api:
    build: ./backend
    depends_on:
      - mongo
    ports:
      - &quot;3001:3001&quot;
    networks: 
      - network-backend
  
  mongo:
    build: ./backend-mongo  
    image: docker-frontend-backend-db-mongo
    restart: always
    volumes: 
      - ./backend-mongo/data:/data/db
    environment: 
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: adminhackp2025
    networks: 
      - network-backend

networks:
  network-backend:

volumes: 
  mongodb_data:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the Docker Compose file, you’ll see sections for web, api, and mongo. Let’s dive into each directory and review the Dockerfiles. The Docker Compose file builds the Docker images using the Dockerfiles located in their respective directories.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
root@ip-172-31-22-24:/home/ubuntu/docker-frontend-backend-db-to_do_app/frontend# cd /home/ubuntu/docker-frontend-backend-db-to_do_app/frontend
root@ip-172-31-22-24:/home/ubuntu/docker-frontend-backend-db-to_do_app/frontend# cat Dockerfile 
# ---------- Build Stage ----------
FROM node:16-alpine AS build

WORKDIR /app

# Copy dependency files first
COPY package*.json ./

# Install dependencies
RUN npm install --legacy-peer-deps

# Copy rest of the app
COPY . .

# Build the React app
RUN npm run build

# ---------- Production Stage ----------
FROM nginx:alpine

# Copy custom nginx config if you have one
# COPY nginx.conf /etc/nginx/conf.d/default.conf

# Copy build output from build stage
COPY --from=build /app/build /usr/share/nginx/html

# Expose port 80
EXPOSE 80

# Start nginx
CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]

***

root@ip-172-31-22-24:/home/ubuntu/docker-frontend-backend-db-to_do_app/backend# cd /home/ubuntu/docker-frontend-backend-db-to_do_app/backend
root@ip-172-31-22-24:/home/ubuntu/docker-frontend-backend-db-to_do_app/backend# cat Dockerfile 
FROM node:10-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3001

***

root@ip-172-31-22-24:/home/ubuntu/docker-frontend-backend-db-to_do_app/backend# cd /home/ubuntu/docker-frontend-backend-db-to_do_app/backend-mongo/
root@ip-172-31-22-24:/home/ubuntu/docker-frontend-backend-db-to_do_app/backend-mongo# cat Dockerfile 
FROM mongo:6.0
EXPOSE 27017
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open the .env file in the frontend directory and update the IP address to your EC2 public IP. This environment variable is used by the frontend to connect to the backend, which runs on port 3001.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vi /home/ubuntu/docker-frontend-backend-db-to_do_app/frontend/.env

#edit the IP address, I have updated my EC2 public IP
~~~
REACT_APP_API_URL=http://54.90.185.176:3001/
~~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can also cross-check the total number of APIs using the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep -R &quot;router.&quot; backend/ | grep &quot;(&quot;
grep -R &quot;app.&quot; backend/ | grep &quot;(&quot;
grep -R &quot;app.&quot; backend/ | grep &quot;(&quot; | wc -l
grep -R &quot;router.&quot; backend/ | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s test the application by spinning up the containers. Navigate back to the project’s root directory and run the Docker Compose command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /home/ubuntu/docker-frontend-backend-db-to_do_app
docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you run the command, Docker will start building the images and spin up the containers as soon as the images are ready&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*aADXzg2UJlfmx_iao7tazg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;building docker containers&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Wait until you see the ‘built’ and ‘created’ messages. Once the containers are up and running, use docker ps -a to verify the status.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*l7deKMuT9cRlE99Bncv8Jw.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;build completed and containers started.&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker ps -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*_2gFMleEoB5YohK868DJdg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;docker processes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once the Docker containers are up and running, verify that the application is working as expected.&lt;/p&gt;
&lt;p&gt;Once the Docker containers are up and running, verify that the application is working as expected. Open the server’s IP address on port 3000. You can confirm the mapped ports in the Docker Compose file or by checking the docker ps -a output. Here, port 3000 is for the frontend web app, port 3001 is for the backend, and MongoDB runs internally on port 27017 without public access. In this example, load the website by entering 54.90.185.176:3000 in your browser.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*IBWj0PwOHw16LSe-DSv-iQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Application interface&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you’re using Chrome, right-click anywhere on the page and open Inspect &amp;gt; Network. Then click on Add Todo to verify that the list updates correctly, and the network console shows a 200 status response&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*9RjZ4aleKcaqonPj3T2J9Q.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;checking the network&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/994/1*WKFtfbYRAqQiM7FSZOeDPQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Application testing&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Click on the buttons and try to add new file, and verify the status codes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*7-biYdl98qtwip0M7q_7QQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Testing&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So far, everything looks good. Now, let’s proceed with the Kubernetes deployment. To configure resources in Kubernetes, we’ll need to create manifest files in YAML format. You can create these files as shown below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir /home/ubuntu/manifest
touch api-deployment.yaml api-service.yaml image_tag.txt mongo-secret.yaml mongo-service.yaml mongo-statefulset-pv-pvc.yaml web-deployment.yaml web-env-configmap.yaml web-service.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now edit each file and add the following contents:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;api-deployment.yaml:&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Defines how the backend API should run inside the cluster.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Creates 2 replicas of the API for reliability.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Uses environment variables from secrets for MongoDB authentication.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Ensures the API pods always restart if they fail.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt; Importance: Provides scalability and fault tolerance for the backend service.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Rolling Update: Gradually replaces old pods with new ones. Uses fewer resources, minimal downtime if tuned, but users may hit bad pods if the new version is faulty.&lt;/p&gt;
&lt;p&gt; Rolling = efficient and native.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  labels:
    app: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 3
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: 495549341534.dkr.ecr.us-east-1.amazonaws.com/hackp2025:api-20250907111542
          ports:
            - containerPort: 3001
          env:
            - name: MONGO_INITDB_ROOT_USERNAME
              valueFrom:
                secretKeyRef:
                  name: mongo-secret
                  key: username
            - name: MONGO_INITDB_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mongo-secret
                  key: password
      restartPolicy: Always
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;api-service.yaml&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Exposes the API deployment to the outside world.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Type NodePort makes the service reachable via :31001.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Ensures frontend or external clients can communicate with the backend.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt; Importance: Acts as a bridge between users/frontend and the backend API.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: api
  labels:
    app: api
spec:
  type: NodePort
  selector:
    app: api
  ports:
    - port: 3001 # internal cluster port
      targetPort: 3001 # container port
      nodePort: 31001 # external port on the node
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;mongo-secret.yaml&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Stores sensitive information (username &amp;amp; password) in base64-encoded format.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Used by both the API and MongoDB.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Keeps credentials out of plain-text manifests.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt; Importance: Secure way to handle database credentials.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Secret
metadata:
  name: mongo-secret
type: Opaque
data:
  # Base64 encoded values
  username: YWRtaW4= # &quot;admin&quot;
  password: YWRtaW5oYWNrcDIwMjU= # &quot;adminhackp2025&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;mongo-service.yaml&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Defines the MongoDB service.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;- ClusterIP: None&lt;/strong&gt; makes it a &lt;strong&gt;headless service&lt;/strong&gt; , required for StatefulSets.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Allows pods to connect to MongoDB by DNS (e.g., mongo-0.mongo).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt; Importance: Provides stable networking for MongoDB StatefulSet pods.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: mongo
  labels:
    app: mongo
spec:
  ports:
    - port: 27017
      targetPort: 27017
  selector:
    app: mongo
  clusterIP: None # headless service for StatefulSet
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;mongo-statefulset-pv-pvc.yaml&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Handles the &lt;strong&gt;database persistence and StatefulSet definition&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;- PersistentVolume (PV):&lt;/strong&gt; Reserves storage (5Gi).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;- PersistentVolumeClaim (PVC):&lt;/strong&gt; Ensures pods can claim storage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;- StatefulSet:&lt;/strong&gt; Guarantees stable network identity and persistent storage for MongoDB.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt; Importance: Ensures &lt;strong&gt;MongoDB data is preserved&lt;/strong&gt; even if the pod restarts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Blue/Green Deployment: Runs two environments (Blue = live, Green = new). Traffic is switched instantly once Green is ready. Near-zero downtime and easy rollback, but requires double resources and is more complex for stateful apps.&lt;/p&gt;
&lt;p&gt; Blue/Green = safer cutover, higher cost.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# PersistentVolume for Green MongoDB
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongo-green-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /root/hackpproject/data-green # separate path for green
  persistentVolumeReclaimPolicy: Retain
  storageClassName: &quot;&quot; # Must match PVC in StatefulSet

---

# StatefulSet for Green MongoDB
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongo-green
  labels:
    app: mongo
    version: green
spec:
  serviceName: mongo # existing headless service
  replicas: 1
  selector:
    matchLabels:
      app: mongo
      version: green
  template:
    metadata:
      labels:
        app: mongo
        version: green
    spec:
      containers:
        - name: mongo
          image: 495549341534.dkr.ecr.us-east-1.amazonaws.com/hackp2025:db-20250907111542
          ports:
            - containerPort: 27017
          env:
            - name: MONGO_INITDB_ROOT_USERNAME
              valueFrom:
                secretKeyRef:
                  name: mongo-secret
                  key: username
            - name: MONGO_INITDB_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mongo-secret
                  key: password
          volumeMounts:
            - name: mongo-data
              mountPath: /data/db
  volumeClaimTemplates:
    - metadata:
        name: mongo-data
      spec:
        accessModes: [&quot;ReadWriteOnce&quot;]
        resources:
          requests:
            storage: 5Gi
        storageClassName: &quot;&quot; # binds to the pre-created PV
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;web-deployment.yaml&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Defines how the &lt;strong&gt;frontend (React.js app)&lt;/strong&gt; should run.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Runs &lt;strong&gt;2 replicas&lt;/strong&gt; for high availability.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Pulls &lt;strong&gt;API endpoint from ConfigMap&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Resource requests/limits ensure fair scheduling.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt; Importance: Deploys the &lt;strong&gt;UI&lt;/strong&gt; and links it to the backend API via config.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Rolling Update: Gradually replaces old pods with new ones. Uses fewer resources, minimal downtime if tuned, but users may hit bad pods if the new version is faulty.&lt;/p&gt;
&lt;p&gt; Rolling = efficient and native.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 3         
      maxUnavailable: 0   
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: 495549341534.dkr.ecr.us-east-1.amazonaws.com/hackp2025:web-20250907111542
          ports:
            - containerPort: 3000
              protocol: TCP
          env:
            - name: REACT_APP_API_URL
              valueFrom:
                configMapKeyRef:
                  name: web-env
                  key: REACT_APP_API_URL
          resources:
            requests:
              cpu: &quot;200m&quot;
              memory: &quot;1024Mi&quot;
            limits:
              cpu: &quot;2&quot;
              memory: &quot;2Gi&quot;
      restartPolicy: Always
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;web-env-configmap.yaml&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Stores non-sensitive environment variables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Defines the &lt;strong&gt;API endpoint&lt;/strong&gt; for the frontend (REACT_APP_API_URL).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Can be updated easily without rebuilding Docker images.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt; Importance: Provides flexibility to change configuration without redeploying code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: web-env
  labels:
    app: web
data:
  REACT_APP_API_URL: http://98.86.216.31:31001
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;web-service.yaml&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Exposes the frontend to users.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;- Type NodePort&lt;/strong&gt; makes it available externally at :32000.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;ul&gt;
&lt;li&gt;Maps port 3000 (service) → port 80 (container).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt; Importance: Allows &lt;strong&gt;end-users to access the web app&lt;/strong&gt; from their browser.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: web
  labels:
    app: web
spec:
  type: NodePort
  selector:
    app: web # Must match Deployment labels
  ports:
    - name: http
      port: 3000 # Service port inside cluster
      targetPort: 80 # Container port
      nodePort: 32000 # External port accessible from outside
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have now moved all the manifest files to /root/hackpproject/manifestfiles.&lt;/p&gt;
&lt;p&gt;Once the manifests are finalised, the next step is to create a repository in ECR to push the build artefact images.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Steps to Create an ECR Repository:&lt;br&gt;
1.Log in to AWS Console → Go to the ECR service.  &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create Repository  &lt;/li&gt;
&lt;li&gt;Click Create repository.  &lt;/li&gt;
&lt;li&gt;Select Private repository.  &lt;/li&gt;
&lt;li&gt;Enter repository names — prodimage. (In this case, we are creating a single repository for all those 3 images)  &lt;/li&gt;
&lt;li&gt;Leave others as default and click Create repository.  &lt;/li&gt;
&lt;li&gt;Authenticate Docker with ECR&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*tMvjSvSO_Unn3MZgrgSnEA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Step 1: Finding the ECR&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*mqSNSwlf5a8EgXNNPf9SRQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Step 2: Creating Repository&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*mTD3xMeYfWtP12A24O_NLA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Step 3: onfiguring Repository&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once the registry is created, you can proceed with the CI/CD pipeline.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*wpOVbkhkdpbgqnPF080prA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Reposiroty end point&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, let’s create a GitHub Actions pipeline to deploy the code to the EC2 K3S cluster. The first step is to configure GitHub Actions with access to the repository, ECR, and the EC2 instance via SSH.&lt;/p&gt;
&lt;p&gt;Navigate to the project directory and create a folder named ‘.github’. Inside this folder, create a file named ‘ci-cd.yml’.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir .github
cd .github
touch ci-cd.yml
vi ci-cd.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ci-cd.yml file is the core configuration file for GitHub Actions that defines your CI/CD pipeline. Now use the following script in that ci-cd.yml file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: CI/CD Pipeline

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13420/deploying-a-simple-app-on-k3s-in-aws-ec2-with-github-actions-ecr</guid>
<pubDate>Tue, 24 Mar 2026 10:36:12 +0000</pubDate>
</item>
<item>
<title>Upgrading Our Raspberry Pi Kubernetes Cluster: From v1.29 to v1.34 &amp; ETCD Backup Guide</title>
<link>https://coderlegion.com/13289/upgrading-our-raspberry-pi-kubernetes-cluster-from-v1-29-to-v1-34-etcd-backup-guide</link>
<description>&lt;hr&gt;
&lt;p&gt;title: Upgrading Our Raspberry Pi Kubernetes Cluster: From v1.29 to v1.34 &amp;amp; ETCD Backup Guide&lt;br&gt;
published: true&lt;br&gt;
date: 2025-10-16 14:35:59 UTC&lt;br&gt;
tags: raspberrypi,kubernetes,etcd,kubernetescluster&lt;/p&gt;
&lt;h3 id=&quot;canonical-url-https-medium-com-mahinshanazeer-upgrading-our-raspberry-pi-kubernetes-cluster-from-v1-29-to-v1-34-etcd-backup-guide-97a2074237b4&quot;&gt;canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@mahinshanazeer/upgrading-our-raspberry-pi-kubernetes-cluster-from-v1-29-to-v1-34-etcd-backup-guide-97a2074237b4&quot;&gt;https://medium.com/@mahinshanazeer/upgrading-our-raspberry-pi-kubernetes-cluster-from-v1-29-to-v1-34-etcd-backup-guide-97a2074237b4&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*D0P7sopIzTaubncyFnNPag.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Keeping your cluster up-to-date is crucial for security, stability, and access to new features. In this blog, we’ll take a closer look at our home lab Kubernetes cluster running on Raspberry Pi devices. The setup includes 1 master node and 3 worker nodes, and it has been running smoothly for over four months without any major issues.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/702/1*RISM21an6y-TUght4dRieg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;k8s home lab configuration — nodes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;master2 — master node/ control plane&lt;br&gt;
master1 — worker node 3&lt;br&gt;
worker 1 — worker node 1&lt;br&gt;
worker 2 — worker node 2&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*AJBUK-y3Yy_dK1vLvcOLlw.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;k8s home lab configuration — system components&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As part of regular maintenance and to stay up-to-date with the latest features and security patches, we decided to upgrade the cluster from Kubernetes v1.29 to v1.34. Alongside the upgrade, we’ll also walk through the process of backing up the ETCD datastore, which is crucial for fault tolerance and disaster recovery.&lt;/p&gt;
&lt;p&gt;Here’s what we’ll cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;⬆️ Step-by-step guide to upgrading Kubernetes components.&lt;/li&gt;
&lt;li&gt;️ Safely backing up ETCD in the master node.&lt;/li&gt;
&lt;li&gt;✅ Post-upgrade verification.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;a-backing-up-etcd&quot;&gt;A. Backing Up ETCD&lt;/h4&gt;
&lt;p&gt;Before diving into the upgrade process, it’s essential to create a backup of ETCD, the key-value store that holds all cluster state data. This step is crucial because if anything goes wrong during the upgrade — whether it’s a misconfiguration or a failed component — we need a reliable way to restore the cluster to its current stable state.&lt;/p&gt;
&lt;p&gt;Think of the ETCD backup as your safety net. It ensures that even in the worst-case scenario, you won’t lose your cluster’s configuration, workloads, or networking setup.&lt;/p&gt;
&lt;p&gt;Run the following command to confirm the ETCD pod name running on your control plane:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pods -n kube-system | grep etcd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*kCOudifkDT-Bb0uCn8rJ2A.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;k8s configuration — ETCD setup&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Suppose your control plane (master node) runs ETCD as a static pod (which is the default in kubeadm-based clusters, including Raspberry Pi setups). In that case, you can run etcdctl directly on the master node host — because the ETCD data directory and certificates are mounted locally at &lt;em&gt;/etc/kubernetes/pki/etcd/&lt;/em&gt; and &lt;em&gt;/var/lib/etcd/.&lt;/em&gt; In that case, you can use the following command to take the backup:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ETCDCTL_API=3 etcdctl snapshot save /var/lib/etcd/backup.db \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If ETCD runs as a container (inside the pod) or you don’t have the etcdctl binary available on the host, then you’ll need to exec into the pod to run the backup from inside the container.&lt;/p&gt;
&lt;p&gt;In my setup, where &lt;strong&gt;etcd&lt;/strong&gt; runs as a container, I need to access the pod directly to execute the backup. There is no etcdctl binary available on the host.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl exec -n kube-system &amp;lt;node-label&amp;gt;-- \
  etcdctl snapshot save /var/lib/etcd/backup.db \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key

#edit the &amp;lt;node-label&amp;gt; with etcd node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*xlWBIPqp8McKoOjKxkbKjg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Creating backup&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, verify the snapshot using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl exec -n kube-system &amp;lt;node-label&amp;gt;-- \
  etcdctl snapshot status /var/lib/etcd/backup.db -w table
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/870/1*9GsdIbKWKprGPs_Seoc2kA.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In the above screenshot, you can seee a deprecation warning. ETCD v3.6+ is deprecated &lt;em&gt;etcdctl snapshot status&lt;/em&gt; and recommends using &lt;em&gt;etcdutl snapshot status&lt;/em&gt;. However, in my kubeadm static pod setup, the &lt;em&gt;etcdutl&lt;/em&gt; binary is not included in the ETCD container. Only &lt;em&gt;etcdctl&lt;/em&gt; exists inside the pod. The warning is simply a deprecation notice, not a failure. You can safely continue using it &lt;em&gt;etcdctl snapshot status&lt;/em&gt; — it works perfectly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In my setup, I came across a deprecation warning while using etcdctl. To verify, I checked the current version of etcd using the following command.&lt;/p&gt;
&lt;p&gt;Since my cluster is kubeadm-managed, even though etcd v3.6+ recommends using etcdutl for snapshot management, etcdctl inside the etcd pod still works perfectly for backup and restore. etcdutl is a newer utility introduced in etcd v3.6+ as a replacement for certain snapshot operations, but in my case, continuing with etcdctl is fully sufficient.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl exec -n kube-system etcd-master2 -- etcdctl version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*YP_D_wTZ1iGmGXza0cm2OQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;etcd version checking&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In some scenarios, the etcd backup file may reside only inside the etcd pod and not in a host-accessible directory.&lt;/p&gt;
&lt;p&gt;This can happen if the etcd container is minimal and lacks tools like tar to move files around, or if the host does not have etcdctl installed to create the snapshot directly. In such cases, you can use specific commands to move the backup file to a host-accessible directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl cp kube-system/etcd-master2:/tmp/backup.tar.gz ./backup.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my setup, I created the etcd snapshot directly on the host in a host-accessible directory (for example, /var/lib/etcd/backup.db). Since the backup is already on the host, it can be copied or archived without using kubectl cp.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo cp /var/lib/etcd/backup.db ~/backup.db
sudo chown $USER:$USER ~/backup.db
ls -l ~/backup.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/855/1*0_RkF8kgG0E7bHsZgk3ywA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;backup.db file status&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Additionally, it’s a good practice to create a copy of the backup on a remote server. This ensures an extra layer of safety and helps protect against data loss in case of hardware failure or accidental deletion.&lt;/p&gt;
&lt;h4 id=&quot;b-upgrading-the-cluster&quot;&gt;B. Upgrading the cluster&lt;/h4&gt;
&lt;p&gt;Since we have backed up etcd, we can safely proceed with the kubeadm upgrade for our Raspberry Pi Kubernetes cluster. Let’s go through a step-by-step process to upgrade the cluster from v1.29 → v1.34.&lt;/p&gt;
&lt;p&gt;Kubernetes officially supports upgrading one minor version at a time, which means that to move from 1.29 to 1.34, you need to perform incremental upgrades:&lt;/p&gt;
&lt;p&gt;Step 1: v1.29 → v1.30 &lt;br&gt;
Step 2: v1.30 → v1.31&lt;br&gt;
Step 3: v1.31 → v1.32&lt;br&gt;
Step 4: v1.32 → v1.33&lt;br&gt;
Step 5: v1.33 → v1.34&lt;/p&gt;
&lt;p&gt;Please refer to the following URL for k8s official documentation on skew policy:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://v1-30.docs.kubernetes.io/releases/version-skew-policy/&quot;&gt;Version Skew Policy&lt;/a&gt;&lt;/p&gt;
&lt;h5 id=&quot;step-1-pre-checks-for-upgrading-the-cluster&quot;&gt;Step 1: Pre-Checks for upgrading the cluster&lt;/h5&gt;
&lt;p&gt;Before upgrading the cluster, it’s essential to ensure that the cluster is healthy and stable. This helps prevent issues during the upgrade and ensures a smooth transition between versions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl version
kubectl get nodes
kubectl get cs
kubectl get pods -n kube-system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*87FwxUqQ2TSc9YgpXHZPXw.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Current status of the cluster&lt;/em&gt;&lt;/p&gt;
&lt;h5 id=&quot;step-2-upgrade-kubeadm-on-the-master-node&quot;&gt;Step 2: Upgrade kubeadm on the Master Node&lt;/h5&gt;
&lt;p&gt;You can also refer to the following documentation for more details regarding kubeadm upgrade:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://v1-30.docs.kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/&quot;&gt;Upgrading kubeadm clusters&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To upgrade to v1.30, ensure that the directory /etc/apt/keyrings exists. If it doesn’t, create it before running the curl command (see the note below). Afterwards, update the package repository to target v1.30.&lt;/p&gt;
&lt;p&gt;Each Kubernetes minor release repository (v1.29, v1.30, v1.31, etc.) may use a different GPG key. Using the key from an older version, such as v1.29, can cause package verification failures when installing packages from the v1.30 repository.&lt;/p&gt;
&lt;p&gt;To prevent such errors, always use the GPG key and repository provided specifically for the minor version you are upgrading to.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.
# sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

# This overwrites any existing configuration in /etc/apt/sources.list.d/kubernetes.list
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before upgradingkubeadmEnsure your package lists are up to date. Then, install the required version by using the following commands&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-mark unhold kubelet kubeadm kubectl &amp;amp;&amp;amp; \
sudo apt-get update
sudo apt-get install -y kubeadm=1.30.14-1.1 kubelet=1.30.14-1.1 kubectl=1.30.14-1.1
sudo apt-mark hold kubelet kubeadm kubectl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to upgrade to a specific Kubernetes version, you can use the following command to list all available minor versions in the corresponding repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt-cache madison kubeadm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/978/1*RDcuF8IS82lXJ05pf4RgwQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;available packages in the repo&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For comprehensive guidance on upgrading your Kubernetes cluster to version &lt;strong&gt;v1.30&lt;/strong&gt; , refer to the official Kubernetes documentation:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot;&gt;Installing kubeadm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This resource provides detailed, step-by-step instructions tailored for clusters managed with &lt;strong&gt;kubeadm&lt;/strong&gt; , ensuring a smooth and efficient upgrade process.&lt;/p&gt;
&lt;p&gt;Once the upgrade is complete, verify the &lt;strong&gt;kubeadm&lt;/strong&gt; version using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubeadm version
kubectl version --client
kubelet --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*z4cxNAMceG76DxwAJF3s6g.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;current kubeadm version&lt;/em&gt;&lt;/p&gt;
&lt;h5 id=&quot;step-3-plan-the-upgrade&quot;&gt;Step 3: Plan the Upgrade&lt;/h5&gt;
&lt;p&gt;After upgrading kubeadm, it’s important to check the upgrade path and verify the versions of all cluster components. This ensures that all parts of your Kubernetes cluster are compatible and running the expected versions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm upgrade plan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It will display:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;current cluster version&lt;/strong&gt; and control plane version.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;available target versions&lt;/strong&gt; you can upgrade to.&lt;/li&gt;
&lt;li&gt;The versions of &lt;strong&gt;kubelet&lt;/strong&gt; and &lt;strong&gt;kubectl&lt;/strong&gt; on your nodes.&lt;/li&gt;
&lt;li&gt;Any &lt;strong&gt;required steps or notes&lt;/strong&gt; for a smooth upgrade.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*15KMi7LmDU8VNoAp1yaaSw.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;kubeadm upgrade plan output — 1&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*iYYt88VaUFyo_7Se5QW1kQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;kubeadm upgrade plan output — 2&lt;/em&gt;&lt;/p&gt;
&lt;h5 id=&quot;step-4-upgrade-the-control-plane&quot;&gt;Step 4: Upgrade the Control Plane&lt;/h5&gt;
&lt;p&gt;Once the upgrade plan is reviewed and all changes are understood, you can proceed with upgrading the cluster. Use the kubeadm upgrade apply command, which is provided in the output of kubeadm upgrade plan.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm upgrade apply v1.30.14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The upgrade process may take some time, depending on the size and complexity of your cluster. Once it completes successfully, you should see a message similar to the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*uTURS7u-pxMBDoiHaUpdMg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;upgraded to “v1.30.14”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now, verify the control plane using the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#rebooting the master node or restarting the kubelete is highly recommended
sudo systemctl daemon-reload
sudo systemctl restart kubelet
kubectl get nodes
kubectl get pods -n kube-system
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;step-5-upgrade-worker-nodes&quot;&gt;Step 5: Upgrade Worker Nodes&lt;/h5&gt;
&lt;p&gt;Now repeat the same steps from ‘Step 2: Upgrade kubeadm on the Master Node’ again in master1, worker1 and worker2 nodes (worker nodes). Workers do not control the cluster state; running upgrade plan there doesn’t provide meaningful information. First drain the worker node from the control plane, so that all the resources will be taken out of the node&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh to controlplane (master2)
-------------------------------
#step 1:
kubectl drain &amp;lt;worker-node&amp;gt; --ignore-daemonsets --delete-emptydir-data

***
kubectl drain worker1 --ignore-daemonsets --delete-emptydir-data
***
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*K82WBCjXW1xi6br5ep5r3A.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;draining resources from worker node (master1)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now you can repeat the steps we followed for control plane for upgrading the version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh to master1 node
-------------------------------
#step 1:
sudo rm -rf /etc/apt/sources.list.d/kubernetes.list

#step 2:
# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.
# sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
#step 3:
# This overwrites any existing configuration in /etc/apt/sources.list.d/kubernetes.list
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

#step 4:
sudo apt-mark unhold kubelet kubeadm kubectl &amp;amp;&amp;amp; \
sudo apt-get update
sudo apt-get install -y kubeadm=1.30.14-1.1 kubelet=1.30.14-1.1 kubectl=1.30.14-1.1
sudo apt-mark hold kubelet kubeadm kubectl

#step 5:
kubeadm version

#step 6:
#rebooting the master node or restarting the kubelete is highly recommended
sudo systemctl daemon-reload
sudo systemctl restart kubelet
kubectl get nodes
kubectl get pods -n kube-system

#step 7:
kubeadm upgrade node

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*X1wudh5rErmUwuoFw4yLFg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;upgrading worker node — host ‘master2’ is a worker node here. (master2 — master node/ control planemaster1 — worker node 3worker 1 — worker node 1worker 2 — worker node 2)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Also verify the nodes from control plane:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/852/1*BtfZU1oK2v7Mp4rqYZ7SzA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;upgrade completed&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now we can following commmand to make the node available for schedule;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl uncordon master1
#run this command in the control plane.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/671/1*9rBFG6wuuuSvW5donzFW1Q.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Marking node as schedulable&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;kubectl uncordon command is used to &lt;strong&gt;mark a node as schedulable again&lt;/strong&gt; after it has been drained.&lt;/p&gt;
&lt;p&gt;Similarly we can do the upgrades in other 2 worker nodes, I will mention the commands below:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;worker1:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh to controlplane (master2)
-------------------------------
#step 1:
kubectl drain &amp;lt;worker-node&amp;gt; --ignore-daemonsets --delete-emptydir-data

***
kubectl drain worker1 --ignore-daemonsets --delete-emptydir-data
***

ssh to worker1 node

#step 1:
sudo rm -rf /etc/apt/sources.list.d/kubernetes.list

#step 2:
# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.
# sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
#step 3:
# This overwrites any existing configuration in /etc/apt/sources.list.d/kubernetes.list
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

#step 4:
sudo apt-mark unhold kubelet kubeadm kubectl &amp;amp;&amp;amp; \
sudo apt-get update
sudo apt-get install -y kubeadm=1.30.14-1.1 kubelet=1.30.14-1.1 kubectl=1.30.14-1.1
sudo apt-mark hold kubelet kubeadm kubectl

#step 5:
kubeadm version

#step 6:
#rebooting the master node or restarting the kubelete is highly recommended
sudo systemctl daemon-reload
sudo systemctl restart kubelet

#step 7:
sudo kubeadm upgrade node

ssh to controlplane (master2)
-------------------------------
#step 1:
kubectl uncordon worker1
kubectl get nodes
kubectl get pods -n kube-system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;worker2:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh to controlplane (master2)
-------------------------------
#step 1:
kubectl drain &amp;lt;worker-node&amp;gt; --ignore-daemonsets --delete-emptydir-data

***
kubectl drain worker2 --ignore-daemonsets --delete-emptydir-data
***

ssh to worker2 node
-------------------------------
#step 1:
sudo rm -rf /etc/apt/sources.list.d/kubernetes.list

#step 2:
# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.
# sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
#step 3:
# This overwrites any existing configuration in /etc/apt/sources.list.d/kubernetes.list
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

#step 4:
sudo apt-mark unhold kubelet kubeadm kubectl &amp;amp;&amp;amp; \
sudo apt-get update
sudo apt-get install -y kubeadm=1.30.14-1.1 kubelet=1.30.14-1.1 kubectl=1.30.14-1.1
sudo apt-mark hold kubelet kubeadm kubectl

#step 5:
kubeadm version

#step 6:
#rebooting the master node or restarting the kubelete is highly recommended
sudo systemctl daemon-reload
sudo systemctl restart kubelet
kubectl get nodes
kubectl get pods -n kube-system

#step 7:
sudo kubeadm upgrade node

ssh to controlplane (master2)
-------------------------------
#step 1:
kubectl uncordon worker2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All nodes have now been successfully upgraded to &lt;strong&gt;v1.30.14&lt;/strong&gt; , as shown in the following screenshot.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/617/1*cX2D2v7kwsgAx5dpAu_CAA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;kubernetes version v1.30.14&lt;/em&gt;&lt;/p&gt;
&lt;h5 id=&quot;step-5-upgrading-to-1-34&quot;&gt;Step 5: Upgrading to 1.34&lt;/h5&gt;
&lt;p&gt;Following the same process, we’ll now proceed to upgrade the cluster from &lt;strong&gt;v1.30.14&lt;/strong&gt; to &lt;strong&gt;v1.31&lt;/strong&gt;. Then we will proceed with with&lt;/p&gt;
&lt;p&gt;Step 1: v1.29.15 → v1.30.14–1.1&lt;br&gt;
Step 2: v1.30.14–1.1 → v1.31.13–1.1&lt;br&gt;
Step 3: v1.31.13–1.1 → v1.32.9&lt;br&gt;
Step 4: v1.32.9 → v1.33.5&lt;br&gt;
Step 5: v1.33.5 → v1.34.1&lt;/p&gt;
&lt;p&gt;In control-plane (master2) use the following steps to upgrade to version 1.31:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh to controlplane (master2)

#step 1:
sudo rm -rf /etc/apt/sources.list.d/kubernetes.list

# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.
# sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

# This overwrites any existing configuration in /etc/apt/sources.list.d/kubernetes.list
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

#step 2:
sudo apt update
apt-cache madison kubeadm

#step 3:
sudo apt-mark unhold kubelet kubeadm kubectl &amp;amp;&amp;amp; \
sudo apt-get update
sudo apt-get install -y kubeadm kubelet kubectl #will take the latest version
sudo apt-mark hold kubelet kubeadm kubectl

#step 4:
kubeadm version
kubectl version --client
kubelet --version

#step 5:
sudo kubeadm upgrade plan

#step 6:
sudo kubeadm upgrade apply &amp;lt;version&amp;gt;
#I used the following command
sudo kubeadm upgrade apply v1.31.13

#step 7:
#rebooting the master node or restarting the kubelete is highly recommended
sudo systemctl daemon-reload
sudo systemctl restart kubelet
kubectl get nodes
kubectl get pods -n kube-system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now lets setup the worker nodes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh to controlplane (master2)
-------------------------------
#step 1:
kubectl drain &amp;lt;worker-node&amp;gt; --ignore-daemonsets --delete-emptydir-data

ssh to &amp;lt;worker-node&amp;gt; node
-------------------------------
#step 1:
sudo rm -rf /etc/apt/sources.list.d/kubernetes.list

#step 2:
# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.
# sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-key
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13289/upgrading-our-raspberry-pi-kubernetes-cluster-from-v1-29-to-v1-34-etcd-backup-guide</guid>
<pubDate>Sat, 21 Mar 2026 13:53:24 +0000</pubDate>
</item>
<item>
<title>Raspberry Pi K8S Cluster Setup for Home Lab with Cilium</title>
<link>https://coderlegion.com/12999/raspberry-pi-k8s-cluster-setup-for-home-lab-with-cilium</link>
<description>&lt;hr&gt;
&lt;p&gt;title: Raspberry Pi K8S Cluster Setup for Home Lab with Cilium&lt;br&gt;
published: true&lt;br&gt;
date: 2025-06-06 16:13:49 UTC&lt;br&gt;
tags: kubernetes,raspberrypi,docker,kubernetescluster&lt;/p&gt;
&lt;h3 id=&quot;canonical-url-https-medium-com-mahinshanazeer-raspberry-pi-k8s-cluster-setup-for-home-lab-with-cilium-c861f7815511&quot;&gt;canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@mahinshanazeer/raspberry-pi-k8s-cluster-setup-for-home-lab-with-cilium-c861f7815511&quot;&gt;https://medium.com/@mahinshanazeer/raspberry-pi-k8s-cluster-setup-for-home-lab-with-cilium-c861f7815511&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*7DQNW_xyewzN37E_e0oA2Q.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I’m setting up a Kubernetes cluster using Raspberry Pis for home lab testing and learning purposes. The cluster consists of four nodes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Master 1:&lt;/strong&gt; Raspberry Pi 5 (8GB RAM) 192.168.1.200&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Master 2:&lt;/strong&gt; Raspberry Pi 5 (4GB RAM) 192.168.1.201&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worker 1:&lt;/strong&gt; Raspberry Pi 3B+ 192.168.1.202&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worker 2:&lt;/strong&gt; Raspberry Pi 3B 192.168.1.203&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ve overclocked the Raspberry Pis to enhance performance. Below are the detailed specifications and overclocking configurations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@master1:/home/admin# neofetch
       _,met\(\)$gg. root@master1 
    ,g\(\)\(\)\(\)\($P. ------------ 
  ,g\)P&quot; &quot;&quot;&quot;Y\(.&quot;. OS: Debian GNU/Linux 12 (bookworm) aarch64 
 ,\)P' `\($. Host: Raspberry Pi 5 Model B Rev 1.0 
',\)P ,ggs. `\(b: Kernel: 6.12.25+rpt-rpi-2712 
`d\)' ,$P&quot;' . \($ Uptime: 2 hours 
 \)P d$' , \(P Packages: 1099 (dpkg), 2 (snap) 
 \): \(. - ,d\)' Shell: bash 5.2.15 
 \(; Y$b._ _,d$P' CPU: (4) @ 2.700GHz 
 Y\). `.`&quot;Y\(\)P&quot;' Memory: 360MiB / 8063MiB 
 `\(b &quot;-.__
  `Y\)                                                
   `Y\(.                                              
     `\)b.
       `Y$$b.
          `&quot;Y$b._
              `&quot;&quot;&quot;

root@master1:/home/admin#

root@master2:/home/admin# neofetch
       _,met\(\)$gg. root@master2 
    ,g\(\)\(\)\(\)\($P. ------------ 
  ,g\)P&quot; &quot;&quot;&quot;Y\(.&quot;. OS: Debian GNU/Linux 12 (bookworm) aarch64 
 ,\)P' `\($. Host: Raspberry Pi 5 Model B Rev 1.0 
',\)P ,ggs. `\(b: Kernel: 6.12.25+rpt-rpi-2712 
`d\)' ,$P&quot;' . \($ Uptime: 1 hour, 6 mins 
 \)P d$' , \(P Packages: 783 (dpkg) 
 \): \(. - ,d\)' Shell: bash 5.2.15 
 \(; Y$b._ _,d$P' CPU: (4) @ 2.700GHz 
 Y\). `.`&quot;Y\(\)P&quot;' Memory: 234MiB / 4050MiB 
 `\(b &quot;-.__
  `Y\)                                                
   `Y\(.                                              
     `\)b.
       `Y$$b.
          `&quot;Y$b._
              `&quot;&quot;&quot;

root@master2:/home/admin#  

admin@worker1:~ $ sudo su
root@worker1:/home/admin# neofetch
       _,met\(\)$gg. root@worker1 
    ,g\(\)\(\)\(\)\($P. ------------ 
  ,g\)P&quot; &quot;&quot;&quot;Y\(.&quot;. OS: Debian GNU/Linux 12 (bookworm) aarch64 
 ,\)P' `\($. Host: Raspberry Pi 3 Model B Plus Rev 1.3 
',\)P ,ggs. `\(b: Kernel: 6.6.74+rpt-rpi-v8 
`d\)' ,$P&quot;' . \($ Uptime: 1 min 
 \)P d$' , \(P Packages: 751 (dpkg) 
 \): \(. - ,d\)' Shell: bash 5.2.15 
 \(; Y$b._ _,d$P' Resolution: 1024x768 
 Y\). `.`&quot;Y\(\)P&quot;' CPU: (4) @ 1.400GHz 
 `\(b &quot;-.__ Memory: 166MiB / 907MiB 
  `Y\)
   `Y\(.                                              
     `\)b.                                            
       `Y$$b.  
          `&quot;Y$b._
              `&quot;&quot;&quot;

root@worker1:/home/admin# 

root@worker2:/home/admin# neofetch
       _,met\(\)$gg. root@worker2 
    ,g\(\)\(\)\(\)\($P. ------------ 
  ,g\)P&quot; &quot;&quot;&quot;Y\(.&quot;. OS: Debian GNU/Linux 12 (bookworm) aarch64 
 ,\)P' `\($. Host: Raspberry Pi 3 Model B Rev 1.2 
',\)P ,ggs. `\(b: Kernel: 6.12.25+rpt-rpi-v8 
`d\)' ,$P&quot;' . \($ Uptime: 1 hour, 6 mins 
 \)P d$' , \(P Packages: 743 (dpkg) 
 \): \(. - ,d\)' Shell: bash 5.2.15 
 \(; Y$b._ _,d$P' CPU: (4) @ 1.400GHz 
 Y\). `.`&quot;Y\(\)P&quot;' Memory: 116MiB / 906MiB 
 `\(b &quot;-.__
  `Y\)                                                
   `Y\(.                                              
     `\)b.
       `Y$$b.
          `&quot;Y$b._
              `&quot;&quot;&quot;

root@worker2:/home/admin#te
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, we’ll configure m &lt;strong&gt;aster2&lt;/strong&gt; (Raspberry Pi 5–4GB) as the &lt;strong&gt;initial control plane node&lt;/strong&gt; , while &lt;strong&gt;master1&lt;/strong&gt; , &lt;strong&gt;worker1&lt;/strong&gt; , and &lt;strong&gt;worker2&lt;/strong&gt; will function as &lt;strong&gt;worker nodes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This setup is intentional — it allows us to explore advanced scenarios in future blog posts, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setting up a new cluster using kubeadm with Cilium.&lt;/li&gt;
&lt;li&gt;Handling control plane degradation and recovery&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Starting simple provides a solid foundation while leaving room to scale and experiment.&lt;/p&gt;
&lt;h4 id=&quot;preparation-in-each-node-&quot;&gt;&lt;strong&gt;Preparation in each node:&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;The following commands prepare the OS for installing external components like container runtimes (Docker/containerd) and Kubernetes tools (kubeadm, kubelet, kubectl).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
sudo apt install -y curl apt-transport-https ca-certificates software-properties-common
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Enabling cgroup for CPU and memory&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hence, we are using Raspberry Pi, we can simply edit the cmdline.txt file in the firmware location to enable the cGroups.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /boot/firmware/cmdline.txt

#add the following content
ipv6.disable=1 cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Now disable swap in all nodes.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Disabling swap on Kubernetes nodes is a common practice due to its potential to negatively impact performance and predictability, especially for containerized workloads. Swap, while it allows a system to use more memory than available RAM, accesses data from the disk, which is significantly slower than RAM. This can lead to performance degradation, increased latency, and unpredictable behaviour, particularly under load.&lt;/p&gt;
&lt;p&gt;Disabling swap is mandatory because Kubernetes requires it for stable and predictable performance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes relies on the kubelet to manage resources, assuming memory is fully available.&lt;/li&gt;
&lt;li&gt;Swap can cause nodes to become slow or unresponsive as critical processes get swapped out.&lt;/li&gt;
&lt;li&gt;Disabling swap ensures the scheduler and kubelet have accurate memory availability data.&lt;/li&gt;
&lt;li&gt;Kubernetes officially fails to start if swap is enabled unless explicitly configured otherwise (which is not recommended).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo dphys-swapfile swapoff
sudo dphys-swapfile uninstall
sudo systemctl disable dphys-swapfile

#or

sudo swapoff -a
sudo apt purge -y dphys-swapfile

#verify using 
free -mh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Enable required kernel modules:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Enabling overlay and br_netfilter kernel modules is essential for Kubernetes networking:&lt;/p&gt;
&lt;p&gt;overlay: Supports overlay network drivers like Calico or Flannel, enabling pods across different nodes to communicate through virtual networks.&lt;/p&gt;
&lt;p&gt;br_netfilter: Allows Linux bridge firewall rules to inspect and filter bridged network traffic, which is crucial for Kubernetes network policies and packet forwarding.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo modprobe overlay
sudo modprobe br_netfilter    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Adding overlay and br_netfilter to /etc/modules-load.d/k8s.conf&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Adding overlay and br_netfilter to /etc/modules-load.d/k8s.conf ensures these kernel modules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load automatically at boot time&lt;/li&gt;
&lt;li&gt;Maintain persistent availability without manual intervention after reboot&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This guarantees Kubernetes networking components relying on these modules are always operational, avoiding network failures after node restarts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vi /etc/modules-load.d/k8s.conf

overlay
br_netfilter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/555/1*xbnkBGXVcVpfqoP51sDlXA.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;k8s.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Configure sysctl:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The sysctl settings in /etc/sysctl.d/k8s.conf enable essential kernel networking features for Kubernetes:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;net.bridge.bridge-nf-call-ip6tables = 1&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Ensures IPv6 bridged network traffic is passed through ip6tables for filtering and firewall rules.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;net.bridge.bridge-nf-call-iptables = 1&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Ensures IPv4 bridged network traffic is processed by iptables, allowing Kubernetes to enforce network policies and firewall rules on pod traffic.&lt;/p&gt;
&lt;p&gt;_net.ipv4.ip_forward = 1_&lt;/p&gt;
&lt;p&gt;Enables IP forwarding at the kernel level, allowing packets to be routed between different network interfaces — critical for pod-to-pod communication across nodes.&lt;/p&gt;
&lt;p&gt;These settings are required for Kubernetes networking to function properly and to enforce network security policies.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl - system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/582/1*ffQeIAi6ZZo355e3bM_6pQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;sysctl.d/k8s.confg&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;2-hostname-and-ip-configuration-in-the-router-&quot;&gt;&lt;strong&gt;2. Hostname and IP configuration in the router.&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;To ensure network stability within the Kubernetes cluster, it’s essential to assign static IP addresses to all nodes. In my setup, I have four Raspberry Pi devices connected to my home router via Wi-Fi.&lt;/p&gt;
&lt;p&gt;While it’s widely recommended to use wired (RJ45) Ethernet connections for Kubernetes clusters, mainly for improved reliability and lower latency, I’ve opted to use Wi-Fi. Despite the general concerns, I’ve tested this configuration extensively under various conditions and, so far, have encountered no issues.&lt;/p&gt;
&lt;p&gt;To maintain consistent IP addressing, I reserved the IPs for each Raspberry Pi in the router’s DHCP settings. This ensures the cluster nodes remain discoverable and reachable, even after reboots or network interruptions.&lt;/p&gt;
&lt;p&gt;I am using a Netlink Router at home. Please find my current configuration in the following screenshot. If you are using similar models, you can refer to the following configuration:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*zlmPaPh79fvRSPogxlqR7Q.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once this is configured, we have to edit the host file in each node and add the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vi /etc/hosts

#add the following
192.168.1.200 master1
192.168.1.201 master2
192.168.1.202 worker1
192.168.1.203 worker2
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-installing-container-runtime-&quot;&gt;3. Installing Container Runtime.&lt;/h4&gt;
&lt;p&gt;In Kubernetes, the smallest deployable unit is the Pod, which usually runs one or more containers. To run these containers, Kubernetes relies on a container runtime.&lt;/p&gt;
&lt;p&gt;Initially, Kubernetes was designed to work closely with Docker, which was the most widely adopted container engine at the time. However, over time, Kubernetes evolved, and the need to support multiple container runtimes became clear.&lt;/p&gt;
&lt;p&gt;To address this, the Open Container Initiative (OCI) was introduced — a set of industry standards that define how containers should be built and executed. In line with OCI, Kubernetes implemented the Container Runtime Interface (CRI), which allows it to interact with any OCI-compliant runtime, such as containerd or CRI-O, in a standardised way.&lt;/p&gt;
&lt;p&gt;Today, Docker is no longer a single, unified project. It consists of several components, including Docker-CLI, Docker-Compose, Docker Network, runc, and containerd. While containerd complies with the OCI standard and is capable of running containers, it does not natively support the CRI that Kubernetes requires.&lt;/p&gt;
&lt;p&gt;To bridge this gap, Kubernetes initially provided a compatibility layer called Docker shim, allowing it to communicate with the Docker Engine. However, Dockershim has now been deprecated, and its role is continued by the community-maintained project CRI-Dockerd.&lt;/p&gt;
&lt;p&gt;In this project, we are using the Docker Engine as the container runtime for Kubernetes, integrated via CRI-Dockerd.&lt;/p&gt;
&lt;p&gt;Let’s move on and see how to install Docker and configure it as a container runtime for Kubernetes using CRI-Dockerd. Currently, Mirantis is managing the CRI-Dockerd project, and I followed their official documentation for configuring CRI-Dockerd on my Raspberry Pi. Please note that we have to configure the container runtime in all nodes for setting up the cluster successfully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installing Docker on all nodes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.docker.com/engine/install/debian/&quot;&gt;Debian&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We’ll begin by installing Docker on all machines in the cluster. If you’re using a Debian-based system (such as Ubuntu or Raspberry Pi OS), you can use the following commands regardless of the system architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release &amp;amp;&amp;amp; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null
sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once Docker installation is completed, run the following command to verify the installation is proper and working fine.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following is the expected output;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/781/1*ESEaoH4EO2n7YMCYbBpNUg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;docker run hello-world&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installing CRI-Dockerd on all nodes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can refer to the following documentation for more details regarding CRI-Dockerd configuration:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mirantis.github.io/cri-dockerd/usage/install/&quot;&gt;Install&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this setup, I’m using Raspberry Pi devices, which are based on the ARM64 architecture. Instead of using the default package manager, I’m downloading the Docker binaries directly from the official release page to ensure compatibility and control over the installation version.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Mirantis/cri-dockerd/releases&quot;&gt;Releases · Mirantis/cri-dockerd&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.17/cri-dockerd-0.3.17.arm64.tgz

#after downloading extract and move it to local binary

tar -xvf cri-dockerd-0.3.17.arm64.tgz
cd cri-dockerd
sudo mv cri-dockerd /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the installation is completed, run the following command to verify the installation;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status cri-docker.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the configuration is correct, running the systemctl command should return an output indicating that the service is active and running.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/954/1*GgL_7d2yDrTbyXRKcZlK0Q.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;node1&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/969/1*ZMCPXp5Jq7XxYwJYtH1dnQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;node2 (master)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/955/1*5j7pnwZhDN3M5o_yFabzvg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;node 3&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/964/1*VGo0B83bxOfH1rGaXCejZg.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;node 4&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;3-installing-kubeadm&quot;&gt;3. Installing Kubeadm&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot;&gt;Installing kubeadm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this setup, we will be using Kubeadm version 1.29. This specific version is chosen intentionally, as we are planning a follow-up blog focused on Kubeadm upgrade procedures. Starting with v1.29 provides a practical baseline for demonstrating version upgrades.&lt;/p&gt;
&lt;p&gt;If you prefer to install the latest available version, you can refer to the official Kubernetes documentation linked above. Otherwise, to proceed with Kubeadm v1.29, follow the version-specific installation steps documented here.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://v1-29.docs.kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot;&gt;Installing kubeadm&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
# apt-transport-https may be a dummy package; if so, you can skip that package
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

#if you are facing any issues with the installation and you want to reinstall the packages, please unhold them first.

sudo apt-mark unhold kubelet kubeadm kubectl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*3TVLF766tuaf8YlvxzL6Lw.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;kubeadm, kubelet and kubectl installation completed&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Follow the installation process on all nodes. Once the installation is completed, enable the kubelet on all nodes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable --now kubelet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The kubelet is now restarting every few seconds, as it waits in a crashloop for kubeadm to tell it what to do.&lt;/p&gt;
&lt;h4 id=&quot;4-cgroup-driver-configuration&quot;&gt;4. Cgroup Driver Configuration&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Why Cgroup Driver Configuration Matters in Kubeadm Installation?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;During a Kubernetes cluster setup with Kubeadm, it is essential to ensure that the cgroup driver used by the container runtime (e.g., Docker, containerd) matches the driver expected by Kubelet, the Kubernetes node agent.&lt;/p&gt;
&lt;p&gt;Cgroups (control groups) are used by the Linux kernel to limit and isolate resource usage (CPU, memory, etc.) of process groups. Kubernetes relies on this mechanism to enforce resource limits on containers.&lt;/p&gt;
&lt;p&gt;There are two common cgroup drivers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cgroupfs — a native driver provided by the Linux kernel&lt;/li&gt;
&lt;li&gt;systemd — integrates with the systemd init system for resource management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If the cgroup drivers between Docker/containerd and Kubelet are mismatched, Kubelet will fail to start, or you may encounter node stability issues.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Kubernetes now recommends using the systemd cgroup driver for better compatibility and integration with modern Linux distributions.&lt;/p&gt;
&lt;p&gt;You can verify and configure the cgroup driver to ensure consistency before initialising the cluster with kubeadm. Refer to the following documentation for more details. In our case, it is not required.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://v1-29.docs.kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/&quot;&gt;Configuring a cgroup driver&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;5-initialising-master-node-&quot;&gt;5. Initialising master-node.&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/&quot;&gt;Creating a cluster with kubeadm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When you run kubeadm init, Kubernetes performs several critical actions to bootstrap the control plane:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Generates PKI Certificates&lt;/strong&gt;&lt;br&gt;
Secure communication between components is enabled by generating TLS certificates under /etc/kubernetes/pki.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bootstrap the Kubeconfig Files.&lt;/strong&gt;&lt;br&gt;
Configuration files required by kubectl and system components are created under /etc/kubernetes/ and $HOME/.kube/config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start Control Plane Components&lt;/strong&gt;&lt;br&gt;
These pods run on the master node and are defined under /etc/kubernetes/manifests/. The following static pods are created and managed by the kubelet  &lt;ul&gt;
&lt;li&gt;kube-apiserver  &lt;/li&gt;
&lt;li&gt;kube-controller-manager  &lt;/li&gt;
&lt;li&gt;kube-scheduler&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Initialises the etcd Database&lt;/strong&gt;&lt;br&gt;
An embedded or external etcd key-value store is initialised to persist cluster state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sets up the Bootstrap Token and Join Configuration&lt;/strong&gt;&lt;br&gt;
A token and join command are generated to allow worker nodes to securely join the cluster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outputs Post-Initialisation Instructions&lt;/strong&gt;&lt;br&gt;
The command displays steps to:  &lt;ul&gt;
&lt;li&gt;Configure kubectl access  &lt;/li&gt;
&lt;li&gt;Install a Pod network add-on (required before scheduling workloads)  &lt;/li&gt;
&lt;li&gt;Join worker nodes using the kubeadm join command&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now run the following command. After initialising the control plane with kubeadm, you need to configure kubectl to communicate with the cluster. The following commands handle that setup:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

export KUBECONFIG=/etc/kubernetes/admin.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The export command sets the environment variable so that kubectl uses the admin configuration directly from the system path. Now run the following command to initialize the master node;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm init --cri-socket=unix:///var/run/cri-dockerd.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the control plane initialisation is successful, you’ll see a message similar to the one shown in the screenshot below. This message confirms that the Kubernetes control plane is up and running.&lt;/p&gt;
&lt;p&gt;Additionally, a bootstrap token will be generated. This token, along with the control plane IP and certificate hash, is required for joining worker nodes to the cluster.&lt;/p&gt;
&lt;p&gt;You’ll also receive instructions to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up your kubectl config&lt;/li&gt;
&lt;li&gt;Install a pod network add-on&lt;/li&gt;
&lt;li&gt;Use the kubeadm join command on worker nodes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*Z3OVvRTm3bHP3h6XIJdlIQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;kubeadm init&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You must use the corresponding sockets depending on the &lt;strong&gt;container runtime version&lt;/strong&gt; you are usi&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12999/raspberry-pi-k8s-cluster-setup-for-home-lab-with-cilium</guid>
<pubDate>Tue, 17 Mar 2026 11:01:15 +0000</pubDate>
</item>
</channel>
</rss>