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

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

노드 1대 — 폐쇄망에서 손으로 k0s --single

TT Lab 에서 이어서 보기

목표

반입 서버(bastion)와 봉인된 노드 한 대(node01)가 있습니다. 반입 서버가 인터넷 쪽에서 받아 둔 k0s 바이너리와 airgap 이미지 묶음만으로 node01 에 k0s install controller --single 클러스터를 세우고, 반입한 nginx 를 띄우고, 반입하지 않은 이미지는 오지 않는 것까지 확인합니다.

왜 중요한가

노드가 한 대면 컨트롤러와 워커가 한 몸이고 저장소도 etcd 대신 kine(SQLite)입니다. 엣지·데모·개발 장비에는 이것으로 충분하지만, 그 노드가 죽으면 클러스터 전체가 멈춥니다. 2·3대 실습에서 이 한계를 하나씩 풉니다. 여기서는 k0sctl 없이 손으로 합니다 — 무엇을 어디에 두면 설치가 되는지를 먼저 눈으로 보고, 다음 실습에서 k0sctl 이 그 일을 대신하는 것을 봅니다.

단계

  1. 반입 서버가 반입을 끝낼 때까지 기다리세요(/opt/airgap/INTAKE_DONE 이 생깁니다, 진행은 tail -f /var/log/airgap-intake.log). 그다음 bastion·node01 각각에서 curl -s -o /dev/null -m 5 -w '%{http_code}' https://<호스트> 로 github.com·quay.io·registry-1.docker.io 에 닿는지 재고, /root/k0s/egress.txt 에 노드 호스트 상태코드 를 한 줄씩 적으세요. 모두 000(연결 자체가 안 됨)이어야 합니다.
  2. /opt/airgap 에서 sha256sum -c SHA256SUMS 의 출력 전체를 /root/k0s/verify.txt 에 남기세요. 모든 줄이 OK 여야 합니다. k0s 바이너리와 airgap 묶음의 해시가 GitHub 릴리스의 sha256sums.txt 값과 같은지도 눈으로 확인하세요(sha256sum /opt/airgap/bin/k0s-v1.37.1+k0s.0-amd64).
  3. 반입한 k0s 바이너리를 node01 의 /usr/local/bin/k0s(755)로, airgap 묶음 k0s-airgap-bundle-v1.37.1+k0s.0-linux-amd64.tar 과 앱 묶음 apps-bundle.tar 를 node01 의 /var/lib/k0s/images/ 로 옮기세요(scp).
  4. node01 의 /etc/k0s/k0s.yaml 을 쓰세요. spec.api.address 는 세션망 주소 172.30.1.3, 파드 대역 172.20.0.0/16, 서비스 대역 172.21.0.0/16, CNI kuberouter, spec.images.default_pull_policy: Never, 텔레메트리 끔.
  5. node01 에서 k0s install controller --single -c /etc/k0s/k0s.yaml --kubelet-extra-args=--node-ip=172.30.1.3 로 서비스를 만들고 k0s start 하세요. 그다음 bastion 의 /root/.kube/config 에 node01 의 관리자 kubeconfig(k0s kubeconfig admin)를 두고, 노드 node01 이 Ready 가 될 때까지 걸린 초를 /root/k0s/install-time.txt 에 적으세요.
  6. kube-system 의 파드가 모두 Running·Ready 가 될 때까지 기다린 뒤, node01 의 containerd 가 가진 이미지 목록(k0s ctr -n k8s.io images ls -q)을 /root/k0s/images.txt 에 남기세요. 목록에 묶음에 든 quay.io/k0sproject/... 이미지와 앱 묶음의 nginx 가 보여야 합니다.
  7. 네임스페이스 demo 에 Deployment web(이미지 docker.io/library/nginx:1.30.6-alpine, imagePullPolicy: IfNotPresent, 2벌)과 NodePort Service web(30080)를 만들고, bastion 에서 curl http://172.30.1.3:30080/ 의 상태 코드를 /root/k0s/web.txt 에 적으세요(200).
  8. demo 에 파드 outside(이미지 docker.io/library/redis:8.2-alpine — 반입하지 않은 것)를 만들고, 당기기 실패(ErrImagePull/ImagePullBackOff)의 메시지를 /root/k0s/pull-fail.txt 에 남기세요. node01 의 봉인 규칙(nft list table inet labhub_airgap)의 reject 카운터가 0 보다 커야 합니다.

참고

정말 폐쇄망인가 — 두 대에서 재 본다

반입 서버가 반입을 끝낼 때까지 기다리세요(/opt/airgap/INTAKE_DONE 이 생깁니다, 진행은 tail -f /var/log/airgap-intake.log). 그다음 bastion·node01 각각에서 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 이 무엇을 남겼는지 보여 줍니다.

반입물을 해시로 대조한다

/opt/airgap 에서 sha256sum -c SHA256SUMS 의 출력 전체를 /root/k0s/verify.txt 에 남기세요. 모든 줄이 OK 여야 합니다. k0s 바이너리와 airgap 묶음의 해시가 GitHub 릴리스의 sha256sums.txt 값과 같은지도 눈으로 확인하세요(sha256sum /opt/airgap/bin/k0s-v1.37.1+k0s.0-amd64).

