<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mursleen Mamoon</title>
    <description>The latest articles on DEV Community by Mursleen Mamoon (@mursleen_mamoon_35bfb471c).</description>
    <link>https://dev.to/mursleen_mamoon_35bfb471c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027967%2Fb7216b6e-9269-4a04-8216-4ac1b33a9311.png</url>
      <title>DEV Community: Mursleen Mamoon</title>
      <link>https://dev.to/mursleen_mamoon_35bfb471c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mursleen_mamoon_35bfb471c"/>
    <language>en</language>
    <item>
      <title>Deploying a Kubernetes Cluster with kubeadm on RHEL 9 &amp; CentOS 10</title>
      <dc:creator>Mursleen Mamoon</dc:creator>
      <pubDate>Tue, 14 Jul 2026 02:44:28 +0000</pubDate>
      <link>https://dev.to/mursleen_mamoon_35bfb471c/deploying-a-kubernetes-cluster-with-kubeadm-on-rhel-9-centos-10-14pf</link>
      <guid>https://dev.to/mursleen_mamoon_35bfb471c/deploying-a-kubernetes-cluster-with-kubeadm-on-rhel-9-centos-10-14pf</guid>
      <description>&lt;h1&gt;
  
  
  Deploying-a-Kubernetes-Cluster-with-kubeadm-on-RHEL-9-CentOS-10
&lt;/h1&gt;

&lt;p&gt;The guide is based on a hands-on implementation and is intended to serve as both a learning resource and a reusable deployment reference for students, DevOps engineers, and anyone building a Kubernetes lab environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploying a Kubernetes Cluster&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;with kubeadm on RHEL 9&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A Corrected &amp;amp; Expanded Reference Build&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;1 Control Plane · 2 Worker Nodes · VMware&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;AUTHOR&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Muhammad Mursleen Mamaoon&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PLATFORM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RHEL 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORCHESTRATION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;kubeadm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CONTAINER RUNTIME&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;containerd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CLUSTER NETWORKING&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Calico CNI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DOCUMENT TYPE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deployment Runbook &amp;amp; Reference Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EDITION&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Corrected Edition - networking, firewall, and lifecycle fixes applied&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Contents&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Purpose and Scope&lt;/strong&gt; 3&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Lab Topology&lt;/strong&gt; 3&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Prerequisites&lt;/strong&gt; 3&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.1 VMware-Specific Gotcha: Clone Hygiene&lt;/strong&gt; 4&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Step 1 - Hostnames and Name Resolution [ALL]&lt;/strong&gt; 4&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Step 2 - Time Synchronization [ALL]&lt;/strong&gt; 5&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Step 3 - Disable Swap [ALL]&lt;/strong&gt; 5&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Step 4 - SELinux [ALL]&lt;/strong&gt; 5&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Step 5 - Kernel Modules [ALL]&lt;/strong&gt; 5&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Step 6 - Kernel Network Parameters [ALL]&lt;/strong&gt; 6&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Step 7 - Firewall Configuration [ALL]&lt;/strong&gt; 6&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Step 8 - Install and Configure containerd [ALL]&lt;/strong&gt; 7&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12. Step 9 - Install Kubernetes Components [ALL]&lt;/strong&gt; 8&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;13. Step 10 - Initialize the Control Plane [MASTER]&lt;/strong&gt; 8&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;14. Step 11 - Join Worker Nodes [WORKER]&lt;/strong&gt; 9&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;15. Step 12 - Install Calico CNI [MASTER]&lt;/strong&gt; 9&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;16. Step 13 - Validate the Cluster [MASTER]&lt;/strong&gt; 9&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;17. Troubleshooting&lt;/strong&gt; 10&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;18. What Changes for Production&lt;/strong&gt; 10&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Right-click the table above and choose "Update Field" after opening in Word if page numbers do not populate automatically.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;1. Purpose and Scope&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;This document is a complete, validated procedure for bootstrapping a Kubernetes cluster using kubeadm, containerd, and Calico on RHEL 9, deployed across three VMware virtual machines. It is written as both an execution runbook and a design reference - each step includes not just the command, but the reasoning behind it, so it can be adapted safely rather than copy-pasted blind.&lt;/p&gt;

