1. 현상 (Issue)

  • Windows(VM 호스트)가 알 수 없는 이유로 블루스크린으로 강제 종료되어 Vmware로 띄운 kubernetes cluster 가 강제 shutdown 되었다.
  • kubernetes cluster 내 노드들을 기동해도 컨트롤 플래인이 계속 비정상 동작함.

2. 원인 (Root Cause)

분석과정

  1. etcd 컨테이너 id를 확인한다.
# containerd 가 관리하는 컨테이너 목록에서 etcd의 id를 확인해 조회한다.
ETCD_ID=$(sudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps -a --name etcd -q | head -1) 
sudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock logs "$ETCD_ID" | tail -200
  1. 로그를 확인한다.

panic: freepages: failed to get all reachable pages (page 1209: multiple references (stack: [5946 6314 6313 1209]))

0403-k8s-cluster-error
### 결론 - 비정상 종료로 인해 etcd가 쓰는 backend DB 손상으로 etcd 데이터 파일이 깨짐 (`/var/lib/etcd`의 기존 member 데이터를 읽다가 panic) - kubeadm은 local etcd를 static Pod 로 띄우지만 etcd가 계속 죽고 → kube-apiserver도 같이 못 뜨는 상태가 됨

추가 기록

# kubectl -> Kubernetes 리소스 보기, 평소 운영용, API 서버 살아 있어야 함
# crictl -> 컨테이너 런타임에서 pod/container 직접 보기, API 서버 죽었을 때 매우 중요, kubeadm static pod 장애 확인에 핵심
# ctr -> containerd 내부를 더 낮은 수준에서 보기, 심화 디버깅용, 일반적인 k8s 장애 확인은 보통 crictl이 더 실용적
# journalctl -> systemd 서비스 로그 보기, kubelet, containerd 서비스 로그 확인용, “누가 무엇을 못 하고 있는가” 확인하는 데 중요

3. 해결 방법(Solution)

1) etcd snapshot 백업이 있는 경우

공식 절차는 API 서버를 먼저 멈추고 → etcd를 복원하고 → API 서버를 다시 올리는 순서이다.

# 0. 복구용 디렉터리
sudo mkdir -p /root/recovery
 
# 1. kube-apiserver 정지
sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /root/recovery/
sleep 20
sudo crictl ps | grep kube-apiserver
 
# 2. 새 data-dir로 restore
sudo rm -rf /var/lib/etcd-restored
sudo etcdutl snapshot restore /backup/etcd-snapshot.db --data-dir /var/lib/etcd-restored
 
# 3. etcd manifest 백업 (반드시 manifests 바깥)
sudo cp /etc/kubernetes/manifests/etcd.yaml /root/recovery/etcd.yaml.bak
 
# 4. /etc/kubernetes/manifests/etcd.yaml 수정
#    - --data-dir=/var/lib/etcd-restored
#    - volumes.hostPath.path: /var/lib/etcd-restored
sudo vi /etc/kubernetes/manifests/etcd.yaml
 
# 5. kubelet 재시작
sudo systemctl restart kubelet
 
# 6. etcd health 확인
ETCDCTL_API=3 sudo etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
  endpoint health
 
# 7. kube-apiserver 다시 올리기
sudo mv /root/recovery/kube-apiserver.yaml /etc/kubernetes/manifests/
sudo systemctl restart kubelet

2) etcd snapshot 백업이 없는 경우 재구축

Snapshot 백업이 없다면 복구보다 ‘재구축’이 더 현실적인 방법이다. 이 경우는 솔직히 말해 현재 cluster state를 안전하게 되살릴 보장된 방법이 없기 때문이다. 단일 control plane에서 local etcd가 깨졌다면, 중요 워크로드가 아니라면 master 재구축 후 worker 재조인이 가장 빠른 경우가 많다고 가이드하고 있다. 이 경우 kubeadm reset 후 kubeadm 을 통해 다시 복구를 진행한다. 핵심 단계는 다음와 같다.