SHA256SUMS 는 받는 쪽(인터넷)에서 만든 목록이고, -c 는 그 목록대로 다시 계산해 맞는지 봅니다. 반입 서버는 받은 직후 공식 해시와 한 번 대조했습니다. 매체를 옮길 때마다 다시 대조하는 것이 폐쇄망 반입의 기본입니다.

바이너리와 묶음을 node01 에 옮긴다

반입한 k0s 바이너리를 node01 의 /usr/local/bin/k0s(755)로, airgap 묶음 k0s-airgap-bundle-v1.37.1+k0s.0-linux-amd64.tar 과 앱 묶음 apps-bundle.tar 를 node01 의 /var/lib/k0s/images/ 로 옮기세요(scp).

k0s 는 워커가 시작할 때 <데이터 디렉터리>/images/ 의 묶음을 containerd 로 들여옵니다. 기본 데이터 디렉터리는 /var/lib/k0s 입니다. 설치 스크립트(get.k0s.sh)는 인터넷에서 받으므로 폐쇄망에서는 쓰지 않습니다. 옮긴 뒤 node01 에서 다시 sha256sum 을 보면 깨지지 않았는지 압니다.

k0s.yaml — 대역·주소·당기지 않기

node01 의 /etc/k0s/k0s.yaml 을 쓰세요. spec.api.address 는 세션망 주소 172.30.1.3, 파드 대역 172.20.0.0/16, 서비스 대역 172.21.0.0/16, CNI kuberouter, spec.images.default_pull_policy: Never, 텔레메트리 끔.

첫 NIC(eth0)는 모든 VM 이 10.0.2.2 입니다. API 주소를 적지 않으면 k0s 가 그 주소로 인증서를 만들어 bastion 에서 접속할 수 없습니다. 기본 서비스 대역 10.96.0.0/12 는 이 VM 이 쓰는 DNS(10.96.0.10)를 덮어 클러스터가 뜨지 않습니다. default_pull_policy: Never 는 k0s 가 띄우는 시스템 파드가 레지스트리에 가지 않고 들여온 이미지만 쓰게 합니다 — 묶음이 빠졌으면 ErrImageNeverPull 로 바로 드러납니다. k0s config create 로 전체 모양을 볼 수 있습니다.

k0s install controller --single

node01 에서 k0s install controller --single -c /etc/k0s/k0s.yaml --kubelet-extra-args=--node-ip=172.30.1.3 로 서비스를 만들고 k0s start 하세요. 그다음 bastion 의 /root/.kube/config 에 node01 의 관리자 kubeconfig(k0s kubeconfig admin)를 두고, 노드 node01 이 Ready 가 될 때까지 걸린 초를 /root/k0s/install-time.txt 에 적으세요.

--single 은 컨트롤러와 워커를 한 프로세스에 담고 etcd 대신 kine(SQLite)을 씁니다. 노드 IP 를 적지 않으면 kubelet 이 eth0 의 10.0.2.2 로 등록합니다. kubeconfig 의 server 주소는 spec.api.address 를 따릅니다. bastion 의 kubectl 은 k0s 바이너리에 이름만 붙인 것입니다.

시스템 파드 — 이미지는 어디서 왔나

kube-system 의 파드가 모두 Running·Ready 가 될 때까지 기다린 뒤, node01 의 containerd 가 가진 이미지 목록(k0s ctr -n k8s.io images ls -q)을 /root/k0s/images.txt 에 남기세요. 목록에 묶음에 든 quay.io/k0sproject/... 이미지와 앱 묶음의 nginx 가 보여야 합니다.

k0s 의 containerd 는 k0s 가 띄운 것이라 k0s ctr 로 봅니다(ctr 를 따로 설치하지 않습니다). -n k8s.io 는 쿠버네티스가 쓰는 네임스페이스입니다. coredns 의 imagePullPolicy 가 Never 인지 보면 default_pull_policy 가 먹었는지 압니다.

반입한 nginx 를 띄우고 접속한다

네임스페이스 demo 에 Deployment web(이미지 docker.io/library/nginx:1.30.6-alpine, imagePullPolicy: IfNotPresent, 2벌)과 NodePort Service web(30080)를 만들고, bastion 에서 curl http://172.30.1.3:30080/ 의 상태 코드를 /root/k0s/web.txt 에 적으세요(200).

이미지는 이미 node01 의 containerd 에 들어와 있으므로 IfNotPresent 면 받으러 나가지 않습니다. Always 로 두면 태그를 확인하러 레지스트리에 가다가 실패합니다. NodePort 는 노드의 세션망 주소로 닿습니다.

반입하지 않은 이미지는 오지 않는다

demo 에 파드 outside(이미지 docker.io/library/redis:8.2-alpine — 반입하지 않은 것)를 만들고, 당기기 실패(ErrImagePull/ImagePullBackOff)의 메시지를 /root/k0s/pull-fail.txt 에 남기세요. node01 의 봉인 규칙(nft list table inet labhub_airgap)의 reject 카운터가 0 보다 커야 합니다.

같은 클러스터가 반입한 nginx 는 띄우고, 반입하지 않은 이미지는 레지스트리에 닿지 못해 실패합니다. 이 두 결과가 함께 있어야 「설치가 바깥에 기대지 않았다」 가 증명됩니다. 메시지는 kubectl -n demo get pod outside -o jsonpath='{.status.containerStatuses[0].state.waiting.message}' 로 봅니다.