집에 굴러다니는 미니 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 복제 + 분산 스토리지 다 구성하는 건 오버엔지니어링. 대신:

  1. Velero 일일 백업 (R2 로 외부 보관)
  2. PriorityClass 로 OOM 시 누가 살아남을지 우선순위 결정
  3. StorageClass.reclaimPolicy: Retain — PVC 가 실수로 지워져도 PV 보존
  4. 솔로몬 디스크 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 다섯 대를 굴리는 글들은 이 시리즈로 묶어 둡니다.