TT Lab
블로그 시작하기
배우기 러닝패스 코스

폐쇄망 k0s — 노드 1·2·3대와 그 위의 Ceph·PostgreSQL

노드 2대 — 왜 컨트롤러 하나인가

TT Lab 에서 이어서 보기

한 줄 요약

노드 2대는 「컨트롤러 1 + 워커 1」 이 정답에 가깝습니다. 2대로는 etcd 정족수 HA 가 안 되고, 컨트롤러가 죽어도 이미 떠 있는 파드는 계속 돕니다 — 멈추는 것은 「바꾸는 일」 입니다.

왜 이게 필요했나

노드가 둘이면 둘 다 컨트롤러로 만들어 「이중화」 하고 싶어집니다. 그런데 etcd 는 다수결로 씁니다. 멤버가 2면 과반은 2라서 하나만 죽어도 쓰기가 멈춥니다. 멤버가 1일 때와 견디는 장애 수가 같고(0), 오히려 멤버끼리의 네트워크가 끊기는 경우가 하나 더 생깁니다. etcd 문서가 3·5 같은 홀수를 권하는 이유입니다. k0s 문서도 2대 제어면을 「API 는 둘로 나뉘지만 etcd 관점에서는 HA 가 아니다」 라고 적습니다.

어떻게 동작하나

k0sctl 의 길. k0sctl 은 bastion 에서 SSH 로 각 노드에 들어가 운영체제를 확인하고, 바이너리를 올리고(uploadBinary + k0sBinaryPath), files 를 올리고, 첫 컨트롤러를 세운 뒤 조인 토큰을 만들어 나머지를 붙입니다. 폐쇄망에서 중요한 것은 k0sBinaryPath 입니다 — 이것 없이 uploadBinary 만 켜면 k0sctl 이 bastion 에서 GitHub 으로 바이너리를 받으러 갑니다. 같은 이유로 업그레이드 확인과 텔레메트리를 깃발로 끕니다.

사설 인터페이스. k0sctl 은 기본 경로의 NIC 를 사설 인터페이스로 골라 etcd 피어 주소와 API 주소를 정합니다. 관리망과 서비스망이 나뉜 서버, 그리고 이 실습의 VM 처럼 첫 NIC 가 모두 같은 주소인 환경에서는 privateInterface·privateAddress 를 적어야 합니다.

컨트롤러가 죽으면. 이미 떠 있는 파드는 노드의 kubelet·containerd 와 kube-proxy 가 깔아 둔 규칙이 지킵니다. 그래서 서비스는 계속 응답합니다. 컨테이너가 죽어도 kubelet 이 API 없이 혼자 되살립니다. 멈추는 것은 새로 띄우기·옮기기·스케일·설정 변경, 그리고 노드가 죽었을 때 다른 노드로 파드를 옮겨 주는 일입니다. 즉 2대 구성은 「잠깐의 컨트롤러 장애」 에는 강하고 「워커 장애」 와 「긴 컨트롤러 장애」 에는 약합니다.

넓히기와 줄이기. k0sctl apply 는 선언형이라 호스트를 목록에 더하면 그 호스트만 조인시킵니다. 뺄 때는 먼저 drain 으로 파드를 옮기고, 호스트에 reset: true 를 적고 apply 하면 k0s 를 지우고 노드 객체도 지웁니다.

정족수 계산. 멤버 N 의 과반은 N/2 를 넘는 가장 작은 정수이고, 견디는 장애 수는 N 에서 과반을 뺀 값입니다. 1→0, 2→0, 3→1, 4→1, 5→2. 짝수를 더해도 견디는 수가 늘지 않으므로 늘릴 때는 홀수로 늘립니다. 그래서 「2대」 라는 조건에서 고를 수 있는 것은 사실상 컨트롤러 1 + 워커 1 하나이고, 남은 질문은 「컨트롤러가 워커를 겸할 것인가」 입니다. 겸하면 노드 하나를 더 쓰지만 워크로드의 메모리 압박이 API·etcd 를 흔들고, 그 노드를 점검하려고 내리면 제어면과 워크로드를 함께 잃습니다. 2대에서는 겸하지 않고 컨트롤러를 작게 두는 편을 권합니다.

폐쇄망에서 k0sctl 이 바깥을 찾는 자리. 바이너리 받기(uploadBinary 만 켜고 경로를 안 줄 때)와 files 의 src 가 URL 일 때 — 둘입니다. 둘 다 k0sBinaryPath 와 로컬 경로로 막습니다. 예전 판에 있던 업그레이드 확인·텔레메트리 깃발은 이 판(v0.33.1)에 없고, 반입 서버의 봉인 기록으로 보면 apply·backup 이 바깥으로 연 TCP 연결은 0건이었습니다. 노드 쪽에서는 k0s 가 묶음을 못 찾았을 때 레지스트리로 가는 것이 유일한 바깥 길이고, default_pull_policy: Never 가 그것을 막습니다.

이 실습 환경에서 잰 값(2026-10-31). k0sctl 로 2대를 세우는 데 약 2분(k0sctl 보고 1분58초), worker02 를 더하는 데 1분42초, worker01 을 비우고 빼는 데 24초, 백업에 16초(416KB)가 걸렸습니다. 컨트롤러를 90초 멈춘 동안 kubectl 은 18번 모두 실패했고 워커의 nginx 는 18번 모두 200 으로 응답했습니다. 그 사이 nginx 마스터 프로세스를 죽이자 kubelet 이 컨테이너를 혼자 되살렸고(재시작 횟수 0→1 은 API 가 돌아온 뒤에야 보였습니다), 컨트롤러를 다시 켜고 4초 만에 API 가 돌아왔습니다. 그리고 처음에는 privateAddress 만 적고 설치했다가 워커가 10.0.2.2:6443 으로 조인하려다 실패했습니다 — 다음 실습에서 onlyBindToAddress 가 왜 필요한지 직접 봅니다.

현장에서 만나는 모습

공장·지점의 작은 클러스터가 2대로 시작하는 일이 많습니다. 흔한 사고는 「컨트롤러 겸 워커 두 대」 로 만든 뒤 한 대를 점검하려고 내렸더니 etcd 가 과반을 잃어 다른 한 대의 API 까지 멈추는 것입니다. 그때 파드는 계속 돌기 때문에 처음에는 문제를 모르다가, 배포가 안 된다는 연락으로 알게 됩니다. 컨트롤러를 하나로 두고 백업(k0sctl backup)을 정기적으로 받아 두는 편이 단순하고 복구도 빠릅니다.

다음 실습에서 할 것

k0sctl.yaml 을 쓰고 2대를 세운 뒤 컨트롤러를 멈춰 워커의 파드를 재고, 워커를 더하고 빼고, 제어면을 백업합니다.