폐쇄망 k0s — 노드 1·2·3대와 그 위의 Ceph·PostgreSQL
노드 3대 — CPLB·NLLB 와 겸임 노드
한 줄 요약
3대는 etcd 가 한 대를 잃어도 버티는 가장 작은 구성입니다. k0s 에서는 셋 다 컨트롤러+워커로 두고, API 주소는 CPLB 의 VIP 로, 워커의 내부 연결은 NLLB 로 풀면 바깥 부하 분산기 없이 HA 가 됩니다.
왜 이게 필요했나
2대 구성은 컨트롤러가 하나라 제어면이 단일 장애점이었습니다. 컨트롤러를 셋으로 늘리면 etcd 과반이 2라 하나를 잃어도 씁니다. 그런데 클라이언트와 워커가 「어느 컨트롤러에 붙나」 가 남습니다. kubeconfig 에 ctrl01 의 주소를 적어 두면 ctrl01 이 죽을 때 kubectl 도 함께 죽습니다. 보통은 그 앞에 HAProxy 같은 부하 분산기를 두지만, 그것을 이중화하는 일이 다시 생깁니다.
어떻게 동작하나
CPLB. k0s 가 컨트롤러마다 keepalived 를 띄워 VRRP 로 가상 주소(VIP) 하나를 셋 중 한 대에 올립니다. VIP 를 가진 노드가 사라지면 다른 노드가 몇 초 안에 VIP 를 가져갑니다. 들어온 요청은 k0s 안의 사용자 공간 프록시가 살아 있는 API 서버들로 나눕니다. VIP 는 노드와 같은 망에 있어야 하고, 노드에 NIC 가 여럿이면 어느 NIC 에 띄울지 적어야 합니다.
NLLB. 워커의 kubelet·kube-proxy 는 노드마다 뜨는 작은 envoy(루프백)에 붙고, envoy 가 살아 있는 컨트롤러 셋으로 나눕니다. CPLB 가 바깥(사람·도구)을 위한 것이라면 NLLB 는 클러스터 안쪽을 위한 것입니다. 둘을 함께 쓰는 것이 문서의 권장입니다.
컨트롤러+워커와 noTaints. 컨트롤러가 워커를 겸하면 기본으로 컨트롤 플레인 테인트가 붙어 일반 파드가 앉지 못합니다. 노드가 셋뿐이고 Ceph(mon·osd 셋)와 PostgreSQL(인스턴스 셋)을 노드마다 하나씩 앉혀야 하므로 테인트를 풉니다. 대가는 제어면과 워크로드가 같은 메모리를 나눈다는 것이라, 노드를 넉넉히 잡고 Ceph 데몬의 메모리를 낮춥니다. 노드가 다섯 이상이면 컨트롤러 셋을 따로 두는 쪽이 낫습니다.
폐쇄망의 이미지. k0s 시스템 이미지는 묶음으로, 그 위의 Rook·Ceph·CSI·CNPG·cert-manager 이미지는 사내 레지스트리로 들입니다. 노드의 containerd 에 hosts.toml 을 두어 quay.io·docker.io·ghcr.io·registry.k8s.io 를 사내 레지스트리로 돌리면 차트와 매니페스트의 이미지 이름을 하나도 고치지 않아도 됩니다. 미러에 없는 이미지는 바깥으로 가려다 막혀 ImagePullBackOff 가 되므로, 무엇이 빠졌는지가 바로 보입니다.
k0s 위의 Ceph. k0s 의 kubelet 디렉터리는 /var/lib/k0s/kubelet 입니다. CSI 노드 플러그인은 그 아래에 소켓을 두고 볼륨을 마운트하므로, 드라이버 차트의 kubeletDirPath 를 맞춥니다.
바깥 부하 분산기와의 비교. HAProxy·nginx 같은 바깥 부하 분산기를 두면(k0s 문서의 6443·8132·9443 세 포트) 컨트롤러와 무관한 자리에서 주소를 지킬 수 있지만, 그 부하 분산기 자체를 다시 이중화해야 하고 폐쇄망에는 그 패키지도 반입해야 합니다. CPLB 는 k0s 바이너리 안의 keepalived 와 프록시를 쓰므로 반입할 것이 늘지 않습니다. 대신 VIP 는 노드들이 같은 L2 망에 있어야 하고(VRRP), 클라우드처럼 L2 가 없는 곳에서는 쓸 수 없습니다.
CNPG 와 Ceph 를 함께 둘 때. PostgreSQL 은 자기 복제(스트리밍 복제)를 하고 Ceph RBD 도 3벌 복제를 하므로 데이터가 9벌이 됩니다. 쓰기 지연이 두 번 쌓이는 대가로, 노드 하나가 사라져도 DB 복제본과 볼륨이 모두 남습니다. 장애 조치는 CNPG 가 하고(새 primary 승격), Ceph 는 그 사이에도 볼륨을 계속 냅니다. 백업은 또 다른 축입니다 — 같은 클러스터의 RGW 는 「다른 장애 영역」 이 아니므로 운영에서는 RGW 버킷을 다른 사이트로 복제하거나 바깥 저장소로 한 번 더 보냅니다. 이 실습은 백업·복원의 절차를 확인하는 데까지입니다.
이 실습 환경에서 잰 값(2026-10-31). 반입 서버가 이미지 18개(1.8GB)를 미러하는 데 182초, k0sctl apply 는 93초에 「끝났다」 고 했지만 세 노드가 모두 등록되기까지는 2분이 더 걸렸습니다 — k0sctl 은 순수 워커만 Ready 를 기다리고 컨트롤러+워커는 기다리지 않습니다. VIP 를 가진 컨트롤러를 멈추자 VIP 가 다른 노드로 옮겨 가 API 와 쓰기가 3초 안에 돌아왔고, 둘째까지 멈추자 쓰기가 30초 내내 실패했습니다. Rook-Ceph 는 차트 설치부터 HEALTH_OK 까지 546초, CNPG 인스턴스 셋은 만든 지 약 14분 만에 모두 Ready 였고(가상 디스크가 느린 자리였습니다), primary 파드를 지우자 49.5초 만에 새 primary 가 쓰기를 받았습니다. 백업 뒤 넣은 행까지 WAL 로 복원되는 것도 확인했습니다 — 다만 넘긴 WAL 이 버킷에 올라가기 전에 복원을 시작하면 백업 시점까지만 돌아옵니다.
현장에서 만나는 모습
HA 를 세웠다고 끝이 아니라, 하나를 정말로 내려 봐야 압니다. 흔한 발견은 kubeconfig 가 VIP 가 아니라 첫 컨트롤러를 가리키고 있었다는 것, VIP 가 엉뚱한 NIC 에 떠 있었다는 것, 그리고 둘을 내리면 etcd 가 정족수를 잃어 API 가 쓰기를 거절한다는 사실을 장애가 나서야 아는 것입니다. 스토리지와 DB 도 같습니다 — primary 를 지웠을 때 몇 초 안에 새 primary 가 쓰기를 받는지, 백업이 실제로 복원되는지를 재 둔 숫자가 있어야 운영 약속을 할 수 있습니다.
다음 실습에서 할 것
k0sctl 로 세 노드를 HA 로 세우고 컨트롤러를 하나·둘 내려 재고, 미러만으로 Rook-Ceph 와 CNPG 를 올려 장애 조치와 백업·복원을 확인합니다.