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

📑 Table of Contents
📌 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
hostnamectl set-hostname control-node # or worker1 / worker2
Enable IPv4 Forwarding
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
Disable Swap
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
c. Install and Configure Containerd
Install containerd
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
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
Enable SystemdCgroup
Update SystemdCgroup to True:
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
sudo systemctl restart containerd
sudo systemctl enable containerd
d. Install Kubernetes Binaries
Run on all 3 nodes:
# 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.
- (You can substitute
v1.36with another minor release, e.g.v1.30, if you plan to install a different Kubernetes version.)
e. Initialize Control Plane
On control-node only:
# 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
# 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:
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):
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
🔍 Validate Setup
View Nodes
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
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



