🏠 목록 클러스터 콜드부팅 복구 — "no route to host" 전체 노드가 항상 cloud-init 장애는 아니다 📄 MD 원본 📁 Files 🔒 Private 🌓 테마
kuberneteskvmlibvirtcontrol-planerunbook

클러스터 콜드부팅 복구 — "no route to host" 전체 노드가 항상 cloud-init 장애는 아니다

NOTE

control-plane /etc/hosts 장애 런북과 증상이 닮았다 — kubectl이 전 노드에 no route to host. 하지만 원인은 완전히 다르다: 그 문서는 게스트 OS 안의 이름해석이 죽은 것이고, 이번은 노드를 담은 KVM VM 자체가 꺼져 있던 것이다. 이 문서는 두 장애를 30초 안에 가르는 감별법과, "ping이 안 되면 죽은 것"이라는 흔한 오해가 이 환경에서는 아예 거짓인 이유 (ICMP 필터링)를 다룬다. 결론 한 문장: 계층을 먼저 갈라라 — 전원(KVM) → 게스트 네트워크(ARP) → 이름해석(lease). ping은 이 감별에서 쓸모가 없다.

Date: 2026-07-01 호스트: homelab (kubespray bare-metal 3노드, 전부 로컬 KVM VM 위에서 구동) 도메인: Kubernetes / KVM/libvirt 상태: ✅ 복구 완료 — 3노드 Ready, lease 실시간 갱신, 전 네임스페이스 pod 정상, 기존 cloud-init 영구수리 유지 대상독자: 홈랩처럼 "k8s 노드 = 로컬 VM"인 환경에서, 재부팅/방치 후 클러스터가 안 잡힐 때 원인을 계층별로 빠르게 좁히려는 사람 선행개념: KVM/libvirt VM 상태(virsh), ARP(L2)와 ICMP(L3)의 차이, leader election lease


1. 배경 — 이 클러스터의 노드는 "컴퓨터"가 아니라 "이 서버 위의 VM"이다

이 홈랩 kubespray 클러스터의 3노드(control-plane 1 + worker 2)와 apiserver 앞단 로드밸런서는 전부 이 워크스테이션 한 대 위에 떠 있는 KVM VM이다 — 별도의 물리 머신이 아니다. 즉 "클러스터가 안 잡힌다"는 증상 앞에는 물리 전원 문제가 있을 수 없는 대신, 더 흔한 실패 모드가 하나 추가된다: VM 자체가 shut off 상태인 것. 이건 리눅스 게스트 안에서 무슨 일이 있었는지와 무관하게, 호스트의 libvirt 레벨에서 결정되는 훨씬 앞단의 계층이다.

이 관점이 중요한 이유는, 예전에 이 클러스터에서 겪은 control-plane 장애겉보기 증상이 완전히 같기 때문이다(kubectl이 전 노드에 접속 실패). 하지만 그 문서의 원인(cloud-init이 게스트 안의 /etc/hosts를 지움)과 이번 원인(VM이 꺼짐)은 서로 다른 계층에 있다. 증상만 보고 지난번과 같은 수리(/etc/hosts 편집)를 시도했다면 시간을 낭비했을 것이다 — 계층을 먼저 가르는 게 이 문서의 핵심이다.


2. 핵심 — 계층을 가르는 감별법 (메커니즘)

멘탈모델 앵커: "no route to host"가 VIP 하나만이면 이름해석/라우팅 문제(게스트 OS 계층)를 의심하고, 클러스터의 모든 IP(VIP+전 노드)가 동시에 no-route면 그보다 훨씬 앞단 — VM 전원(libvirt 계층)을 먼저 의심한다. 그리고 이 환경에서는 ping(ICMP)이 거짓 정보를 준다 — 이 노드들은 ICMP을 필터링하므로, VM이 멀쩡히 켜져 있어도 ping은 실패한다. 판정은 반드시 서비스 포트(TCP)로 한다.

2.1 두 장애를 가르는 표

이번(2026-07-01, 콜드부팅) 이전(2026-06-01 outage)
증상 .211(VIP)~.214(전 노드) 전부 no-route 노드는 응답, VIP 이름 해석만 실패
실패 계층 libvirt(VM 전원) 게스트 OS(/etc/hosts)
VM 자체 상태 shut off running(게스트 안에서 문제)
복구 virsh start /etc/hosts 편집 + cloud-init 템플릿

2.2 ping이 이 환경에서 쓸모없는 이유

VM을 켠 직후 5분 가까이 4개 IP 모두 ping에 무응답이라 "부팅이 안 되나" 의심했지만, 실제로는 ARP는 이미 정상 응답 중이었다(ip neigh get으로 MAC 주소가 정확히 잡힘 = L2/게스트 네트워크 스택은 살아있다는 뜻). 즉 ping(ICMP, L3)만 막혀 있었을 뿐 실제 서비스 포트(TCP, apiserver :6443/sshd :22)는 열려 있었다. ping 무응답을 "노드가 죽었다"로 오독하면 이미 복구된 클러스터를 붙잡고 계속 헤매게 된다 — 이게 이 문서에서 가장 시간을 아껴주는 교훈이다.

