Introduction
Setting up a Kubernetes cluster manually is the best way to understand how Kubernetes works internally.
In this guide, we set up a 2-node Kubernetes cluster on AWS EC2, where:
- One instance acts as the Master Node
- One instance acts as a Worker Node
This documentation is based on real troubleshooting, not just ideal steps. It includes:
- Correct setup steps
- Common beginner mistakes
- Real errors faced during setup
- Why those errors happened
- Exact commands used to fix them
- AWS Security Group configuration
- Final NGINX deployment and access
By the end, you will have:
- A working Kubernetes cluster
- Clear understanding of pod networking
- Knowledge of CNI (Flannel)
- Practical exposure to NodePort services
Cluster Components
| Component | Description |
|---|---|
| Master Node | Kubernetes control plane |
| Worker Node | Runs application pods |
| Runtime | containerd |
| CNI | Flannel (VXLAN) |
| Exposure | NodePort |
🔹 STEP A: Create AWS EC2 Instances
Create 2 EC2 instances in AWS.
Instance Details
- OS: Ubuntu 22.04 / 24.04 LTS
- Instance Type: t2.medium (minimum)
- VPC/Subnet: Same for both instances
-
Key Pair:
k8s.pem
Instance Names
-
Master Node:
master-node -
Worker Node:
worker-node-1
🔹 STEP B: Configure Security Group (IMPORTANT)
Attach the same Security Group to both instances.
Inbound Rules
| Type | Protocol | Port | Source |
|---|---|---|---|
| SSH | TCP | 22 | Your IP |
| Kubernetes API | TCP | 6443 | VPC CIDR |
| NodePort | TCP | 30080 | 0.0.0.0/0 |
⚠️ Without port 30080 open, NodePort will not work in the browser.
🔹 STEP C: Get Public IPs
After instances start, note:
- Master Public IP
- Worker Public IP
Example:
Worker Node Public IP: 65.0.4.177
🔹 STEP D: Prepare SSH Key on Windows (PowerShell)
Open PowerShell as Administrator in the folder containing k8s.pem.
Fix permission issues (VERY IMPORTANT on Windows)
icacls k8s.pem /inheritance:r
icacls k8s.pem /grant:r "$($env:USERNAME):(R)"
This avoids:
- Bad permissions
- Permission denied (publickey)
🔹 STEP E: SSH into EC2 Instances
Connect to Master Node
ssh -i k8s.pem ubuntu@<MASTER_PUBLIC_IP>
Connect to Worker Node
ssh -i k8s.pem ubuntu@65.0.4.177
👉 You are now inside the Linux terminal of your EC2 instance.
🔹 STEP F: Verify Connection
Run on both nodes:
whoami
hostname
Expected Output
ubuntu
master-node / worker-node-1
STEP 1: Set Hostnames & Hosts File
Master Node
sudo hostnamectl set-hostname master-node
hostname
Worker Node
sudo hostnamectl set-hostname worker-node-1
hostname
Update /etc/hosts on BOTH nodes
sudo nano /etc/hosts
Example:
172.31.xx.xx master-node
172.31.yy.yy worker-node-1
STEP 2: Disable Swap & Enable Kernel Networking
Run on BOTH nodes.
Disable Swap
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
Load Required Kernel Modules
sudo modprobe overlay
sudo modprobe br_netfilter
sudo modprobe vxlan
Persist Modules
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
vxlan
EOF
Enable sysctl Settings
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system
STEP 3: Install containerd Runtime
Run on BOTH nodes.
sudo apt update
sudo apt install -y containerd
Generate Default Config
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
Enable systemd Cgroup
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' \
/etc/containerd/config.toml
Restart & Enable
sudo systemctl restart containerd
sudo systemctl enable containerd
STEP 4: Install Kubernetes Components
Run on BOTH nodes.
sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl gpg
Add Kubernetes Repository
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \
https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /" | \
sudo tee /etc/apt/sources.list.d/kubernetes.list
Install Components
sudo apt update
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
STEP 5: Initialize Master Node
sudo kubeadm init --pod-network-cidr=10.244.0.0/16
Note: Above IP is fixed. Use same IP.
STEP 6: Configure kubectl (Master Node)
mkdir -p $HOME/.kube
sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
STEP 7: Install Flannel CNI
Initial Confusion
kubectl get pods -n kube-system | grep flannel
➡️ Output was empty (Flannel does not run in kube-system)
Correct Installation Command
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
Correct Namespace Check
kubectl get ds -n kube-flannel
kubectl get pods -n kube-flannel
❌ Errors Faced & How They Were Fixed
❌ Error 1: Flannel CrashLoopBackOff
Cause
-
br_netfilternot loaded -
vxlanmissing -
/proc/sys/net/bridgenot present
Fix
sudo modprobe br_netfilter
sudo modprobe vxlan
lsmod | egrep 'vxlan|br_netfilter'
❌ Error 2: sysctl bridge error
cannot stat /proc/sys/net/bridge/bridge-nf-call-iptables
Cause
-
br_netfilternot loaded on worker
Fix (Worker Node)
sudo modprobe br_netfilter
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1
sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=1
Persist Settings
cat <<EOF | sudo tee /etc/sysctl.d/99-kubernetes.conf
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system
❌ Error 3: CoreDNS stuck in ContainerCreating
Cause
- Flannel networking not ready
Fix
kubectl delete pod -n kube-flannel -l app=flannel
kubectl get pods -n kube-flannel
Once Flannel became Running, CoreDNS automatically turned Running.
STEP 8: Join Worker Node
sudo kubeadm join <MASTER-IP>:6443 --token <TOKEN> \
--discovery-token-ca-cert-hash sha256:<HASH>
STEP 9: Validate Cluster
kubectl get nodes
kubectl get pods -n kube-system
kubectl get pods -n kube-flannel
All components must be Running.
🚀 Deploy NGINX Application
nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
kubectl apply -f nginx-deployment.yaml
kubectl get pods
nginx-service.yaml (NodePort)
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30080
kubectl apply -f nginx-service.yaml
kubectl get svc
Access Application
From Worker Node
curl http://localhost:30080
From Browser
http://<WORKER_PUBLIC_IP>:30080
⚠️ Requires AWS Security Group port 30080 open
Conclusion
This setup demonstrated:
- Kubernetes cluster creation using
kubeadm - Real CNI networking issues and fixes
- Importance of kernel modules for Flannel
- AWS networking & NodePort behavior
- End-to-end deployment of an application
Top comments (0)