# 🔧 Complete Guide: Setting Up a Kubernetes v1.36 Cluster with Kubeadm and Containerd on AWS

## **📑 Table of Contents**

1.  [Introduction](#introduction)
    
2.  [Wh](#introduction)[y Use kube](#why-use-kubeadm)[adm](#introduction)
    
3.  [System Re](#introduction)[quir](#why-use-kubeadm)[ements](#introduction)
    
4.  [Cl](#introduction)[us](#why-use-kubeadm)[t](#system-requirements)[er A](#why-use-kubeadm)[rchitecture](#introduction) [Ove](#system-requirements)[rview](#why-use-kubeadm)
    
5.  [Networking](#introduction) [Req](#system-requirements)[uirements](#why-use-kubeadm)
    
6.  [Wh](#why-use-kubeadm)[y D](#cluster-topology-for-this-setup)[isable Swap](#introduction)
    
7.  [Understandin](#why-use-kubeadm)[g Cgroups an](#introduction)[d D](#system-requirements)[river](#why-use-kubeadm)[s](#introduction)
    
8.  [Step-by](#introduction)[\-S](#networking-between-nodes)[t](#system-requirements)[ep Setup](#why-use-kubeadm)
    
    *   a[. L](#system-requirements)[a](#cluster-topology-for-this-setup)[unc](#system-requirements)[h EC2 Instan](#introduction)[ces](#system-requirements)
        
    *   b[. Prepare All No](#introduction)[des](#why-use-kubeadm)
        
    *   c. [Insta](#why-use-kubeadm)[ll and Confi](#introduction)[gur](#system-requirements)[e Contain](#why-use-kubeadm)[erd](#introduction)
        
    *   d. [In](#introduction)[sta](#system-requirements)[ll Kubernetes B](#why-use-kubeadm)[inar](#cluster-topology-for-this-setup)[ies](#system-requirements)
        
    *   e. [Initial](#introduction)[iz](#system-requirements)[e](#cluster-topology-for-this-setup) [Control Plane](#why-use-kubeadm)
        
    *   f[. Join Worke](#introduction)[r N](#system-requirements)[odes](#why-use-kubeadm)
        
    *   g. [Inst](#why-use-kubeadm)[a](#cluster-topology-for-this-setup)[ll Network A](#introduction)[dd-](#system-requirements)[On](#why-use-kubeadm)
        
9.  [Validation](#introduction) [and](#system-requirements) [Testing](#why-use-kubeadm)
    
10.  [Cl](#why-use-kubeadm)[eanup Reminder](#introduction)
     
11.  [Conclusion](#why-use-kubeadm)
     

* * *

## **📌 Introduction**

Managed Kubernetes services such as EKS are convenient, but they hide many of the components that make Kubernetes work. In this guide you'll build a Kubernetes v1.36 cluster from scratch on AWS using kubeadm and containerd, giving you hands-on experience with the control plane, worker nodes, networking, and cluster bootstrapping.

* * *

## **🛠️ Why Use kubeadm ?**

kubeadm is a tool designed to **simplify Kubernetes cluster setup**.

It automates:

*   Control plane component setup
    
*   Certificate generation
    
*   Worker node joining
    
*   Bootstrapping configs
    

Without kubeadm, you’d need to manually configure:

*   kube-apiserver
    
*   etcd
    
*   kube-scheduler
    
*   controller-manager
    
*   kubelet
    
*   kube-proxy
    
*   A container runtime (e.g., containerd)
    

That’s a heavy lift—and error-prone. So let’s do it the smart way.

* * *

## **🧱 System Requirements**

Each node should have:

| **Requirement** | **Value** |
| --- | --- |
| OS | Ubuntu 24.04 LTS |
| RAM | ≥ 4GB |
| CPU | ≥ 2 vCPUs |
| Swap | Disabled |
| Ports Open | 6443, 10250, 10256, 10259, 2379-2380 |
| IPv4 Forwarding | Enabled |
| Hostnames | Unique per node |

> ⚠️ Kubernetes cannot install the control plane on Windows. Worker nodes can be Windows, but only Linux supports the control plane.

Note: For learning environments you may allow all TCP within the security group. For production environments restrict traffic to only required Kubernetes ports.

This rule **only allows internal traffic** — for example:

*   Pods talking to each other across nodes
    
*   kubelet or containerd communicating
    
*   Control plane → worker communication
    

✅ **Good** for Kubernetes **node-to-node communication**

❌ **NOT enough** for traffic from **outside AWS** (like your laptop)

* * *

## **🌐 Cluster Topology (for this setup)**

*   **Control Plane Node:** t2.medium (2 vCPU, 4GB RAM)
    
*   **Worker Node 1:** t2.medium (2 vCPU, 4GB RAM)
    
*   **Worker Node 2:** t2.medium (2 vCPU, 4GB RAM)
    

This is a **cost-optimized test cluster** for learning—not for heavy workloads.

* * *

## **🔌 Networking Between Nodes**

Kubernetes requires:

*   **Full network connectivity** between all nodes (ping from any to any)
    
*   **Open ports** for components to talk (API server, kubelet, etc.)
    
*   **Seamless Pod-to-Pod communication**, even across nodes
    

Without this, your cluster may initialize but won’t function correctly.

* * *

## **🚫 Why Disable Swap?**

Kubernetes uses real-time memory metrics to schedule pods. If swap is enabled:

*   The OS may “fake” available memory
    
*   Scheduler makes bad decisions
    
*   Pods crash unexpectedly
    

So we **must disable swap** to ensure scheduling is predictable.

* * *

## **🧠 Cgroups and Why They Matter**

**Cgroups (Control Groups)** are a Linux kernel feature that:

*   Isolate and limit CPU/RAM for processes (like pods)
    
*   Help enforce resource requests/limits in Kubernetes
    

Cgroup Drivers:

*   systemd: Recommended for modern Ubuntu
    
*   cgroupfs: Older or alternate driver
    

> ⚠️ Both the container runtime and kubelet must use the **same cgroup driver**—or you’ll get errors. Thankfully, kubeadm sets kubelet to use systemd by default on modern distros.

* * *

## **⚙️ Step-by-Step Setup**

### **a. Launch EC2 Instances**

In AWS:

*   Launch 3 EC2 instances
    
*   Ubuntu 24.04 LTS AMI or latest
    
*   SG allows **internal communication** (use custom SG, allow all TCP from self)
    

Label your instances clearly: control-node, worker1, worker2.

* * *

### **b. Prepare All Nodes**

SSH into each node and run:

#### **Set Hostname**

```plaintext
hostnamectl set-hostname control-node # or worker1 / worker2
```

#### **Enable IPv4 Forwarding**

```plaintext
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
```

#### **Disable Swap**

```plaintext
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
```

* * *

### **c. Install and Configure Containerd**

#### **Install containerd**

```plaintext
sudo apt-get update && sudo apt-get install -y containerd

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter
```

#### **Generate Default Config**

```plaintext
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
```

#### **Enable SystemdCgroup**

Update SystemdCgroup to True:

```yaml
nano /etc/containerd/config.toml

# search for below then update to true
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true
```

#### **Restart containerd**

```bash
sudo systemctl restart containerd
sudo systemctl enable containerd
```

* * *

### **d. Install Kubernetes Binaries**

Run on all 3 nodes:

```bash
# Install prerequisites.Update apt and install the packages needed for repository setup 
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg

# Download and save the new signing key.The same key is used for all versions
sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key \
  | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

# Add the new Kubernetes repository. This overwrites any previous Kubernetes list file and points apt to the new repo
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \
https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /" \
| sudo tee /etc/apt/sources.list.d/kubernetes.list

# Update apt and install the Kubernetes binaries
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
```

Note: Executing apt-mark hold command on kubernetes components (like kubeadm, kubelet, kubectl) will stop the components from automatically upgrading so it will always be in sync with the cluster’s version.

If they upgrade unexpectedly:

*   You may break compatibility.
    
*   Your nodes could fail to join the cluster.
    
*   Upgrades must be coordinated carefully using kubeadm upgrade.
    

2.  *(You can substitute* `v1.36` *with another minor release, e.g.* `v1.30`*, if you plan to install a different Kubernetes version.)*
    

* * *

### **e. Initialize Control Plane**

On **control-node** only:

```bash
# replace <PRIVATE-IP> the the actual Private-IP of your server
sudo kubeadm init \
  --apiserver-advertise-address=<PRIVATE-IP> \
  --pod-network-cidr=10.244.0.0/16 \
  --cri-socket=unix:///run/containerd/containerd.sock
```

#### Once your Kubernetes control-plane has initialized successfully

```bash
# start Cluster
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# Alternatively, if you are the root user, you can run:
export KUBECONFIG=/etc/kubernetes/admin.conf
```

* * *

### **f. Join Worker Nodes**

Copy the kubeadm join command shown in the output and run it on **worker1** and **worker2**:

```bash
sudo kubeadm join <CONTROL-PLANE-IP>:6443 --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH> \
  --cri-socket unix:///run/containerd/containerd.sock
```

* * *

### **g. Install Network Add-On**

Use Flannel (compatible with CIDR above):

```bash
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
```

* * *

## **🔍 Validate Setup**

#### **View Nodes**

```bash
kubectl get nodes

# you should see if you setup correctly 
NAME           STATUS   ROLES           AGE   VERSION
control-node   Ready    control-plane   28m   v1.36.2
worker-01      Ready    <none>          20m   v1.36.2
worker-02      Ready    <none>          20m   v1.36.2

# You can label the workers so it reads worker(purely cosmetic)
kubectl label node worker-01 node-role.kubernetes.io/worker=worker
kubectl label node worker-02 node-role.kubernetes.io/worker=worker

NAME           STATUS   ROLES           AGE   VERSION
control-node   Ready    control-plane   38m   v1.36.2
worker-01      Ready    worker          30m   v1.36.2
worker-02      Ready    worker          30m   v1.36.2
```

#### **Create Namespace & Pod**

```bash
kubectl create ns test
kubectl run nginx --image=nginx -n test
kubectl get pods -n test
```

Your Kubernetes cluster is now **fully up and running** with:

*   ✅ control-node: Ready
    
*   ✅ worker1: Ready
    
*   ✅ worker2: Ready
    

You’re all set to start deploying workloads!

* * *

## **🧹 Cleanup Reminder**

Terminate the EC2 instances if you’re not using them to avoid AWS charges.

* * *

## **📌 Conclusion**

You now have:

*   Full control plane setup
    
*   Worker nodes registered
    
*   Networking configured
    
*   A working kubectl setup
    

This setup is **learning-grade in structure**, even if you’re using smaller instances for testing.

In my next blog post I will setup our kubernetes cluster created with kubeadm to be production grade with the following;

*   HA control plane
    
*   Load balancer
    
*   External etcd
    
*   Backup strategy
    
*   Monitoring
    
*   Logging
    
*   Pod security standards
    
*   RBAC hardening
    
*   Ingress controller
    
*   Certificate management
    

After which we’re all set to start deploying workloads.

*   Deploy a voter application
    
*   Set up monitoring (Prometheus, Grafana)
    
*   Configure persistent storage
    
*   Automate with GitOps or CI/CD
