Skip to content

미니쿠베 없다고 치고, 내 PC에 직접 세우는 초경량 클러스터 ​

  • CKA 스터디 주제2 : 로컬에서 반복하기
  • AWS편의 축소판입니다. 개인 장비 한 대로 비슷한 경험만 뽑아내는 게 목적이에요.

minikube는 지구상에 없다고 가정하고 시작합니다. 설치돼 있으면 지우고 들어가는 게 좋아요. 남아 있으면 막힐 때마다 손이 그쪽으로 갑니다.

본문에는 목표와 할 일만 적었습니다. 명령과 설정은 일부러 뺐어요. 찾아가면서 막히는 지점 자체가 커리큘럼이라고 봐서요. (그래도 필요하면 맨 아래 정답지를 여시면 됩니다)

먼저 알고 갈 것 셋.

  • 이 스터디의 주제는 워크로드가 아니라 부트스트랩임. minikube도 속으로는 kubeadm을 돌림. 다만 대신 해주고 결과만 보여줘서 정작 볼 게 안 남음
  • kubelet·containerd는 리눅스 커널 기능(cgroup, netfilter)에 묶여 있음. 그래서 세 환경 모두 리눅스 VM 위에 올림. Ubuntu 호스트도 예외를 두지 않음 — 부수고 다시 세우는 게 목적이라서
  • 끝나는 지점이 명확함. nginx 페이지 하나가 브라우저에 뜨면 끝이고, 그 이상은 범위 밖임

개요 — 무엇을 만들고 언제 끝내나 ​

목적: kubeadm 부트스트랩을 손으로 반복하며 AWS편을 돈 안 쓰고 리허설하는 것. 완료: 내가 세운 3노드 클러스터의 nginx가 호스트 브라우저에서 페이지를 응답하는 것. 방법: 리눅스 VM을 노드처럼 쓰고, 1노드 → 3노드 두 단계만 함.

minikube를 안 쓰기로 했으니 나머지 후보도 같이 정리해둠. 왜 굳이 직접 하는지에 대한 답임.

도구부팅내부가 보이는가이 스터디에 맞나
minikube빠름✗ (kubeadm을 대신 돌려줌)없다고 가정
kind아주 빠름△ (노드가 컨테이너)부트스트랩이 감춰짐
k3s / k3d아주 빠름✗ (단일 바이너리로 통합)구조가 표준과 다름
VM + kubeadm (이 글)느림✓ (전부 내 손)시험과 같은 구조
단계구성핵심 학습 대상
Local 1VM 1대 (CP 겸 워커)static pod, etcd 단독, 테인트 제거
Local 2VM 3대 (CP 1 + 워커 2)조인, 노드 간 파드 통신, 스케줄링

AWS편 Case 3(CP 3대 HA)은 로컬에서 재현할 이유가 크지 않음. etcd 쿼럼은 AWS편에서 하는 게 나아 보임.


실습 환경 — 세 가지를 같이 굴림 ​

스터디원 장비가 갈리니 세 환경을 나란히 지원합니다. 절차·노드 이름·완료 기준은 똑같이 맞추고, VM 레이어만 다르게 씀.

환경 A환경 B환경 C
호스트macOS (Apple M4 Max)Ubuntu 24.04Windows 11
아키텍처arm64amd64amd64
메모리32GB32GB32GB
디스크 여유100GB 이상100GB 이상100GB 이상
가상화 백엔드Apple Virtualization frameworkKVM (네이티브)Hyper-V
대안 도구Lima / UTMlibvirt + virt-installHyper-V 관리자 / VirtualBox
게스트 OSUbuntu 24.04 arm64Ubuntu 24.04 amd64Ubuntu 24.04 amd64

세 환경 공통 규약 ​

  • VM 도구는 Multipass로 통일함. 세 OS를 다 지원하고 명령이 같아서, 스터디에서 "환경별로 다른 명령"을 설명할 일이 없어짐
  • 게스트는 Ubuntu 24.04로 통일 (최소 20.04 LTS 이상)
  • 노드 이름은 k8s-cp-01 / k8s-wk-01 / k8s-wk-02
  • 완료 기준도 동일 — 호스트 브라우저에서 nginx 응답