원본 보존 → control plane 재초기화 → CNI 재적용 → worker 재조인

# 1. 원본보존
(master)
sudo systemctl stop kubelet
 
TS=$(date +%F-%H%M)
sudo mkdir -p /root/recovery-$TS
 
sudo cp -a /etc/kubernetes /root/recovery-$TS/
sudo cp -a /var/lib/etcd /root/recovery-$TS/
sudo cp -a /etc/cni /root/recovery-$TS/ 2>/dev/null || true
 
(worker)
TS=$(date +%F-%H%M)
sudo mkdir -p /root/recovery-$TS
sudo cp -a /etc/cni /root/recovery-$TS/ 2>/dev/null || true
sudo cp -a /var/lib/kubelet /root/recovery-$TS/ 2>/dev/null || true
 
# 2. master,worker 재초기화
(master, worker)
sudo kubeadm reset -f
sudo rm -rf /etc/cni/net.d  # 이전 CNI 설정 정리
 
sudo iptables -F
sudo iptables -t nat -F
sudo iptables -t mangle -F
sudo iptables -X
sudo ipvsadm -C
 
# 3. master에서 새 control plane 초기화
sudo kubeadm init \  
--apiserver-advertise-address=172.30.1.37 \  
--pod-network-cidr=192.168.0.0/16  
  
mkdir -p $HOME/.kube  
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config  
sudo chown $(id -u):$(id -g) $HOME/.kube/config
 
# 3. CNI 재적용
kubectl apply -f <네가 쓰던 Calico manifest 또는 Helm>  
kubectl get pods -A
 
# 4. worker 재조인
(master)
sudo kubeadm token create --print-join-command
(worker)
sudo kubeadm join <master-ip>:6443 --token ... --discovery-token-ca-cert-hash sha256:...

4. 재발 방지 및 개선

이런 뭣같은 상황을 방지하려면 어떻게 해야 할까? 크게 아키텍처 개선 방량, snapshot 자동화, 그리고 부수적이지만 control plane의 자원 보호가 있다.

1. 아키텍처 개선

공식 문서도 single-node etcd는 테스트용이라고 보고, etcd는 홀수 개 멤버, 가능하면 멀티노드, 그리고 전용 또는 격리된 환경에서 운영하라고 권장하고 있다. (물론 집에서 가지고 노는 나는 적용하기 힘든 구조이다.) 또한 리소스 부족이나 디스크 I/O 경쟁만으로도 heartbeat timeout이 나고, 그 상태면 etcd leader가 불안정해져 Kubernetes가 변경을 못 받는다. 지금처럼 모든 VM이 한 대의 Windows 호스트에 매달린 구조는 host failure 한 번에 전부 영향받는다.

kubeadm은 기본적으로 local etcd를 쓰지만, external etcd 토폴로지도 공식 지원한다.

2. etcd snapshot을 자동화

etcd 안에는 Kubernetes 오브젝트 상태가 모두 들어 있으므로, 공식 문서도 주기적 백업을 강하게 권장한다. snapshot은 live member에서 뜰 수 있고, etcdutl snapshot status로 검증할 수 있다.

다음과 같이 적용한다.

  • 주기적 snapshot: 예를 들면 30분~1시간 간격
  • 보관 위치 분리: master VM 내부가 아니라 다른 VM / NAS / 다른 물리 장비

백업 예시는 아래 형태이다.

ETCDCTL_API=3 sudo etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
  snapshot save /backup/etcd/etcd-$(date +%F-%H%M).db
 
sudo etcdutl --write-out=table snapshot status /backup/etcd/etcd-YYYY-MM-DD-HHMM.db

3. control plane 자원 보호

