미니쿠베 없다고 치고, 내 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 1 | VM 1대 (CP 겸 워커) | static pod, etcd 단독, 테인트 제거 |
| Local 2 | VM 3대 (CP 1 + 워커 2) | 조인, 노드 간 파드 통신, 스케줄링 |
AWS편 Case 3(CP 3대 HA)은 로컬에서 재현할 이유가 크지 않음. etcd 쿼럼은 AWS편에서 하는 게 나아 보임.
실습 환경 — 세 가지를 같이 굴림
스터디원 장비가 갈리니 세 환경을 나란히 지원합니다. 절차·노드 이름·완료 기준은 똑같이 맞추고, VM 레이어만 다르게 씀.
| 환경 A | 환경 B | 환경 C | |
|---|---|---|---|
| 호스트 | macOS (Apple M4 Max) | Ubuntu 24.04 | Windows 11 |
| 아키텍처 | arm64 | amd64 | amd64 |
| 메모리 | 32GB | 32GB | 32GB |
| 디스크 여유 | 100GB 이상 | 100GB 이상 | 100GB 이상 |
| 가상화 백엔드 | Apple Virtualization framework | KVM (네이티브) | Hyper-V |
| 대안 도구 | Lima / UTM | libvirt + virt-install | Hyper-V 관리자 / VirtualBox |
| 게스트 OS | Ubuntu 24.04 arm64 | Ubuntu 24.04 amd64 | Ubuntu 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-01 | 4 | 6GB | 30GB | etcd + apiserver + controller-manager + scheduler |
k8s-wk-01 | 2 | 4GB | 25GB | |
k8s-wk-02 | 2 | 4GB | 25GB | |
| 합계 | 8 | 14GB | 80GB | 예산 20GB 중 14GB 사용 |
왜 20GB를 다 안 쓰나 — 세 가지 이유임.
- 재구축할 때 구·신 VM이 잠깐 겹침. 다 태워버리기 전에 새 노드를 띄워 비교하는 순간이 반드시 옴
- 호스트가 IDE·브라우저·회의 앱을 계속 물고 있음. 스왑에 들어가면 etcd가 제일 먼저 비명을 지름 (AWS편 1.4와 같은 증상)
- 스냅샷 복원 여유분 — 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노드가 올라오는지 측정. 안 되면 어디서 시간이 새는지 찾기
완료 기준
세 개가 되면 이 스터디는 끝난 걸로 봄.
- 호스트 브라우저에서 내가 세운 클러스터의 nginx 페이지가 뜬다
- 명령 몇 줄로 3노드가 5분 안에 올라온다 — 안 되면 Step 2.5(VM 정의 파일화)로 돌아감
- 부수는 데 망설임이 없다 — 아깝다는 느낌이 들면 아직 2번이 안 된 것
스터디 진행 제안
4회로 쪼갰습니다. 회차별로 "여기까지 되면 끝"만 정해두고, 방법은 각자 찾아와서 비교하는 식으로 굴릴 생각이에요.
| 회차 | 범위 | 그날의 종료 조건 |
|---|---|---|
| 1회차 | Step 0~2 | 노드 3대가 서로 통신됨 |
| 2회차 | Step 3~4 | 3노드가 Ready로 뜸 |
| 3회차 | Step 5~7 | 브라우저에 nginx 페이지 |
| 4회차 | Step 8 | 5분 재구축 + 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)
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대 모두)
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 containerdcontainerd config default로 덮는 이유: 우분투 기본 설정은 CRI 플러그인이 꺼져 있어서kubeadm init이 바로 실패함SystemdCgroup = true이거 하나가 최다 실패 지점임 (Step 3.3)
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 kubectl3. 엔드포인트를 이름으로 (Step 4.2)
3대 VM 전부 + 호스트의 hosts 파일에 한 줄. (윈도우는 C:\Windows\System32\drivers\etc\hosts, 관리자 권한)
<CP_IP> k8s-api4. CP 초기화 (Step 4.3~4.5)
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 붙이기.
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)
# 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)
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)
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)
# 클러스터만 리셋
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 누락 |
노드가 계속 NotReady | CNI 미설치 |
| join 실패 | 토큰 24시간 만료 |
exec format error | amd64 전용 이미지를 arm64(환경 A)에서 실행 |
| 호스트에서 kubectl 타임아웃 | hosts 파일에 k8s-api 누락 |
| 인증서 오류가 갑자기 | 맥 sleep 후 게스트 시계 어긋남 (환경 A) |
| 어제 되던 게 오늘 안 됨 | Hyper-V Default Switch 서브넷 변경 (환경 C) |
| 스크립트가 이상한 데서 죽음 | CRLF 줄바꿈 (환경 C) |
참고
- kubeadm 설치 요구사항 — 노드 최소 스펙과 사전 조건
- Creating a cluster with kubeadm
- Container Runtimes — cgroup 드라이버
- Multipass — 세 환경 공통 VM 도구
- libvirt — 환경 B, KVM을 직접 다룰 때
- Hyper-V on Windows — 환경 C, 스위치 설정
- CNCF CKA Curriculum