알고 갈 차이 ​

  • 아키텍처가 갈리는 게 이 구성의 가장 큰 변수임. A는 arm64, B·C는 amd64. 같은 매니페스트를 돌리면 arm64 이미지가 없는 애드온에서 A만 깨짐. 귀찮지만 멀티아치가 뭔지 몸으로 배우는 구간이라 일부러 살려둠
  • 호스트에 직접 깔 수 있는 건 B뿐인데, 한 번 세우면 되돌리기가 어려워서 이 스터디에선 비권장. 부수는 게 목적임
  • 팬·발열·배터리는 신경 쓰지 않음. 세션 길이 제한 없이 감

리소스 예산 — VM에 몇 GB를 떼줄 것인가 ​

호스트 32GB 중 VM에 최대 20GB까지 떼줄 수 있는 상황을 기준으로 잡음. 실제로는 14GB만 배정하고 6GB를 남김. 세 환경 모두 같은 배정을 씀.

노드vCPU메모리디스크(선언)비고
k8s-cp-0146GB30GBetcd + apiserver + controller-manager + scheduler
k8s-wk-0124GB25GB
k8s-wk-0224GB25GB
합계814GB80GB예산 20GB 중 14GB 사용

왜 20GB를 다 안 쓰나 — 세 가지 이유임.

  1. 재구축할 때 구·신 VM이 잠깐 겹침. 다 태워버리기 전에 새 노드를 띄워 비교하는 순간이 반드시 옴
  2. 호스트가 IDE·브라우저·회의 앱을 계속 물고 있음. 스왑에 들어가면 etcd가 제일 먼저 비명을 지름 (AWS편 1.4와 같은 증상)
  3. 스냅샷 복원 여유분 — Step 8에서 자주 씀
  • CP에 메모리를 더 준 이유는 etcd 때문임. 디스크·메모리 지연에 가장 먼저 반응하는 게 etcd고, 로컬에서도 똑같음
  • kubeadm 최소 요구는 노드당 2 vCPU / 2GB. 위 배정은 그 위로 넉넉히 잡은 것이라 스로틀링 걱정은 없음
  • 세 환경 다 vCPU는 병목이 아님. 병목은 메모리와 디스크 I/O 쪽에서 먼저 옴
  • 디스크는 선언량 기준. 스파스 할당이라 실사용은 훨씬 적지만 이미지 pull이 쌓이면 계속 늘어남 → 정리를 재구축 사이클에 포함시킬 것

축소해야 할 때 배정.

구성메모리 배정언제 쓰나
Local 1 (1노드)8GB (4 vCPU / 30GB)Step 4까지 연습할 때
2노드 (CP + 워커 1)10GB호스트가 빡빡할 때. 조인 경험은 그대로 남음
Local 2 (3노드)14GB기본 구성

환경별 함정 ​

AWS편 Phase 2와 같은 자리. 시작 전에 먼저 읽는 게 이득임.

공통 ​

  • [ ] 호스트에서 Pod/Service CIDR로 직접 라우팅 안 됨 — VM 네트워크 밖이라 호스트에서 ClusterIP를 때려도 안 됨. 포트포워드 / NodePort / 호스트 라우트 중 접근 경로를 하나 정해야 함. 완료 기준(nginx 응답)이 여기에 걸림
  • [ ] LoadBalancer 타입은 pending으로 멈춤 — CCM이 없음. AWS편 2.5와 같은 상황이고 원인도 같음
  • [ ] VM 내부 swap — 호스트 swap과 무관하게 게스트 안에서 꺼야 함. 클라우드 이미지에 따라 기본 켜져 있음
  • [ ] VM 간 통신이 되는 네트워크 모드인지 먼저 확인 — 여기서 안 되면 3노드 조인이 아예 막힘. Step 2에서 가장 먼저 검증할 것
  • [ ] 디스크가 조용히 참 — 이미지 pull이 누적됨. 100GB 여유도 재구축 몇십 번이면 줄어듦

