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

Search for a command to run...

No comments yet. Be the first to comment.
Jenkins is an essential tool for automating software builds, deployments, and CI/CD pipelines. If you're running Jenkins on an AWS EC2 instance, exposing it securely over the internet is crucial. By default, Jenkins runs on port 8080 over HTTP, which...

Introduction Cloud computing is being adopted vastly by more than 90 percent of the fortune 500 companies with Amazon Web Services (AWS) getting a large slice of the pie while it continues on it's quest for world domination in the cloud computing spa...

Guide on how to create a Python monitoring application using Flask, containerize it using Docker, and deploy it to kubernetes.

Web scraping is the process of extracting data from websites, which can be a very useful technique for obtaining large amounts of data quickly and efficiently. In this blog, we will explore how to write a web scraper in Python using the requests and ...

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.
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.
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)
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.
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.
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 (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.
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.
SSH into each node and run:
hostnamectl set-hostname control-node # or worker1 / worker2
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
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
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
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
sudo systemctl restart containerd
sudo systemctl enable containerd
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.
v1.36 with another minor release, e.g. v1.30, if you plan to install a different Kubernetes version.)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
# 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
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
Use Flannel (compatible with CIDR above):
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
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
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!
Terminate the EC2 instances if you’re not using them to avoid AWS charges.
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