클라우드에서 쿠버네티스 쓰는 건 쉽죠. GKE, EKS, AKS… 버튼 몇 번이면 뚝딱입니다. 그런데 정작 “쿠버네티스가 어떻게 굴러가는가”를 이해하려면, 한 번쯤은 맨땅에서 직접 올려봐야 합니다. 이번 실습은 VirtualBox 위에 VM 3대를 띄우고 kubeadm으로 클러스터를 직접 구성해본 기록입니다.
🎯 목표: 1 Master + 2 Worker 구성
클라우드 없이, 내 노트북 위에서 진짜 쿠버네티스를 돌리겠다는 게 목표였습니다.
| 역할 | 호스트명 | IP | 스펙 |
|---|---|---|---|
| 👑 Control Plane | k8s-master | 192.168.56.10 | 2 vCPU, 4GB RAM |
| 👷 Worker | k8s-worker1 | 192.168.56.11 | 2 vCPU, 4GB RAM |
| 👷 Worker | k8s-worker2 | 192.168.56.12 | 2 vCPU, 4GB RAM |
- 가상화: VirtualBox 7.x (Host-Only Network)
- OS: Ubuntu Server 22.04 LTS
- K8s 버전:
v1.29.15 - CNI: Calico
🏗️ 진행 과정 — 단계별로 뜯어보기
1단계: 사전 환경 설정 (모든 노드 공통)
쿠버네티스는 까다로운 친구라서, 설치 전에 OS 환경을 맞춰줘야 합니다. 세 노드 전부에서 아래를 진행합니다.
Swap 비활성화 — 쿠버네티스는 메모리 관리를 직접 담당하는데, Swap이 켜져 있으면 예측 불가능한 동작이 생깁니다.
sudo swapoff -a
# /etc/fstab에서 swap 관련 줄도 주석 처리
커널 모듈 로드 — 컨테이너 네트워크가 제대로 동작하려면 overlay와 br_netfilter 모듈이 필요합니다.
sudo modprobe overlay
sudo modprobe br_netfilter
브리지 네트워크 허용 — 컨테이너들이 iptables를 통해 통신할 수 있도록 설정합니다.
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
2단계: containerd 설치
Docker 대신 경량 런타임인 containerd를 선택했습니다. 설치 후 반드시 SystemdCgroup = true로 설정해야 kubelet과 cgroup 드라이버가 맞습니다. 이 설정이 빠지면 나중에 클러스터 초기화가 깔끔하게 안 됩니다.
sudo apt install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
# config.toml에서 SystemdCgroup = true 로 수정
sudo systemctl restart containerd
3단계: kubeadm / kubelet / kubectl 설치 + 버전 고정
sudo apt install -y kubeadm kubelet kubectl
# 버전이 자동 업그레이드되면 클러스터가 뻑나므로 반드시 고정!
sudo apt-mark hold kubeadm kubelet kubectl
4단계: 클러스터 초기화 (Master 노드에서만)
드디어 핵심입니다. kubeadm init으로 Control Plane을 초기화합니다.
sudo kubeadm init \
--apiserver-advertise-address=192.168.56.10 \
--pod-network-cidr=10.244.0.0/16
초기화가 끝나면 kubeadm join 명령어가 출력됩니다. 이걸 잘 복사해두세요. Worker 노드를 붙일 때 씁니다.
5단계: CNI 설치 (Calico)
파드끼리 통신하려면 네트워크 플러그인이 필요합니다. Calico를 배포합니다.
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
6단계: Worker 노드 합류
각 Worker 노드에서 아까 복사해둔 join 명령어를 실행합니다.
# k8s-worker1, k8s-worker2 각각에서 실행
sudo kubeadm join 192.168.56.10:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash>
🚨 삽질 포인트 — 가장 황당했던 에러
💥 kubectl get nodes → No route to host
Master 초기화 직후 kubectl을 쳤는데 이런 에러가 났습니다.
Unable to connect to the server: dial tcp 192.168.49.2:8443: connect: no route to host
192.168.49.2? 내 마스터 IP는 192.168.56.10인데 왜 다른 IP로 접속하려 하는 걸까요?
원인: 192.168.49.x 대역은 Minikube가 쓰는 대역입니다. 예전에 Minikube를 설치했던 흔적(~/.kube/config)이 남아있어서, kubectl이 엉뚱한 곳으로 접속을 시도하고 있었던 겁니다.
해결:
# 1. 기존 설정 백업
mv $HOME/.kube/config $HOME/.kube/config.bak
# 2. 새 클러스터 설정으로 교체
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
이 한 줄로 해결됐습니다. kubectl은 ~/.kube/config를 바라본다는 것, 꼭 기억해두세요.
✅ 최종 결과
NAME STATUS ROLES AGE VERSION
k8s-master Ready control-plane 10m v1.29.15
k8s-worker1 Ready <none> 8m v1.29.15
k8s-worker2 Ready <none> 8m v1.29.15
세 노드 모두 Ready 상태! 이 화면을 보는 순간이 가장 뿌듯했습니다.
💡 이걸 해보고 나서 달라진 것
클라우드에서 kubectl만 치던 때와 달리, 이제 “왜 kubelet이 있고, containerd가 뭘 하고, CNI는 언제 필요한가”를 몸으로 이해하게 됐습니다. 클러스터가 망가지면 어디서부터 봐야 할지도 알 수 있고요.
다음 글에서는 이렇게 구성한 클러스터에 Pod를 띄우고, 생명주기를 직접 다뤄보겠습니다.