환경 A — macOS (Apple Silicon) ​

  • [ ] arm64 이미지가 없는 애드온이 있음 — amd64 전용을 물면 exec format error로 죽음. 증상만 보면 원인 추정이 어려우니 미리 알고 갈 것
  • [ ] sleep에서 깨면 게스트 시계가 튐 — 인증서 검증 실패나 etcd 리더 선출 이상으로 나타남. 실습 중 뚜껑 닫았다 열었으면 의심해볼 것
  • [ ] 호스트 디렉토리 공유 볼륨이 느림 — hostPath 실습은 VM 내부 디스크로

환경 B — Ubuntu 24.04 호스트 ​

  • [ ] 중첩 가상화가 아니라 네이티브 KVM — 오버헤드가 작아 같은 예산으로 더 잘 돎. 대신 호스트 방화벽·브리지 설정을 직접 봐야 함
  • [ ] 호스트에도 컨테이너 런타임이 깔려 있으면 헷갈림 — 호스트의 docker/containerd와 게스트의 것을 구분할 것
  • [ ] 호스트에 직접 kubeadm을 깔고 싶어지는 유혹 — 되긴 되는데 리셋이 지저분함. Step 8이 무너짐

환경 C — Windows 11 ​

  • [ ] Hyper-V Default Switch는 재부팅할 때마다 서브넷이 바뀜 — 노드 IP가 통째로 갈려서 멀쩡하던 클러스터가 다음 날 죽어 있음. 환경 C에서 시간을 가장 크게 잡아먹는 지점. 고정 IP를 쓸 수 있는 스위치로 잡아야 함
  • [ ] WSL2를 노드로 쓰지 말 것 — WSL2 배포판들은 같은 VM·같은 네트워크를 공유해서 노드 3대로 안 갈라짐. 리눅스 셸 용도로만
  • [ ] Hyper-V는 Pro/Enterprise 에디션만 — Home이면 VirtualBox 드라이버로 우회. 성능 손해는 감수해야 함
  • [ ] CRLF 줄바꿈 — 윈도우에서 편집한 스크립트·YAML을 VM에 옮기면 \r 때문에 엉뚱한 에러로 죽음. 원인 찾기 제일 짜증나는 부류라 에디터를 LF로 먼저 고정해둘 것
  • [ ] hosts 파일은 관리자 권한 — C:\Windows\System32\drivers\etc\hosts. Step 4.2에서 필요함
  • [ ] Windows Defender 방화벽이 NodePort를 막을 수 있음 — 완료 기준 직전에 걸리는 경우가 있음

Step 0. 무엇을 만들지 먼저 정하기 ​

이 스터디는 아래로 고정하고 시작함. 바꾸고 싶으면 여기만 바꾸면 됨.

  • [x] 0.1 연습 대상: 부트스트랩. 워크로드·스케줄링은 곁다리로만 봄
  • [x] 0.2 minikube 완전 삭제. 남아 있으면 막힐 때 손이 그쪽으로 감
  • [x] 0.3 완료 기준: nginx 초간단 페이지가 호스트 브라우저에서 응답. 그 이상 안 함
  • [ ] 0.4 재구축 목표 시간 5분 — 안 되면 단계를 더 줄일 것
  • [ ] 0.5 내 환경이 A·B·C 중 뭔지 확정하기

Step 1. 리소스 배정 확정 ​

  • [ ] 1.1 위 예산표대로 노드 3대 스펙 확정 (합계 14GB)
  • [ ] 1.2 호스트 여유 메모리 실측 — 실습 중 스왑에 들어가는지 확인
  • [ ] 1.3 디스크 여유 확인하고 정리 명령을 재구축 절차에 미리 포함
  • [ ] 1.4 3노드가 버거우면 2노드(10GB)로 축소 — 조인 경험은 남음

