--- type: runbook tags: [kubernetes, kvm, libvirt, control-plane, runbook] created: 2026-07-01 --- # 클러스터 콜드부팅 복구 — "no route to host" 전체 노드가 항상 cloud-init 장애는 아니다 > [!abstract] > [control-plane `/etc/hosts` 장애 런북](arch__runbook-controlplane-outage.html)과 증상이 닮았다 — > `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 장애](arch__runbook-controlplane-outage.html)와 **겉보기 증상이 완전히 같기 때문**이다(`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](arch__runbook-controlplane-outage.html)) | |---|---|---| | 증상 | `.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 -> 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 :6443: connect: no route to host ``` VIP뿐 아니라 **노드 3개 전부**(`ping -c1 -W2 `) no-route. [이전 장애](arch__runbook-controlplane-outage.html)의 "이름만 실패, 노드는 살아있음" 패턴과 다르다는 첫 단서. ### 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 확인 ```bash for vm in lb-haproxy k8s-master1 k8s-worker1 k8s-worker2; do virsh start "$vm"; done ``` 기동 직후 4개 IP에 `ping -c1 -W2`를 반복했지만 전부 무응답이 5분 가까이 지속됐다. 여기서 "부팅이 오래 걸리나" 의심하는 대신 §2.2의 판정 순서로 내려갔다: ```bash $ ip neigh get dev br0 dev br0 lladdr 52:54:00:f6:c5:d3 DELAY # <- MAC이 잡힘 = 게스트 살아있음 $ timeout 3 bash -c 'cat /dev/tcp//6443' && echo OPEN :6443 OPEN # <- 서비스 포트 응답 $ kubectl --context homelab get --raw=/healthz ok ``` ARP는 즉시 정상, TCP도 즉시 열려 있었다 — **ping만 계속 무응답**이었다(노드가 ICMP을 필터링하는 구성). 5분을 ping으로 허비할 필요가 없었다는 뜻. --- ## 4. 수리 절차 (재현 가능) ```bash # (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 dev br0 for hp in :6443 :22; do ip=${hp%:*}; p=${hp#*:} timeout 3 bash -c "cat /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 기동은 [위험 작업 정책](arch__runbook-controlplane-outage.html)상 사용자 승인 후 진행. 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` 이름해석이 살아있다는 뜻이기도 하다 — 즉 **[이전 장애](arch__runbook-controlplane-outage.html)의 영구수리(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`**: `Ready`나 `healthz: ok`만으로는 부족하고, control loop가 "지금" 갱신되고 있는지가 진짜 활성 증거다([이전 장애](arch__runbook-controlplane-outage.html) §2의 "살아있는 듯한 정지" 착시와 같은 원칙). ## What you might be missing - **영구수리가 재부팅을 "견뎠다"는 것이 곧 "다시는 안 터진다"는 뜻은 아니다.** cloud-init 템플릿 수정은 이번 콜드부팅 1회를 통과했을 뿐, 근본 해결(kubespray inventory에서 `manage_etc_hosts` 끄기)은 여전히 미완이다 — 다음 재부팅 후에도 반드시 lease부터 재확인할 것. - **"VM이 꺼진 이유" 자체는 이번에 추적하지 않았다.** 의도적 종료였는지, 호스트 재부팅에 VM autostart가 안 걸려 있었는지는 별도 확인이 필요하다 — `virsh dominfo | grep Autostart`로 다음에 점검할 만하다. - **감별 절차(전원→ARP→이름해석→lease)는 이 문서가 처음 정리한 것**이라, 다음에 비슷한 증상이 나오면 이 순서를 그대로 따르면 된다 — "ping이 안 되니 죽었다"는 결론으로 바로 뛰지 말 것. ## 참조 - [control-plane `/etc/hosts` 장애 런북](arch__runbook-controlplane-outage.html) — 겉보기 증상이 같은 이전 장애, 위험 작업 승인 정책도 여기 상속 - [GSLB DNS resolution 재현 랩](gw__guide-egress-dns-gslb-repro-lab.html) — 이 복구 직후 이어서 진행한 작업