폐쇄망 k0s — 노드 1·2·3대와 그 위의 Ceph·PostgreSQL
노드 2대 — k0sctl 로 컨트롤러 1 + 워커, 그리고 넓히고 줄이기
목표
반입 서버(bastion)의 k0sctl 로 봉인된 노드 2대(ctrl01 컨트롤러 + worker01 워커)에 k0s 를 세웁니다. 컨트롤러가 멈췄을 때 워커의 파드가 어떻게 되는지 재고, worker02 를 더해 1+2 로 넓힌 뒤 worker01 을 비우고 빼고, 제어면을 백업합니다.
왜 중요한가
2대는 가장 흔한 「작은 운영」 구성이지만 고가용성이 아닙니다. etcd 는 과반이 있어야 쓰는데 2대 중 1대만 죽어도 과반이 깨지므로, 컨트롤러를 둘로 늘려도 나아지지 않습니다.
그래서 컨트롤러 1 + 워커 1 로 두고, 컨트롤러가 죽으면 무엇이 멈추고 무엇이 계속 도는지를 정확히 알아 두는 것이 운영의 시작입니다.
k0sctl 은 1실습에서 손으로 한 일(바이너리·묶음 옮기기, 설정, 설치, 조인)을 SSH 로 한 번에 합니다. 폐쇄망에서는 k0sBinaryPath 와 files 가 그 핵심입니다.
단계
- 반입 서버가 반입을 끝낼 때까지 기다리세요(
/opt/airgap/INTAKE_DONE이 생깁니다, 진행은tail -f /var/log/airgap-intake.log). 그다음 bastion·ctrl01·worker01·worker02 각각에서curl -s -o /dev/null -m 5 -w '%{http_code}' https://<호스트>로github.com·quay.io·registry-1.docker.io에 닿는지 재고,/root/k0s/egress.txt에노드 호스트 상태코드를 한 줄씩 적으세요. 모두000(연결 자체가 안 됨)이어야 합니다. /root/k0s/k0sctl.yaml에 2대 클러스터를 적으세요. ctrl01(172.30.1.3) 은controller, worker01(172.30.1.4) 은worker, k0s 판v1.37.1+k0s.0. 모든 노드에uploadBinary: true와k0sBinaryPath(반입한 바이너리),privateInterface: eth1·privateAddress, 워커에는files로 airgap 묶음과 앱 묶음을/var/lib/k0s/images/에,--kubelet-extra-args=--node-ip=<세션망 주소>를 적습니다. k0s 설정은 1실습과 같은 대역·kuberouter·default_pull_policy: Never·텔레메트리 끔에 더해spec.api.onlyBindToAddress: true입니다.k0sctl apply --config /root/k0s/k0sctl.yaml로 세우고, 출력은/root/k0s/apply-2.log, 걸린 초는/root/k0s/apply-2.time에 남기세요.k0sctl kubeconfig로 bastion 의/root/.kube/config를 만들고, 노드worker01이 Ready(InternalIP172.30.1.4)여야 합니다. 컨트롤러 전용 ctrl01 은 노드 목록에 없습니다.- 네임스페이스
demo에 Deploymentweb(docker.io/library/nginx:1.30.6-alpine, IfNotPresent, 2벌)과 NodePort Serviceweb(30080)를 만들고curl http://172.30.1.4:30080/이 200 인지 보세요. - ctrl01 의
k0scontroller를 멈추고 90초 동안 5초마다kubectl get nodes의 성공 여부와curl http://172.30.1.4:30080/의 상태 코드를/root/k0s/ctrl-down.txt에t=<초> kubectl=<ok|fail> web=<코드>로 적으세요. 20초쯤 worker01 의 nginx 마스터 프로세스 하나를 죽여(pkill -o -x nginx) 보고, 끝나면 컨트롤러를 다시 켜 API 가 돌아오기까지의 초를 마지막 줄recovered <초>로 적습니다. 전후의 재시작 횟수를/root/k0s/restarts-before.txt·/root/k0s/restarts-after.txt에 남기세요. /root/k0s/k0sctl.yaml에 worker02(172.30.1.5) 를 worker01 과 같은 모양으로 더하고 다시 apply 하세요. 출력은/root/k0s/apply-3.log, 걸린 초는/root/k0s/apply-3.time. worker01·worker02 둘 다 Ready 여야 합니다.- worker01 을
kubectl drain(데몬셋 무시, emptyDir 삭제)으로 비운 뒤, k0sctl.yaml 의 worker01 호스트에reset: true를 적고 apply 해 노드에서 k0s 를 지우고 클러스터에서 빼세요. 출력은/root/k0s/apply-shrink.log. 노드는 worker02 하나만 남고, demo/web 2벌은 worker02 에서 가용해야 합니다. k0sctl backup으로 제어면(etcd·인증서·키)을 백업해/root/k0s/backup/에k0s_backup_*.tar.gz를 남기세요.
참고
- 이 터미널은 bastion 입니다. 노드는
ssh ctrl01·ssh worker01·ssh worker02. 반입물은/opt/airgap, 작업 파일은/root/k0s. - 판: k0s
v1.37.1+k0s.0, k0sctlv0.33.1. worker02 는 6단계까지 비어 있는 봉인된 노드입니다. - k0sctl apply 는 수 분이 걸릴 수 있습니다. 세션은 60분에 시작하고 화면의 +시간으로 늘립니다. 세션이 끝나면 VM 넷과 작업물이 함께 사라집니다.
정말 폐쇄망인가 — 네 대에서 재 본다
반입 서버가 반입을 끝낼 때까지 기다리세요(/opt/airgap/INTAKE_DONE 이 생깁니다, 진행은 tail -f /var/log/airgap-intake.log). 그다음 bastion·ctrl01·worker01·worker02 각각에서 curl -s -o /dev/null -m 5 -w '%{http_code}' https://<호스트> 로 github.com·quay.io·registry-1.docker.io 에 닿는지 재고, /root/k0s/egress.txt 에 노드 호스트 상태코드 를 한 줄씩 적으세요. 모두 000(연결 자체가 안 됨)이어야 합니다.
폐쇄망 일의 첫 질문은 「정말 막혔나」 입니다. 막혔다고 믿고 설치했는데 사실은 바깥에서 받아 온 것이면 진짜 폐쇄망에서 같은 절차가 실패합니다. 다른 노드에는 ssh <노드> '<명령>' 으로 들어갑니다(세션망 열쇠가 이미 깔려 있습니다). 이 망은 이름 풀이는 되지만 연결은 거절됩니다 — nft list table inet labhub_airgap 이 무엇을 남겼는지 보여 줍니다.
k0sctl.yaml — 2대를 반입물로만
/root/k0s/k0sctl.yaml 에 2대 클러스터를 적으세요. ctrl01(172.30.1.3) 은 controller, worker01(172.30.1.4) 은 worker, k0s 판 v1.37.1+k0s.0. 모든 노드에 uploadBinary: true 와 k0sBinaryPath(반입한 바이너리), privateInterface: eth1·privateAddress, 워커에는 files 로 airgap 묶음과 앱 묶음을 /var/lib/k0s/images/ 에, --kubelet-extra-args=--node-ip=<세션망 주소> 를 적습니다. k0s 설정은 1실습과 같은 대역·kuberouter·default_pull_policy: Never·텔레메트리 끔에 더해 spec.api.onlyBindToAddress: true 입니다.
uploadBinary 만 켜면 k0sctl 이 bastion 에서 바이너리를 받으러 갑니다. k0sBinaryPath 로 반입한 파일을 가리켜야 폐쇄망에서 됩니다. k0sctl 은 기본 경로의 NIC 를 사설 인터페이스로 고르므로, 이 VM 들처럼 eth0 이 모두 같은 주소면 privateInterface 를 적어야 etcd·API 주소가 맞습니다. 그런데 privateAddress 는 etcd 피어 주소와 kubelet 의 --node-ip 에만 쓰이고 API 주소는 k0s 가 다시 기본 경로 NIC 에서 고릅니다 — 그러면 조인 토큰이 10.0.2.2 를 가리켜 워커가 붙지 못합니다. onlyBindToAddress: true 를 주면 k0sctl 이 노드마다 API 주소를 privateAddress 로 채웁니다(다중 NIC 컨트롤러에 대한 k0s 문서의 권고). 공식 문서는 묶음을 워커에만 두라고 합니다 — 컨트롤러 전용 노드는 kubelet 이 없어 이미지를 쓰지 않습니다. k0sctl init --k0s 가 뼈대를 줍니다.
k0sctl apply — 2대가 서는 시간
k0sctl apply --config /root/k0s/k0sctl.yaml 로 세우고, 출력은 /root/k0s/apply-2.log, 걸린 초는 /root/k0s/apply-2.time 에 남기세요. k0sctl kubeconfig 로 bastion 의 /root/.kube/config 를 만들고, 노드 worker01 이 Ready(InternalIP 172.30.1.4)여야 합니다. 컨트롤러 전용 ctrl01 은 노드 목록에 없습니다.
k0sctl 은 SSH 로 노드마다 바이너리를 올리고(Upload k0s binaries), files 를 올리고, 컨트롤러를 먼저 세운 뒤 조인 토큰으로 워커를 붙입니다. 이 판(v0.33.1)의 k0sctl 에는 예전의 --disable-telemetry·--disable-upgrade-check 깃발이 없습니다(주면 깃발 오류로 바로 끝납니다). 오래 걸리면 systemd-run 으로 뒤에서 돌리고 로그를 보세요. controller 역할에는 kubelet 이 없어서 kubectl get nodes 에 보이지 않습니다 — 고장이 아닙니다.
워커에 nginx 두 벌
네임스페이스 demo 에 Deployment web(docker.io/library/nginx:1.30.6-alpine, IfNotPresent, 2벌)과 NodePort Service web(30080)를 만들고 curl http://172.30.1.4:30080/ 이 200 인지 보세요.
이미지는 k0sctl 이 올린 앱 묶음에서 worker01 의 containerd 로 들어와 있습니다. 1실습의 7단계와 같은 매니페스트입니다.
컨트롤러가 죽으면 워커의 파드는?
ctrl01 의 k0scontroller 를 멈추고 90초 동안 5초마다 kubectl get nodes 의 성공 여부와 curl http://172.30.1.4:30080/ 의 상태 코드를 /root/k0s/ctrl-down.txt 에 t=<초> kubectl=<ok|fail> web=<코드> 로 적으세요. 20초쯤 worker01 의 nginx 마스터 프로세스 하나를 죽여(pkill -o -x nginx) 보고, 끝나면 컨트롤러를 다시 켜 API 가 돌아오기까지의 초를 마지막 줄 recovered <초> 로 적습니다. 전후의 재시작 횟수를 /root/k0s/restarts-before.txt·/root/k0s/restarts-after.txt 에 남기세요.
컨트롤러는 「바뀌는 것」 을 다룹니다. 이미 떠 있는 파드는 노드의 kubelet·containerd·kube-proxy 규칙이 지키므로 API 가 없어도 응답합니다. 컨테이너가 죽으면 kubelet 이 API 없이 혼자 되살립니다(재시작 횟수는 API 가 돌아온 뒤에 보고됩니다). 새로 띄우기·옮기기·스케일은 API 가 돌아올 때까지 멈춥니다. 그래서 2대 구성의 진짜 위험은 「서비스가 멈춘다」 가 아니라 「그동안 아무것도 바꿀 수 없고, 워커가 죽으면 다른 곳에 다시 띄워 줄 주체가 없다」 입니다.
worker02 를 더한다 — 1+2
/root/k0s/k0sctl.yaml 에 worker02(172.30.1.5) 를 worker01 과 같은 모양으로 더하고 다시 apply 하세요. 출력은 /root/k0s/apply-3.log, 걸린 초는 /root/k0s/apply-3.time. worker01·worker02 둘 다 Ready 여야 합니다.
k0sctl apply 는 선언형입니다. 이미 있는 노드는 건드리지 않고, 목록에 새로 생긴 호스트만 바이너리·묶음을 올려 조인시킵니다. 같은 파일을 다시 apply 해도 아무 일도 일어나지 않아야 정상입니다.
worker01 을 비우고 뺀다
worker01 을 kubectl drain(데몬셋 무시, emptyDir 삭제)으로 비운 뒤, k0sctl.yaml 의 worker01 호스트에 reset: true 를 적고 apply 해 노드에서 k0s 를 지우고 클러스터에서 빼세요. 출력은 /root/k0s/apply-shrink.log. 노드는 worker02 하나만 남고, demo/web 2벌은 worker02 에서 가용해야 합니다.
drain 은 파드를 다른 노드로 옮기는 일입니다(옮길 곳이 있어야 합니다 — 그래서 6단계를 먼저 했습니다). reset: true 는 k0sctl 이 그 호스트의 k0s 를 지우고 노드 객체도 지웁니다. 끝나면 그 호스트 줄을 파일에서 지워 두는 것이 다음 apply 의 실수를 막습니다.
k0sctl backup — 제어면을 파일 하나로
k0sctl backup 으로 제어면(etcd·인증서·키)을 백업해 /root/k0s/backup/ 에 k0s_backup_*.tar.gz 를 남기세요.
백업은 컨트롤러에서 k0s backup 을 돌려 bastion 으로 가져옵니다. 안에는 etcd 스냅숏과 PKI 가 들어 있습니다 — 키가 들어 있으니 운영에서는 백업 파일을 비밀처럼 다룹니다. 복원은 k0sctl apply --restore-from <파일> 로 빈 컨트롤러에 합니다(이 실습에서는 하지 않습니다).