Step 2. VM 레이어 세우기 ​

  • [ ] 2.1 Multipass 설치 (환경별 설치 방법만 다르고 이후 명령은 같음)
  • [ ] 2.2 VM 간 통신 검증부터. 노드 3대가 서로 닿는 게 확인되기 전엔 다음으로 가지 말 것
  • [ ] 2.3 호스트에서 각 VM으로 셸 접속 확인
  • [ ] 2.4 hostname 규칙 확정 (k8s-cp-01, k8s-wk-01, k8s-wk-02) — 세 환경 공통으로 맞출 것
  • [ ] 2.5 VM 정의를 파일이나 스크립트로 남기기 — 손으로 클릭해서 만들면 재구축 5분은 불가능함
  • [ ] 2.6 깨끗한 상태에서 VM 스냅샷 한 번 (Step 8에서 계속 씀)

Step 3. 노드 준비 (AWS편 Phase 4~6 축약) ​

  • [ ] 3.1 swap 끄기, 커널 모듈·sysctl 세팅
  • [ ] 3.2 VM 간 이름 해석 확인
  • [ ] 3.3 컨테이너 런타임 설치 — cgroup 드라이버를 systemd로 (AWS편 5.2와 같은 최다 실패 지점)
  • [ ] 3.4 kubeadm / kubelet / kubectl 설치 후 버전 고정
  • [ ] 3.5 kubelet이 잡는 노드 IP가 VM 네트워크 대역인지 확인
  • [ ] 3.6 3대에 같은 걸 반복하는 순간 자동화 도입 — 셸 스크립트든 Ansible이든

Step 4. 클러스터 세우기 ​

  • [ ] 4.1 Pod CIDR / Service CIDR을 VM 네트워크 대역과 겹치지 않게 정하기
  • [ ] 4.2 API 엔드포인트를 IP가 아니라 이름으로 잡기 (AWS편 0.3과 같은 이유. 로컬은 hosts 파일로 충분)
  • [ ] 4.3 CP 초기화 → static pod와 etcd가 뜨는지 눈으로 확인
  • [ ] 4.4 이 시점 노드가 NotReady인 이유를 설명할 수 있는지 스스로 점검
  • [ ] 4.5 호스트에서 kubectl이 붙도록 kubeconfig 배치
  • [ ] 4.6 Local 1 — 조인 없이 CP 테인트만 제거
  • [ ] 4.7 Local 2 — 워커 2대 조인. 토큰 만료되면 재발급 절차 익히기

Step 5. CNI와 접근 경로 ​

  • [ ] 5.1 CNI 하나 선택 — arm64·amd64 양쪽 이미지가 있는지 먼저 확인 (A와 B·C가 같이 써야 함)
  • [ ] 5.2 MTU 확인 — VM 네트워크 위에 오버레이면 같은 문제가 그대로 남
  • [ ] 5.3 CoreDNS가 Running으로 넘어가는지 확인
  • [ ] 5.4 노드 간 파드 통신 확인 (Local 2에서만 의미 있음)
  • [ ] 5.5 호스트에서 서비스에 닿는 경로 하나 정하기 — 포트포워드 / NodePort 중 택1. 완료 기준이 이 경로를 탐

Step 6. 애드온은 최소로 ​

  • [ ] 6.1 스토리지 — local-path 정도면 충분
  • [ ] 6.2 metrics-server (HPA까지 볼 생각이면)
  • [ ] 6.3 Ingress·모니터링 스택은 넣지 말 것 — 14GB 안에서 이게 실습을 밀어냄

Step 7. 완료 기준 달성 — nginx 응답시키기 ​

이 스터디의 종착점임. 아래 순서로 하나씩 확인.

  • [ ] 7.1 nginx Deployment 배포 → 파드 Running
  • [ ] 7.2 replica 늘려서 워커 2대에 나눠 뜨는지 확인
  • [ ] 7.3 Service 붙이고 클러스터 안에서 응답 확인
  • [ ] 7.4 Step 5.5에서 정한 경로로 노출
  • [ ] 7.5 호스트 브라우저에서 페이지 뜨는지 확인 ← 여기까지가 완료
  • [ ] 7.6 파드 안에서 DNS 조회 한 번
  • [ ] 7.7 (여유 있으면) PVC 붙이고 파드 재시작 후 데이터 남는지