Kubernetes는 system daemons와 pod가 자원을 서로 갉아먹지 않도록 Node Allocatable, kubeReserved, systemReserved 설정을 권장한다. 특히 한 대의 master VM에 여러 부하가 몰리면 kubelet, container runtime, etcd가 같이 흔들릴 수 있어 master VM 에는 하기 작업을 권장한다.

  • 일반 워크로드 스케줄링 억제
  • CPU/메모리 여유 확보
  • 디스크 여유 및 I/O 병목 완화
  • swap 비활성, 디스크 fullness 감시

+ (2026.06.13)

host pc 가 또 문제가 생겨 k8s node 들이 다시 죽는 이슈가 발생했다. 이번에는 etcd 백업본을 통해 간단하게 k8s cluster 를 복구할 수 있었다.

sudo ./restore-etcd.sh /backup/etcd/etcd-snapshot-20260516-151807-metallb.db
0523-k8s-cluster-error 0523-k8s-cluster-error2

참고로 복구 스크립트 원문 (restore-etcd.sh) 은 아래와 같다. 👋

#!/usr/bin/env bash
set -euo pipefail
 
# ==============================
# etcd snapshot restore script
# kubeadm single master / stacked etcd 기준
# ==============================
 
# 복구에 사용할 etcd snapshot 파일 경로
SNAP="${1:-/tmp/etcd-snapshot.db}"
 
# master 노드 이름
ETCD_NAME="${ETCD_NAME:-k8s-master}"
 
# master IP
MASTER_IP="${MASTER_IP:-172.30.1.37}"
 
# root 권한 확인
if [ "$(id -u)" -ne 0 ]; then
  echo "[ERROR] root 권한으로 실행해야 합니다. 예: sudo ./restore-etcd.sh /tmp/etcd-snapshot.db"
  exit 1
fi
 
# snapshot 파일 존재 확인
if [ ! -f "$SNAP" ]; then
  echo "[ERROR] snapshot 파일이 없습니다: $SNAP"
  exit 1
fi
 
# etcdutl 존재 확인
if ! command -v etcdutl >/dev/null 2>&1; then
  echo "[ERROR] etcdutl 명령어가 없습니다. etcdutl 설치 후 다시 실행하세요."
  exit 1
fi
 
echo "[1/9] kubelet static pod 임시 중지 디렉터리 생성"
mkdir -p /etc/kubernetes/manifests.off
 
echo "[2/9] kube-apiserver static pod 중지"
if [ -f /etc/kubernetes/manifests/kube-apiserver.yaml ]; then
  mv /etc/kubernetes/manifests/kube-apiserver.yaml /etc/kubernetes/manifests.off/
fi
 
echo "[3/9] etcd static pod 중지"
if [ -f /etc/kubernetes/manifests/etcd.yaml ]; then
  mv /etc/kubernetes/manifests/etcd.yaml /etc/kubernetes/manifests.off/
fi
 
echo "[4/9] kubelet 재시작으로 static pod 중지 반영"
systemctl restart kubelet
sleep 20
 
echo "[5/9] 기존 깨진 etcd 데이터 디렉터리 제거"
rm -rf /var/lib/etcd
 
echo "[6/9] snapshot으로 etcd 데이터 복원"
etcdutl snapshot restore "$SNAP" \
  --data-dir /var/lib/etcd \
  --name "$ETCD_NAME" \
  --initial-cluster "${ETCD_NAME}=https://${MASTER_IP}:2380" \
  --initial-advertise-peer-urls "https://${MASTER_IP}:2380"
 
echo "[7/9] etcd static pod 재기동"
mv /etc/kubernetes/manifests.off/etcd.yaml /etc/kubernetes/manifests/
systemctl restart kubelet
sleep 30
 
echo "[8/9] kube-apiserver static pod 재기동"
mv /etc/kubernetes/manifests.off/kube-apiserver.yaml /etc/kubernetes/manifests/
systemctl restart kubelet
sleep 30
 
echo "[9/9] kubectl 확인"
export KUBECONFIG=/etc/kubernetes/admin.conf
 
kubectl get nodes -o wide
kubectl get pods -A -o wide
 
echo "[DONE] etcd snapshot restore 완료"