homelab89 Docs Logs Legacy Files ☰ TOC 🌓
runbookistio 2026-07-01kuberneteskvmlibvirtcontrol-plane

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

ABSTRACT

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 인가) 정상 -> 이전 장애의 증상 없음
기존물  : 기존 실험 네임스페이스·리소스 무결
버전 호환성 참고 (2026-07-05 갱신)

위 “정상” 판정은 클러스터가 실제로 동작했다는 뜻이지, 이 버전 조합이 Istio 공식 지원 대상이라는 뜻은 아니다. Istio 1.30부터 공식 지원 Kubernetes 범위가 1.32~1.36으로 올라가면서 최소 지원 버전도 1.32.x로 상향됐다 — 여기서 쓰는 Kubernetes 1.30.6은 Istio 1.30.0 입장에서 “테스트는 했으나 공식 지원 대상 아님(tested but not officially supported)” 등급이다. 이 콜드부팅 복구 자체의 결론(전원→ARP→TCP→lease 감별 순서)에는 영향이 없지만, 이 조합을 프로덕션에 그대로 옮기기 전에는 Kubernetes를 1.32 이상으로 올리거나 비공식 지원 범위임을 명시해야 한다.

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


핵심 정리

  • 증상(전 노드 no-route)이 같아도 원인 계층은 다를 수 있다: VM 전원(libvirt) vs 게스트 OS 이름해석. 감별은 virsh list --all이 가장 먼저다 — 게스트 안을 보기 전에 “VM이 켜져 있긴 한가"부터 확인.
  • ping은 이 환경에서 판정 도구로 쓰지 않는다: ICMP 필터링 때문에 살아있는 노드도 ping엔 응답하지 않는다. ARP(ip neigh get)로 L2 생존을, TCP(/dev/tcp)로 서비스 포트를 확인한다.
  • 최종 판정은 항상 lease renewTime: Readyhealthz: ok만으로는 부족하고, control loop가 “지금” 갱신되고 있는지가 진짜 활성 증거다(이전 장애 §2의 “살아있는 듯한 정지” 착시와 같은 원칙).

What you might be missing

  • 영구수리가 재부팅을 “견뎠다"는 것이 곧 “다시는 안 터진다"는 뜻은 아니다. cloud-init 템플릿 수정은 이번 콜드부팅 1회를 통과했을 뿐, 근본 해결(kubespray inventory에서 manage_etc_hosts 끄기)은 여전히 미완이다 — 다음 재부팅 후에도 반드시 lease부터 재확인할 것.
  • “VM이 꺼진 이유” 자체는 이번에 추적하지 않았다. 의도적 종료였는지, 호스트 재부팅에 VM autostart가 안 걸려 있었는지는 별도 확인이 필요하다 — virsh dominfo <vm> | grep Autostart로 다음에 점검할 만하다.
  • 감별 절차(전원→ARP→이름해석→lease)는 이 문서가 처음 정리한 것이라, 다음에 비슷한 증상이 나오면 이 순서를 그대로 따르면 된다 — “ping이 안 되니 죽었다"는 결론으로 바로 뛰지 말 것.

참조

검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)

검증 방법: 공식 문서 대조 + homelab 클러스터 실측(istio-verify 네임스페이스 및 kube-system Lease 직접 관측).

주장 판정 근거
C1. 모든 IP 동시 no-route → libvirt(전원) 계층부터 감별(virsh list --all 최우선) 실측 불가 홈랩 특유 토폴로지 휴리스틱 — libvirt/K8s/Istio 어느 공식 문서에도 이 규칙이 명문화돼 있지 않고, 실측(VM shutdown/start 반복)은 실제 클러스터 노드를 정지시키는 위험 작업이라 이번 검증 범위에서는 실행하지 않음
C2. ICMP 필터링 환경에서 ping 무응답은 장애를 의미하지 않는다 — ARP+TCP로 판정 ✅ 실측 확인 docs.redhat.com/…/sec-managing_icmp_requests, T41 실측
C3. control-plane 활성 최종 판정 기준은 Ready/healthz가 아니라 Lease renewTime 갱신 ✅ 실측 확인 kubernetes.io/docs/concepts/architecture/leases/, T42 실측
C4. ip neigh get MAC 응답 = ARP(L2) 정상, ping(L3) 무관하게 게스트 네트워크 스택 생존 ✅ 문헌 확인 rfc-editor.org/rfc/rfc826
C5. istioctl version은 client/control-plane/data-plane 버전을 구분 표시(버전 스큐 확인용) ✅ 문헌 확인 istio.io/latest/docs/reference/commands/istioctl/
C6. istioctl proxy-config로 Envoy 실 설정과 istiod 송신 설정 간 동기화 점검 가능 ✅ 문헌 확인 istio.io/latest/docs/ops/diagnostic-tools/proxy-cmd/
C7. apiserver 기본 서비스 포트는 TCP :6443 ✅ 문헌 확인 kubernetes.io/docs/reference/networking/ports-and-protocols/
C8. “Kubernetes 1.30.6 + Istio 1.30.0"을 정상/유효 조합으로 제시 ❌ 오류 — 본문 교정 istio.io/latest/docs/releases/supported-releases/ (Istio 1.30 공식 지원 범위는 k8s 1.32~1.36; 1.30.6은 테스트만 됐을 뿐 공식 미지원 등급 — §5에 경고 콜아웃 추가)

Files