판정 순서:
  virsh list --all       -> VM 자체가 shut off 인가? (가장 앞단)
       |  running이면
       v
  ip neigh get <ip>      -> ARP 응답(MAC 잡힘) = 게스트 네트워크 스택 살아있음 (ping 말고 이걸로 판정)
       |
       v
  TCP :6443 / :22        -> 서비스 포트 자체 확인 (/dev/tcp 또는 timeout+bash)
       |
       v
  kubectl / lease        -> control-plane 실제 활성 여부 (renewTime ≈ now)

3. 증상과 추적 경로

3.1 1차 증상

$ kubectl --context homelab get nodes
Unable to connect to the server: dial tcp <VIP>:6443: connect: no route to host

VIP뿐 아니라 노드 3개 전부(ping -c1 -W2 <ip>) no-route. 이전 장애의 "이름만 실패, 노드는 살아있음" 패턴과 다르다는 첫 단서.

3.2 계층을 앞으로 — libvirt 확인

$ virsh list --all
 Id   Name          State
------------------------------
 -    k8s-master1   shut off
 -    k8s-worker1   shut off
 -    k8s-worker2   shut off
 -    lb-haproxy    shut off

4개 VM(3노드 + 로드밸런서) 전부 꺼져 있었다. 이게 진짜 원인 — 게스트 OS 안을 들여다볼 필요조차 없다.

3.3 기동 후에도 5분 무응답 — red herring 확인

for vm in lb-haproxy k8s-master1 k8s-worker1 k8s-worker2; do virsh start "$vm"; done

기동 직후 4개 IP에 ping -c1 -W2를 반복했지만 전부 무응답이 5분 가까이 지속됐다. 여기서 "부팅이 오래 걸리나" 의심하는 대신 §2.2의 판정 순서로 내려갔다:

$ ip neigh get <master1-ip> dev br0
<master1-ip> dev br0 lladdr 52:54:00:f6:c5:d3 DELAY     # <- MAC이 잡힘 = 게스트 살아있음

$ timeout 3 bash -c 'cat </dev/null >/dev/tcp/<vip>/6443' && echo OPEN
<vip>:6443  OPEN                                          # <- 서비스 포트 응답

$ kubectl --context homelab get --raw=/healthz
ok

ARP는 즉시 정상, TCP도 즉시 열려 있었다 — ping만 계속 무응답이었다(노드가 ICMP을 필터링하는 구성). 5분을 ping으로 허비할 필요가 없었다는 뜻.


4. 수리 절차 (재현 가능)

# (1) VM 상태 확인
virsh list --all

# (2) 기동 — 로드밸런서(VIP 제공)를 먼저
for vm in lb-haproxy k8s-master1 k8s-worker1 k8s-worker2; do virsh start "$vm"; done

# (3) 판정은 ping이 아니라 ARP + TCP로
ip neigh get <master-ip> dev br0
for hp in <vip>:6443 <master-ip>:22; do
  ip=${hp%:*}; p=${hp#*:}
  timeout 3 bash -c "cat </dev/null >/dev/tcp/$ip/$p" && echo "$hp OPEN"
done

# (4) 클러스터 헬스 — 핵심은 lease가 '지금'으로 갱신되는가
kubectl --context homelab get --raw=/healthz
kubectl --context homelab get nodes
date -u +%Y-%m-%dT%H:%M:%SZ
kubectl --context homelab -n kube-system get lease kube-controller-manager -o jsonpath='{.spec.renewTime}'
kubectl --context homelab -n kube-system get lease kube-scheduler          -o jsonpath='{.spec.renewTime}'
kubectl --context homelab get pods -A | grep -vE 'Running|Completed'   # 비정상 0이면 정상

⚠ VM 기동은 위험 작업 정책상 사용자 승인 후 진행. 4개 VM 전부를 대상으로 하는 이 조치는 클러스터 전체에 영향을 주므로 먼저 상태를 파악하고 승인을 받았다.


5. 검증 결과

healthz : ok
nodes   : 3노드 모두 Ready (k8s v1.30.6)
lease   : controller-manager/scheduler renewTime ≈ 현재 시각(지연 <1s) -> control loop 실제 활성
pods    : 전 네임스페이스 비정상 0. istio-system istiod/ingress/egress 1/1 (restart 1, 이번 부팅분)
istio   : istioctl 1.30.0 client/control/data plane 일치, 6 proxies
경로    : istioctl proxy-config / kubectl logs(kubelet webhook 인가) 정상 -> 이전 장애의 증상 없음
기존물  : 기존 실험 네임스페이스·리소스 무결

lease가 즉시 갱신된다는 것은 노드에서 lb-apiserver.kubernetes.local 이름해석이 살아있다는 뜻이기도 하다 — 즉 이전 장애의 영구수리(cloud-init 템플릿 항목)가 이번 콜드부팅을 견뎠다는 부수 확인이 됐다. 두 장애가 서로 다른 계층이라도, 복구 검증 명령(lease renewTime)은 똑같이 유효하다는 점이 흥미롭다.


핵심 정리

What you might be missing

참조