Step 8. 부수고 다시 세우기 ​

여기부터가 진짜임. 7번까지는 한 번 하면 끝이고, 8번은 반복하는 것임.

  • [ ] 8.1 워커 VM 하나 강제 종료 → 파드 재스케줄 타이밍 관찰
  • [ ] 8.2 CP VM 종료 → 기존 파드는 계속 도는 것 확인 (kubelet 독립성)
  • [ ] 8.3 etcd 스냅샷 받고 실제로 복원까지 해보기
  • [ ] 8.4 클러스터 리셋 후 같은 절차로 재구축
  • [ ] 8.5 Step 2.6 스냅샷에서 복원 → Step 3부터 다시
  • [ ] 8.6 5분 안에 3노드가 올라오는지 측정. 안 되면 어디서 시간이 새는지 찾기

완료 기준 ​

세 개가 되면 이 스터디는 끝난 걸로 봄.

  1. 호스트 브라우저에서 내가 세운 클러스터의 nginx 페이지가 뜬다
  2. 명령 몇 줄로 3노드가 5분 안에 올라온다 — 안 되면 Step 2.5(VM 정의 파일화)로 돌아감
  3. 부수는 데 망설임이 없다 — 아깝다는 느낌이 들면 아직 2번이 안 된 것

스터디 진행 제안 ​

4회로 쪼갰습니다. 회차별로 "여기까지 되면 끝"만 정해두고, 방법은 각자 찾아와서 비교하는 식으로 굴릴 생각이에요.

회차범위그날의 종료 조건
1회차Step 0~2노드 3대가 서로 통신됨
2회차Step 3~43노드가 Ready로 뜸
3회차Step 5~7브라우저에 nginx 페이지
4회차Step 85분 재구축 + etcd 복원 성공

환경 A·B·C를 섞어서 진행하면 좋을 것 같습니다. 같은 단계에서 서로 다른 데가 막히는데, 그 차이를 비교하는 게 혼자 할 때는 안 나오는 소득이에요. 특히 A(arm64)와 B·C(amd64)가 갈리는 지점은 각자 화면 띄워놓고 같이 보면 좋겠습니다.

이 글에서 일부러 뺀 것 ​

  • 명령·설정 파일 전문 — 본문에선 뺐고 맨 아래 정답지에만 최소한으로 넣었음. 먼저 찾아보고 막힐 때 여는 용도임
  • HA 컨트롤 플레인 — 14GB 예산에 안 들어감. AWS편에서
  • CCM / 클라우드 연동 — 로컬엔 붙일 클라우드가 없음
  • Ingress·모니터링 — 완료 기준(nginx 응답)에 필요 없음

부록. 정답지 (macOS / Apple Silicon 기준) ​

먼저 스스로 해보고, 막혔을 때만 여는 걸 권합니다. 환경 A 기준 최단 경로만 적었어요. B·C는 1번(VM 생성)만 각 OS의 Multipass 설치 방법으로 바꾸면 2번부터는 그대로 같습니다.

1. VM 3대 (Step 1~2) ​

bash
brew install --cask multipass
multipass launch 24.04 --name k8s-cp-01 --cpus 4 --memory 6G --disk 30G
multipass launch 24.04 --name k8s-wk-01 --cpus 2 --memory 4G --disk 25G
multipass launch 24.04 --name k8s-wk-02 --cpus 2 --memory 4G --disk 25G
multipass list          # IP 확인 → 이 IP를 아래에서 계속 씀

2. 노드 공통 준비 (Step 3, 3대 모두) ​

bash
sudo swapoff -a && sudo sed -i '/ swap / s/^/#/' /etc/fstab

printf 'overlay\nbr_netfilter\n' | sudo tee /etc/modules-load.d/k8s.conf
sudo modprobe overlay && sudo modprobe br_netfilter

printf 'net.bridge.bridge-nf-call-iptables=1\nnet.ipv4.ip_forward=1\n' \
  | sudo tee /etc/sysctl.d/k8s.conf
