K3s 실전 1편 — 5대 서버 K3s 클러스터를 4-Tier로 쪼개기
집에 굴러다니는 미니 PC 와 노트북을 모아 5대 서버 K3s 클러스터 를 만들었습니다. 처음에는 그냥 다 워커로 묶었는데 한 노드에 죄다 몰리는 문제가 생겨서, 결국 4-Tier 역할 분리 로 다시 설계했습니다. 이 글은 그 과정과 최종 구도를 정리합니다.
이 글에서 다루는 것
- 5대 노드의 스펙과 역할 (르무엘 / 루이스 / 데이비드 / 일원 / 솔로몬)
- 4-Tier 모델: Edge / Management / Worker / Storage
- nodeSelector, taints, label 로 워크로드를 어디에 보낼지 강제하는 법
- 단일 장애점(SPOF) 을 어떻게 받아들였는지
1. 왜 4-Tier 로 나눴나
처음에는 “워커는 다 똑같으니까 라벨도 안 붙이고 그냥 K3s 가 알아서 스케줄링하게 두자” 고 생각했습니다. 결과는:
- 컨트롤 플레인 노드(르무엘) 에 ArgoCD, Velero, 모니터링, DB, 앱 다 떠서 CPU Load 11.0 까지 치솟음
- DB 가 앱 옆에서 같이 OOM 으로 죽음 (메모리 burst 가 DB 를 밀어냄)
- 디스크 용량이 가장 큰 노드(솔로몬 686GB)는 idle, 디스크 작은 노드는 PV 가 못 들어가서 PVC pending
그래서 하드웨어의 강점과 약점에 맞게 역할을 고정 하기로 했습니다.
2. 노드 별 스펙
| 노드 | 역할 | CPU | RAM | Disk | IP |
|---|---|---|---|---|---|
| 르무엘 | control-plane | 4c | 31GB | 500GB | .101 |
| 루이스 | worker | 8c | 15GB | 500GB | .109 |
| 데이비드 | observability | 4c | 16GB | 256GB | .107 |
| 일원 | worker (메인) | 12c (i7-8700) | 14→32GB ★ | 457GB | .110 |
| 솔로몬 | storage | 8c | 16GB | 686GB | .108 |
💡 포인트: 솔로몬은 디스크가 가장 큼. → DB 전용으로 박는다. 일원은 CPU 코어가 가장 많음. → 앱 메인 워커.
3. 4-Tier 분류
Tier 1 — Edge (외부 노출)
르무엘. 집 공유기에 가장 가까운 노드. Cloudflare Tunnel daemon 만 여기서 돌립니다.
- ingress NodePort 는 어디서 떠도 OK (kube-proxy 가 redirect 해줌)
- Cloudflared 만 르무엘 에 고정 → Cloudflare 라우트 IP 변경 불필요
# cloudflared DaemonSet 대신 systemd 로 르무엘 로컬 운영
nodeSelector:
kubernetes.io/hostname: lemuel
Tier 2 — Management (시스템 코어)
르무엘 (control-plane) + 데이비드 (observability). K3s API server, etcd, scheduler, controller-manager 는 컨트롤 플레인 노드에 묶이고, 모니터링 스택은 별도 노드(데이비드)에 격리합니다.
# 데이비드 노드에 라벨 부착
kubectl label node david tier=observability
# 모니터링 차트 values.yaml
nodeSelector:
tier: observability
이렇게 해두면 데이비드 가 죽어도 워크로드는 멀쩡, 워크로드 노드가 죽어도 모니터링은 살아남아서 죽음의 원인을 쫓아갈 수 있음 입니다.
Tier 3 — Worker (앱)
일원 + 루이스. 일반 stateless 앱(Spring, Next.js, Node) 이 떠야 할 곳.
# 앱 Deployment
spec:
template:
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: tier
operator: In
values: [worker]
preferred 로 둔 이유: 워커가 부족하면 솔로몬으로라도 가게(SPOF 회피). 단, 그 반대는 막아야 합니다:
Tier 4 — Storage (DB / 영속 데이터)
솔로몬. local-path-provisioner 의 nodePathMap 으로 솔로몬에만 PV 가 만들어지게 강제합니다.
# StatefulSet
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: solomon # ★ 솔로몬에만
tolerations:
- key: storage-only
operator: Exists
effect: NoSchedule
# 솔로몬에 taint 걸어서 일반 워크로드 차단
kubectl taint node solomon storage-only=true:NoSchedule
이러면 일반 앱은 솔로몬에 못 붙고, DB 는 nodeSelector + tolerations 로 솔로몬에 들어감.
4. 라벨 / 테인트 적용 일괄 명령
# 라벨
kubectl label node lemuel tier=control-plane --overwrite
kubectl label node louise tier=worker --overwrite
kubectl label node david tier=observability --overwrite
kubectl label node ilwon tier=worker --overwrite
kubectl label node solomon tier=storage --overwrite
# 테인트
kubectl taint node solomon storage-only=true:NoSchedule
kubectl taint node david monitoring-only=true:NoSchedule
# 컨트롤플레인은 K3s 가 기본으로 NoSchedule 안 검 (단일 컨트롤플레인 환경)
# 따라서 르무엘 에는 시스템 워크로드만 가도록 priority 로 다시 거름 (다음 글에서)
5. 외부 노출 — Cloudflare Tunnel 한 곳에 고정
5 노드 어디서 떠도 NodePort 30xxx 로 받기 때문에 라우트는 하나만 가리키면 됩니다.
# cloudflared config.yml
ingress:
- hostname: argo.lemuel.co.kr
service: http://192.168.219.101:32287 # 르무엘:NodePort
- hostname: settlement.lemuel.co.kr
service: http://192.168.219.101:30808
- service: http_status:404
K3s 가 다른 노드로 Pod 를 옮겨도 NodePort 는 변하지 않으니까 Cloudflare 라우트 갱신 불필요. 노드 IP 가 바뀌면 다시 잡아주면 됩니다.
6. 단일 장애점(SPOF) 은 어떻게 받아들였나
이 구도의 명백한 약점:
- 솔로몬 죽으면 → 모든 DB 접근 불가
- 르무엘 죽으면 → control-plane 다운, kubectl 안 됨 (앱은 돈다)
- 일원 죽으면 → 메인 워커가 사라짐
홈랩 수준에서 HA 컨트롤플레인 + DB 복제 + 분산 스토리지 다 구성하는 건 오버엔지니어링. 대신:
- Velero 일일 백업 (R2 로 외부 보관)
- PriorityClass 로 OOM 시 누가 살아남을지 우선순위 결정
- StorageClass.reclaimPolicy: Retain — PVC 가 실수로 지워져도 PV 보존
- 솔로몬 디스크 RAID (다음 작업)
진짜 운영이라면 솔로몬 1대 더 두고 Longhorn 같은 분산 스토리지를 써야겠지만, 홈랩 / 사이드프로젝트 단계에서는 백업 + 빠른 복구 가 더 현실적인 선택이었습니다.
7. 실측 효과
| 항목 | 분리 전 | 분리 후 |
|---|---|---|
| 르무엘 Load (1m) | 11.0 | 6.x → 1.x 목표 |
| 솔로몬 디스크 사용 | 0% | DB 8개 약 30GB (4%) |
| 데이비드 CPU | idle | 모니터링 평균 10% |
| 일원 활용도 | idle | 앱 떠나면 50% 예정 |
르무엘 컨트롤 플레인 한 노드가 다 짊어지던 부하를, 4 곳으로 펼치면서 각각이 자기 일만 하는 구조 가 잡혔습니다.
다음 글
- K3s 실전 2편 — 솔로몬에 DB 몰빵하기 (StorageClass + StatefulSet)
- K3s 실전 3편 — LimitRange / ResourceQuota / PriorityClass 거버넌스
- K3s 실전 4편 — flannel cross-node DNS 함정과 NodeLocal DNSCache
홈랩 K3s 다섯 대를 굴리는 글들은 이 시리즈로 묶어 둡니다.