Skip to main content

Command Palette

Search for a command to run...

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

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

📑 Table of Contents

  1. Introduction

  2. Why Use kubeadm

  3. System Requirements

  4. Cluster Architecture Overview

  5. Networking Requirements

  6. Why Disable Swap

  7. Understanding Cgroups and Drivers

  8. Step-by-Step Setup

  9. Validation and Testing

  10. Cleanup Reminder

  11. Conclusion


📌 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.

  1. (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:

# 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