&lt;p&gt;This edition corrects several issues identified in the original draft, most notably a pod-network / host-network CIDR collision and a firewall rule that blocked Calico's actual encapsulation mode, and adds operational detail commonly missing from kubeadm walkthroughs: VMware clone hygiene, time synchronization, version pinning, and a fuller validation pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SCOPE NOTE&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As written, this procedure produces a &lt;strong&gt;functional lab / reference cluster&lt;/strong&gt; - suitable for learning, testing, and demonstrating the mechanics of kubeadm. It is &lt;strong&gt;&lt;em&gt;not&lt;/em&gt;&lt;/strong&gt;, as-is, a hardened production topology: a single control-plane node is a single point of failure, SELinux is relaxed rather than properly policed, and there is no external etcd or highly-available API server. Section 18 outlines what changes for production use.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;2. Lab Topology&lt;/strong&gt;
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hostname&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;IP Address&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;vCPU&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Control Plane&lt;/td&gt;
&lt;td&gt;k8s-master&lt;/td&gt;
&lt;td&gt;192.168.1.10&lt;/td&gt;
&lt;td&gt;2+&lt;/td&gt;
&lt;td&gt;2 GB+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker Node 1&lt;/td&gt;
&lt;td&gt;k8s-worker1&lt;/td&gt;
&lt;td&gt;192.168.1.11&lt;/td&gt;
&lt;td&gt;2+&lt;/td&gt;
&lt;td&gt;2 GB+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker Node 2&lt;/td&gt;
&lt;td&gt;k8s-worker2&lt;/td&gt;
&lt;td&gt;192.168.1.12&lt;/td&gt;
&lt;td&gt;2+&lt;/td&gt;
&lt;td&gt;2 GB+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stack: &lt;strong&gt;RHEL 9 · VMware Workstation/ESXi · kubeadm · containerd · Calico CNI&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Notation Used Throughout&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;[ALL]&lt;/strong&gt; - run on every node&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[MASTER]&lt;/strong&gt; - control plane only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[WORKER]&lt;/strong&gt; - worker nodes only&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;3. Prerequisites&lt;/strong&gt;
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Three RHEL 9 VMs with routable connectivity to each other and to the internet, for package and container image pulls.&lt;/li&gt;
&lt;li&gt;SSH access with sudo privileges on all nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hard minimum of 2 vCPUs on the control-plane node.&lt;/strong&gt; This is not a soft recommendation - kubeadm init runs a preflight check and refuses to proceed on a single-vCPU control plane.&lt;/li&gt;
&lt;li&gt;A pod network CIDR that does not overlap the physical/VM network. The original draft used 192.168.0.0/16 for pods on a 192.168.1.0/24 host network - those ranges overlap. This guide uses 10.244.0.0/16 instead (see Section 10).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3.1 VMware-Specific Gotcha: Clone Hygiene&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If any of these three VMs were created by cloning a template - extremely common in VMware labs - they can inherit an identical /etc/machine-id and product UUID. Kubernetes uses these values to distinguish nodes, and duplicates cause node registration conflicts, DHCP lease collisions, and kubelet identity confusion that are painful to debug after the fact. Check uniqueness before proceeding:&lt;/p&gt;

&lt;p&gt;# [ALL] Every value below must differ across all three nodes&lt;/p&gt;

&lt;p&gt;cat /etc/machine-id&lt;/p&gt;

&lt;p&gt;cat /sys/class/dmi/id/product_uuid&lt;/p&gt;

&lt;p&gt;ip link show | grep ether&lt;/p&gt;

&lt;p&gt;If any values match across nodes, regenerate the machine ID on the affected VM:&lt;/p&gt;

&lt;p&gt;# [ALL, only if duplicated]&lt;/p&gt;

&lt;p&gt;sudo rm -f /etc/machine-id /var/lib/dbus/machine-id&lt;/p&gt;

&lt;p&gt;sudo systemd-machine-id-setup&lt;/p&gt;

&lt;p&gt;sudo ln -s /etc/machine-id /var/lib/dbus/machine-id&lt;/p&gt;

&lt;p&gt;Duplicate product_uuid values require a fix at the hypervisor level - regenerate the BIOS UUID in the VM's .vmx settings, or re-clone from the template with "new identity" options enabled.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;4. Step 1 - Hostnames and Name Resolution [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;# Run the appropriate line on each respective node&lt;/p&gt;

&lt;p&gt;sudo hostnamectl set-hostname k8s-master # on the control plane&lt;/p&gt;