sudo sysctl --system

sudo apt-get update && sudo apt-get install -y containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
  • containerd config default로 덮는 이유: 우분투 기본 설정은 CRI 플러그인이 꺼져 있어서 kubeadm init이 바로 실패함
  • SystemdCgroup = true 이거 하나가 최다 실패 지점임 (Step 3.3)
bash
K8S_MINOR=v1.33   # [확인 필요] 현재 stable 마이너로 교체할 것
curl -fsSL https://pkgs.k8s.io/core:/stable:/${K8S_MINOR}/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:/${K8S_MINOR}/deb/ /" \
  | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update && sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

3. 엔드포인트를 이름으로 (Step 4.2) ​

3대 VM 전부 + 호스트의 hosts 파일에 한 줄. (윈도우는 C:\Windows\System32\drivers\etc\hosts, 관리자 권한)

<CP_IP>  k8s-api

4. CP 초기화 (Step 4.3~4.5) ​

bash
sudo kubeadm init \
  --control-plane-endpoint=k8s-api:6443 \
  --apiserver-cert-extra-sans=k8s-api,<CP_IP> \
  --pod-network-cidr=10.244.0.0/16

호스트에서 kubectl 붙이기.

bash
multipass exec k8s-cp-01 -- sudo cat /etc/kubernetes/admin.conf > ~/.kube/config-lab
export KUBECONFIG=~/.kube/config-lab
kubectl get nodes        # NotReady 정상 — CNI가 아직 없음

5. 조인 / 테인트 (Step 4.6~4.7) ​

bash
# CP에서 조인 명령 재발급
kubeadm token create --print-join-command
# 워커 2대에서 그대로 실행 (sudo)

# Local 1(단일 노드)로 갈 때만
kubectl taint nodes --all node-role.kubernetes.io/control-plane-

6. CNI (Step 5) ​

bash
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
kubectl get nodes -w     # Ready 전환 확인
  • Flannel을 고른 이유: arm64·amd64 이미지가 다 있어서 세 환경이 같이 쓸 수 있고, 기본 Pod CIDR이 위 10.244.0.0/16과 그대로 맞음
  • 맥의 Multipass 대역(보통 192.168.64.0/24)과도 안 겹침

7. 완료 기준 — nginx (Step 7) ​

bash
kubectl create deployment nginx --image=nginx:alpine --replicas=2
kubectl expose deployment nginx --type=NodePort --port=80
kubectl get pods -o wide      # 워커 2대에 나눠 떴는지
kubectl get svc nginx         # 30000~32767 중 할당된 포트 확인

호스트 브라우저에서 http://<워커_IP>:<NodePort> → nginx 기본 페이지가 뜨면 끝.

  • LoadBalancer 타입은 pending에서 안 넘어감. CCM이 없어서고, 정상임

8. 부수기 (Step 8) ​

bash
# 클러스터만 리셋
sudo kubeadm reset -f && sudo rm -rf /etc/cni/net.d ~/.kube

# VM 통째로
multipass delete --all --purge

# 스냅샷 (중지 상태에서만 됨)
multipass stop k8s-cp-01 && multipass snapshot k8s-cp-01
multipass restore k8s-cp-01.snapshot1

막혔을 때 — 증상 → 원인 ​

증상유력 원인
kubeadm init이 CRI 못 찾음containerd 기본 설정에 CRI 비활성
kubelet 재시작 루프SystemdCgroup 누락
노드가 계속 NotReadyCNI 미설치
join 실패토큰 24시간 만료
exec format erroramd64 전용 이미지를 arm64(환경 A)에서 실행
호스트에서 kubectl 타임아웃hosts 파일에 k8s-api 누락
인증서 오류가 갑자기맥 sleep 후 게스트 시계 어긋남 (환경 A)
어제 되던 게 오늘 안 됨Hyper-V Default Switch 서브넷 변경 (환경 C)
스크립트가 이상한 데서 죽음CRLF 줄바꿈 (환경 C)

참고 ​