&lt;p&gt;sudo hostnamectl set-hostname k8s-worker1 # on worker 1&lt;/p&gt;

&lt;p&gt;sudo hostnamectl set-hostname k8s-worker2 # on worker 2&lt;/p&gt;

&lt;p&gt;Add all three nodes to /etc/hosts on every node:&lt;/p&gt;

&lt;p&gt;| sudo tee -a /etc/hosts &amp;lt;&lt;br&gt;192.168.1.10 k8s-master&lt;br&gt;&lt;br&gt;192.168.1.11 k8s-worker1&lt;br&gt;&lt;br&gt;192.168.1.12 k8s-worker2&lt;br&gt;&lt;br&gt;EOF | |&lt;br&gt;
| --- | |&lt;br&gt;
| &lt;strong&gt;IDEMPOTENCY NOTE&lt;/strong&gt;&lt;br&gt;&lt;br&gt;tee -a appends unconditionally. Re-running this step produces duplicate lines - harmless for resolution, but if scripting this for repeatable builds, guard it with a grep -qxF check first. | |&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;5. Step 2 - Time Synchronization [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;ADDED IN THIS EDITION&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The original draft did not include this step, and it should have. Kubernetes leans heavily on TLS certificate validity windows and etcd's Raft consensus, both sensitive to clock drift between nodes.&lt;/p&gt;

&lt;p&gt;RHEL 9 ships chrony by default - confirm it is active:&lt;/p&gt;

&lt;p&gt;sudo systemctl enable --now chronyd&lt;/p&gt;

&lt;p&gt;chronyc tracking&lt;/p&gt;

&lt;p&gt;Confirm System time shows a sub-second offset before continuing.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;6. Step 3 - Disable Swap [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The kubelet refuses to start with swap enabled, unless explicitly configured via NodeSwap / MemorySwap, which is out of scope for this build.&lt;/p&gt;

&lt;p&gt;sudo swapoff -a&lt;/p&gt;

&lt;p&gt;sudo sed -i '/\sswap\s/s/^/#/' /etc/fstab&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;7. Step 4 - SELinux [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;| sudo setenforce 0&lt;br&gt;&lt;br&gt;sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config | |&lt;br&gt;
| --- | |&lt;br&gt;
| &lt;strong&gt;PRODUCTION CAUTION&lt;/strong&gt;&lt;br&gt;&lt;br&gt;Permissive mode disables SELinux enforcement cluster-wide, which is acceptable for a lab but removes a real security boundary in production. If extending this design, install the container-selinux package (ships proper policies for containerd/CRI-O) and keep SELinux in enforcing mode - most modern kubeadm/containerd combinations on RHEL run fine enforced, provided the runtime's SELinux policy is installed correctly. | |&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;8. Step 5 - Kernel Modules [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/modules-load.d/k8s.conf&lt;/p&gt;

&lt;p&gt;overlay&lt;/p&gt;

&lt;p&gt;br_netfilter&lt;/p&gt;

&lt;p&gt;EOF&lt;/p&gt;

&lt;p&gt;sudo modprobe overlay&lt;/p&gt;

&lt;p&gt;sudo modprobe br_netfilter&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;9. Step 6 - Kernel Network Parameters [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/sysctl.d/k8s.conf&lt;/p&gt;

&lt;p&gt;net.bridge.bridge-nf-call-iptables = 1&lt;/p&gt;

&lt;p&gt;net.bridge.bridge-nf-call-ip6tables = 1&lt;/p&gt;

&lt;p&gt;net.ipv4.ip_forward = 1&lt;/p&gt;

&lt;p&gt;EOF&lt;/p&gt;

&lt;p&gt;sudo sysctl --system&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;10. Step 7 - Firewall Configuration [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;CORRECTED IN THIS EDITION&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The original port list included two deprecated ports and opened the wrong protocol for Calico's default overlay mode.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dropped 10251/tcp and 10252/tcp&lt;/strong&gt; - pre-1.19 insecure HTTP endpoints for kube-scheduler and kube-controller-manager, replaced by the authenticated endpoints on 10259 and 10257. Modern kubeadm does not expose the old ones at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replaced 4789/udp (VXLAN) with the IP-in-IP protocol&lt;/strong&gt; - Calico's default manifest (calico.yaml) uses IP-in-IP encapsulation, not VXLAN, unless explicitly reconfigured. Opening only 4789/udp, as the original draft did, leaves the actual overlay traffic blocked by the firewall.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Control Plane&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;sudo firewall-cmd --permanent --add-port={6443,2379,2380,10250,10257,10259,179}/tcp&lt;/p&gt;

&lt;p&gt;sudo firewall-cmd --permanent --add-protocol=ipip&lt;/p&gt;

&lt;p&gt;sudo firewall-cmd --reload&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Worker Nodes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;sudo firewall-cmd --permanent --add-port={10250,179,30000-32767}/tcp&lt;/p&gt;

&lt;p&gt;sudo firewall-cmd --permanent --add-protocol=ipip&lt;/p&gt;

&lt;p&gt;sudo firewall-cmd --reload&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Port Reference&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Nodes&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6443&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;kube-apiserver&lt;/td&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2379-2380&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;etcd client/peer&lt;/td&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10250&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;kubelet API&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10257&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;kube-controller-manager (secure)&lt;/td&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10259&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;kube-scheduler (secure)&lt;/td&gt;
&lt;td&gt;Control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;179&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Calico BGP&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;IP-in-IP (proto 4)&lt;/td&gt;
&lt;td&gt;Calico overlay (default mode)&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30000-32767&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;NodePort service range&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;IF YOU SWITCH TO VXLAN&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Calico is later switched to VXLAN mode instead of IP-in-IP (common on networks that block IP-in-IP, e.g. some cloud VPCs), swap the --add-protocol=ipip rule for --add-port=4789/udp on every node. Don't open both unless intentionally running mixed mode.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;11. Step 8 - Install and Configure containerd [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;sudo dnf install -y dnf-plugins-core&lt;/p&gt;

&lt;p&gt;sudo dnf config-manager --add-repo &lt;a href="https://download.docker.com/linux/rhel/docker-ce.repo" rel="noopener noreferrer"&gt;https://download.docker.com/linux/rhel/docker-ce.repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;sudo dnf install -y containerd.io&lt;/p&gt;

&lt;p&gt;sudo mkdir -p /etc/containerd&lt;/p&gt;

&lt;p&gt;containerd config default | sudo tee /etc/containerd/config.toml&lt;/p&gt;

&lt;p&gt;# Kubernetes requires the systemd cgroup driver (RHEL 9 uses cgroup v2 by default)&lt;/p&gt;

&lt;p&gt;sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml&lt;/p&gt;

&lt;p&gt;sudo systemctl enable --now containerd&lt;/p&gt;

&lt;p&gt;sudo systemctl restart containerd&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;12. Step 9 - Install Kubernetes Components [ALL]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;| &lt;strong&gt;VERSION UPDATED IN THIS EDITION&lt;/strong&gt;&lt;br&gt;&lt;br&gt;The original draft pinned v1.31, which has already reached end-of-life. Kubernetes follows an N-2 support policy across roughly four-month release cycles; the currently supported branches are 1.34, 1.35, and 1.36. Check kubernetes.io/releases for the current set before building, and substitute below. | |&lt;br&gt;
| --- | |&lt;br&gt;
| cat &amp;lt;&lt;br&gt;[kubernetes]&lt;br&gt;&lt;br&gt;name=Kubernetes&lt;br&gt;&lt;br&gt;baseurl=&lt;a href="https://pkgs.k8s.io/core:/stable:/v1.34/rpm/" rel="noopener noreferrer"&gt;https://pkgs.k8s.io/core:/stable:/v1.34/rpm/&lt;/a&gt;&lt;br&gt;&lt;br&gt;enabled=1&lt;br&gt;&lt;br&gt;gpgcheck=1&lt;br&gt;&lt;br&gt;gpgkey=&lt;a href="https://pkgs.k8s.io/core:/stable:/v1.34/rpm/repodata/repomd.xml.key" rel="noopener noreferrer"&gt;https://pkgs.k8s.io/core:/stable:/v1.34/rpm/repodata/repomd.xml.key&lt;/a&gt;&lt;br&gt;&lt;br&gt;exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni&lt;br&gt;&lt;br&gt;EOF | |&lt;/p&gt;

&lt;p&gt;Pin exact versions instead of floating on "latest." Without pinning, three separate dnf install runs across three nodes can silently pull three different patch versions if a new patch lands mid-build - tedious to debug later. Check what's available first:&lt;/p&gt;

&lt;p&gt;dnf list --showduplicates kubelet --disableexcludes=kubernetes&lt;/p&gt;

&lt;p&gt;Then install a matched version explicitly:&lt;/p&gt;

&lt;p&gt;| sudo dnf install -y kubelet-1.34.* kubeadm-1.34.* kubectl-1.34.* --disableexcludes=kubernetes&lt;br&gt;&lt;br&gt;sudo systemctl enable --now kubelet | |&lt;br&gt;
| --- | |&lt;br&gt;
| &lt;strong&gt;EXPECTED BEHAVIOR&lt;/strong&gt;&lt;br&gt;&lt;br&gt;It is normal for kubelet to sit in a crash-restart loop at this point (systemctl status kubelet will look unhappy) - it is waiting for kubeadm init / kubeadm join to supply a configuration. Not a bug. | |&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;13. Step 10 - Initialize the Control Plane [MASTER]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;| &lt;strong&gt;CIDR CORRECTED IN THIS EDITION&lt;/strong&gt;&lt;br&gt;&lt;br&gt;The original draft used 192.168.0.0/16 for the pod network, which fully contains the 192.168.1.0/24 host network the VMs sit on. Overlapping ranges break routing between pods and real hosts - the kernel cannot disambiguate them. This guide uses a distinct, non-overlapping range. | |&lt;br&gt;
| --- | |&lt;br&gt;
| sudo kubeadm init \&lt;br&gt;&lt;br&gt;--apiserver-advertise-address=192.168.1.10 \&lt;br&gt;&lt;br&gt;--pod-network-cidr=10.244.0.0/16 | |&lt;/p&gt;

&lt;p&gt;Save the kubeadm join … command printed at the end of initialization - it is required on both worker nodes, and expires after 24 hours by default.&lt;/p&gt;

&lt;p&gt;Configure kubectl for your user:&lt;/p&gt;

&lt;p&gt;mkdir -p \$HOME/.kube&lt;/p&gt;

&lt;p&gt;sudo cp -i /etc/kubernetes/admin.conf \$HOME/.kube/config&lt;/p&gt;

&lt;p&gt;sudo chown \$(id -u):\$(id -g) \$HOME/.kube/config&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;14. Step 11 - Join Worker Nodes [WORKER]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;sudo kubeadm join 192.168.1.10:6443 \&lt;/p&gt;

&lt;p&gt;--token &amp;lt;token&amp;gt; \&lt;/p&gt;

&lt;p&gt;--discovery-token-ca-cert-hash sha256:&amp;lt;hash&amp;gt;&lt;/p&gt;

&lt;p&gt;If the token has expired (default TTL: 24 hours), generate a fresh one from the control plane:&lt;/p&gt;

&lt;p&gt;kubeadm token create --print-join-command&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;15. Step 12 - Install Calico CNI [MASTER]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;| &lt;strong&gt;CIDR ALIGNMENT REQUIRED&lt;/strong&gt;&lt;br&gt;&lt;br&gt;Calico's default manifest ships with its own IP pool CIDR (192.168.0.0/16), which must match the --pod-network-cidr given to kubeadm. Since that value changed to 10.244.0.0/16, the manifest needs the same edit before applying. | |&lt;br&gt;
| --- | |&lt;br&gt;
| curl -O &lt;a href="https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml&lt;/a&gt;&lt;br&gt;&lt;br&gt;sed -i 's/192.168.0.0\/16/10.244.0.0\/16/' calico.yaml&lt;br&gt;&lt;br&gt;kubectl apply -f calico.yaml | |&lt;/p&gt;

&lt;p&gt;Double-check the Calico version tag (v3.28.0) against what's current before building - Calico ships new minor releases on its own cadence, independent of Kubernetes.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;16. Step 13 - Validate the Cluster [MASTER]&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Nodes should transition to Ready once Calico's pods come up - this can take a minute or two:&lt;/p&gt;

&lt;p&gt;kubectl get nodes -o wide&lt;/p&gt;

&lt;p&gt;kubectl get pods -n kube-system -o wide&lt;/p&gt;

&lt;p&gt;All calico-node, calico-kube-controllers, and coredns pods should be Running. If coredns is stuck Pending, it is almost always waiting on CNI - check the calico-node pods first.&lt;/p&gt;

&lt;p&gt;Sanity-check core cluster health:&lt;/p&gt;

&lt;p&gt;kubectl get componentstatuses # deprecated but still useful for a quick glance&lt;/p&gt;

&lt;p&gt;kubectl cluster-info&lt;/p&gt;

&lt;p&gt;Deploy and expose a smoke-test workload:&lt;/p&gt;

&lt;p&gt;kubectl create deployment nginx-demo --image=nginx&lt;/p&gt;

&lt;p&gt;kubectl expose deployment nginx-demo --type=NodePort --port=80&lt;/p&gt;

&lt;p&gt;kubectl get svc nginx-demo&lt;/p&gt;

&lt;p&gt;curl http://&amp;lt;any-worker-ip&amp;gt;:&amp;lt;node-port&amp;gt;&lt;/p&gt;

&lt;p&gt;A default NGINX welcome page confirms the API server, scheduler, kubelet, CNI, and kube-proxy are all functioning end-to-end.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;17. Troubleshooting&lt;/strong&gt;
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cause / Fix&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kubelet won't start / crashlooping before join&lt;/td&gt;
&lt;td&gt;Expected pre-kubeadm init/join. If it persists after joining, confirm SystemdCgroup = true is actually set in /etc/containerd/config.toml and that containerd was restarted after the edit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker stuck NotReady&lt;/td&gt;
&lt;td&gt;Expected until Calico is applied. If it persists afterward, check kubectl get pods -n kube-system for calico-node crash loops - usually a firewall or IP-in-IP protocol issue (Section 10).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubeadm join fails / times out&lt;/td&gt;
&lt;td&gt;Verify firewall ports (Section 10), hostname resolution (Section 4), and that the join token/hash haven't expired.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pods reach each other on the same node but not across nodes&lt;/td&gt;
&lt;td&gt;Almost always the overlay protocol being blocked - confirm firewall-cmd --add-protocol=ipip was applied on every node, not just the control plane.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two nodes seem to "merge" identity, or one registers then vanishes&lt;/td&gt;
&lt;td&gt;Cloned-VM machine-id / UUID collision - see Section 3.1.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kubeadm init preflight fails on CPU count&lt;/td&gt;
&lt;td&gt;Control-plane node has fewer than 2 vCPUs - a hard requirement, not advisory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster works but certs/etcd behave oddly under load&lt;/td&gt;
&lt;td&gt;Check chronyc tracking on all nodes - clock drift across etcd peers causes subtle, hard-to-diagnose failures.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost the join command&lt;/td&gt;
&lt;td&gt;kubeadm token create --print-join-command from the control plane, any time.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;18. What Changes for Production&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;This build is correct and internally consistent for a lab or reference deployment, but a few things are deliberately out of scope and worth naming explicitly if the design is extended:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single control-plane node = single point of failure.&lt;/strong&gt; Production topologies run 3 (or 5) control-plane nodes behind a load balancer, with --control-plane-endpoint set to the LB's DNS name from the first kubeadm init, and --upload-certs for subsequent control-plane joins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;etcd&lt;/strong&gt; here is stacked on the control-plane node. Production clusters commonly run etcd on dedicated nodes for isolation and easier backup/restore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SELinux permissive&lt;/strong&gt; should become enforcing with proper container SELinux policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No backup strategy&lt;/strong&gt; for etcd snapshots or cluster state is defined here - add one before treating this as anything beyond a lab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No admission control / network policy&lt;/strong&gt; beyond Calico's defaults - production clusters typically layer in NetworkPolicies and often an admission controller (OPA/Gatekeeper, Kyverno, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This guide reflects Kubernetes' supported release lifecycle and Calico's default networking behavior as of the time of writing. Both move on independent release cadences - re-verify version numbers and encapsulation defaults before using this as a template for a new build.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contact info:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Gmail:&lt;/em&gt; &lt;a href="mailto:mursleenmamoon@gmail.com"&gt;&lt;em&gt;mursleenmamoon@gmail.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;LinkedIn:&lt;/em&gt; &lt;a href="https://www.linkedin.com/in/mursleen-mamoon-097837350/" rel="noopener noreferrer"&gt;&lt;em&gt;https://www.linkedin.com/in/mursleen-mamoon-097837350/&lt;/em&gt;&lt;/a&gt;&lt;em&gt;Github:&lt;/em&gt; &lt;a href="https://github.com/Mursleen666" rel="noopener noreferrer"&gt;&lt;em&gt;https://github.com/Mursleen666&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
