쿠버네티스 전문가 — 장애를 진단하고 클러스터를 지킨다
컨트롤 플레인과 etcd — 스냅숏·복원·정적 파드·인증서·용량
목표
kubeadm 클러스터의 etcd 스냅숏을 뜨고, 지워진 네임스페이스를 그 스냅숏으로 같은 객체 그대로 되살립니다. 이어서 API 서버·스케줄러가 정적 파드 매니페스트 한 줄 때문에 멈춘 것을 kubectl 없이 진단하고,
인증서를 갱신한 뒤 갱신이 실제로 쓰이게 하고, etcd 가 상한에 닿아 클러스터 전체의 쓰기가 멈춘 것을 되살립니다.
왜 중요한가
컨트롤 플레인의 장애는 「모든 것이 멈춘」 모습으로 옵니다. 그런데 그 원인은 대개 파일 한 줄(매니페스트·인증서·플래그)이고, 그 순간 kubectl 은 쓸 수 없습니다.
백업이 있다는 것과 복원이 된다는 것은 다른 사실입니다 — 플랫폼 엔지니어 채용 공고가 스냅숏과 함께 정기 복원 훈련을 요구하는 이유입니다.
이 실습은 그 훈련을 한 번 실제로 해 보고, 복원 뒤에 남는 함정(kubeadm 이 기억하는 데이터 디렉터리)까지 닫습니다.
단계
- controlplane 에서 etcdctl 로 etcd 의 상태를 JSON 으로 읽어
/root/kx/etcd/status.json에 저장하세요(endpoint status -w json). 인증서는/etc/kubernetes/pki/etcd/의 것을 씁니다. - etcd 스냅숏을
/root/kx/etcd/snap.db에 저장하고, etcdutl 로 그 파일의 상태를 JSON 으로 읽어/root/kx/etcd/snap.json에 저장하세요. 스냅숏은 다음 단계의 사고가 나기 전에 떠야 합니다. kx-incident etcd-loss를 실행하면 누군가 네임스페이스payments를 지우고, 그 뒤로도 클러스터는 계속 쓰입니다. 2단계의 스냅숏으로 etcd 를 복원해payments(Deploymentledger, ConfigMap, Secret)를 스냅숏 때의 그 객체 그대로 되살리세요. 복원한 데이터 디렉터리를 쓰도록 etcd 정적 파드를 바꾸고, kubeadm 이 기억하는 etcd 데이터 디렉터리(kube-system의 ConfigMapkubeadm-config의etcd.local.dataDir)도 같은 경로로 맞추세요.kx-incident apiserver-flag를 실행한 뒤 API 서버를 되살리세요.kubectl은 쓸 수 없으니 컨테이너 런타임과 kubelet 쪽에서 원인을 찾고, 원인을 가리키는 로그 한 줄을/root/kx/etcd/apiserver.txt에evidence: <로그 한 줄>로 적습니다.kx-incident scheduler-conf를 실행하면 파드default/sched-probe가 Pending 에 머뭅니다. 원인을 찾아 고쳐sched-probe가 Running 이 되게 하고, 원인을 가리키는 로그 한 줄을/root/kx/etcd/scheduler.txt에evidence: <로그 한 줄>로 적으세요.kubeadm certs renew all로 kubeadm 이 관리하는 인증서를 모두 갱신하고, 갱신된 인증서를 실제로 쓰게 하세요 — kubeconfig 를 시작할 때 한 번 읽는 구성요소(kube-controller-manager·kube-scheduler)와 etcd·API 서버 정적 파드를 갱신 뒤에 다시 띄우고, root 의~/.kube/config도 갱신된admin.conf의 자격으로 바꿉니다.kx-incident etcd-nospace를 실행하면 클러스터에 아무것도 쓸 수 없게 됩니다. 쓰기를 되살리고, 원인이 된 쓰레기(네임스페이스junk)를 지우고, etcd 의 용량 상한을 기본값(2GiB, 플래그 없음)으로 되돌리세요. 원인을 가리키는 한 줄을/root/kx/etcd/nospace.txt에evidence: <한 줄>로 적습니다.
참고
- 이 터미널은 controlplane 입니다. 쿠버네티스 1.36.4(kubeadm), etcd 는 정적 파드(
/etc/kubernetes/manifests/etcd.yaml)이고etcdctl·etcdutl이 준비돼 있습니다(etcd 이미지 안의 것과 같은 판). - etcdctl 인증서 인자:
--endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key. 변수로 두면 편합니다. - 정적 파드를 다시 띄우려면 매니페스트를
/etc/kubernetes/manifests밖으로 옮겼다가 되돌립니다. 원본을 고치기 전에 사본을 떠 두세요. - 정적 파드가 내려가고 올라오는 데 20초에서 60초가 걸립니다. 그사이
kubectl이connection refused나TLS handshake timeout을 내는 것은 정상입니다. - 세션이 끝나면 VM 둘과 작업 파일이 함께 사라집니다.
etcd 가 지금 어떤 상태인지 숫자로 읽는다
controlplane 에서 etcdctl 로 etcd 의 상태를 JSON 으로 읽어 /root/kx/etcd/status.json 에 저장하세요(endpoint status -w json). 인증서는 /etc/kubernetes/pki/etcd/ 의 것을 씁니다.
kubeadm 의 etcd 는 정적 파드이고 클라이언트 인증서를 요구합니다. --endpoints=https://127.0.0.1:2379 --cacert=.../etcd/ca.crt --cert=.../etcd/server.crt --key=.../etcd/server.key. 출력의 dbSize·dbSizeQuota·revision·leader 를 읽어 보세요 — 마지막 단계에서 이 숫자들이 사고의 원인이 됩니다.
스냅숏을 뜨고, 뜬 것을 확인한다
etcd 스냅숏을 /root/kx/etcd/snap.db 에 저장하고, etcdutl 로 그 파일의 상태를 JSON 으로 읽어 /root/kx/etcd/snap.json 에 저장하세요. 스냅숏은 다음 단계의 사고가 나기 전에 떠야 합니다.
저장은 etcdctl snapshot save(살아 있는 서버에서 받아 오는 일), 파일의 확인과 복원은 etcdutl(서버 없이 파일을 다루는 일)입니다. etcd 3.6 에서 etcdctl snapshot status·restore 는 없어졌습니다. etcdutl snapshot status <파일> -w json 의 hash·revision 을 기록해 두면 나중에 같은 파일인지 확인할 수 있습니다.
지워진 네임스페이스를 스냅숏으로 되돌린다
kx-incident etcd-loss 를 실행하면 누군가 네임스페이스 payments 를 지우고, 그 뒤로도 클러스터는 계속 쓰입니다. 2단계의 스냅숏으로 etcd 를 복원해 payments(Deployment ledger, ConfigMap, Secret)를 스냅숏 때의 그 객체 그대로 되살리세요. 복원한 데이터 디렉터리를 쓰도록 etcd 정적 파드를 바꾸고, kubeadm 이 기억하는 etcd 데이터 디렉터리(kube-system 의 ConfigMap kubeadm-config 의 etcd.local.dataDir)도 같은 경로로 맞추세요.
순서가 전부입니다. API 서버가 살아 있는 채로 etcd 를 바꾸면 API 서버의 캐시가 새 데이터와 어긋납니다 — 먼저 /etc/kubernetes/manifests/kube-apiserver.yaml 을 밖으로 옮겨 API 서버를 내리고, etcdutl snapshot restore <파일> --data-dir <새 디렉터리> 로 풀고(--bump-revision 1000000000 --mark-compacted 는 남은 감시자들이 옛 리비전을 붙잡지 않게 합니다), etcd.yaml 의 데이터 hostPath 를 새 디렉터리로 바꾼 뒤 API 서버 매니페스트를 되돌립니다. 손으로 네임스페이스를 다시 만들면 이름은 같아도 UID 가 다른 새 객체입니다. kubeadm-config 를 그대로 두면 다음 kubeadm upgrade 가 옛 디렉터리를 쓰는 매니페스트를 다시 씁니다.
API 서버가 응답하지 않는다
kx-incident apiserver-flag 를 실행한 뒤 API 서버를 되살리세요. kubectl 은 쓸 수 없으니 컨테이너 런타임과 kubelet 쪽에서 원인을 찾고, 원인을 가리키는 로그 한 줄을 /root/kx/etcd/apiserver.txt 에 evidence: <로그 한 줄> 로 적습니다.
kubectl 이 안 될 때의 눈은 crictl 입니다 — crictl ps -a --name kube-apiserver 로 컨테이너가 재시작을 되풀이하는지 보고, crictl logs <컨테이너 ID> 2>&1 | tail 로 그 컨테이너가 남긴 마지막 말을 읽습니다. 정적 파드의 정의는 /etc/kubernetes/manifests/ 에 있고, 고쳐 저장하면 kubelet 이 알아서 다시 띄웁니다. etcd 는 클라이언트(2379)와 피어(2380) 포트를 따로 엽니다.
새 파드가 이벤트 하나 없이 Pending 이다
kx-incident scheduler-conf 를 실행하면 파드 default/sched-probe 가 Pending 에 머뭅니다. 원인을 찾아 고쳐 sched-probe 가 Running 이 되게 하고, 원인을 가리키는 로그 한 줄을 /root/kx/etcd/scheduler.txt 에 evidence: <로그 한 줄> 로 적으세요.
kubectl describe pod sched-probe 의 Events 가 비어 있다는 것 자체가 단서입니다. 노드가 모자라 못 놓는 파드에는 스케줄러가 FailedScheduling 이벤트를 남깁니다 — 아무 말이 없으면 판단할 스케줄러가 없는 것입니다. kubectl -n kube-system get pods 와 kubectl -n kube-system get lease kube-scheduler 의 갱신 시각을 보세요.
인증서를 갱신하고, 갱신이 실제로 쓰이게 한다
kubeadm certs renew all 로 kubeadm 이 관리하는 인증서를 모두 갱신하고, 갱신된 인증서를 실제로 쓰게 하세요 — kubeconfig 를 시작할 때 한 번 읽는 구성요소(kube-controller-manager·kube-scheduler)와 etcd·API 서버 정적 파드를 갱신 뒤에 다시 띄우고, root 의 ~/.kube/config 도 갱신된 admin.conf 의 자격으로 바꿉니다.
kubeadm certs check-expiration 으로 갱신 전후를 보세요. 갱신은 파일만 바꿉니다 — 공식 문서도 갱신 뒤 컨트롤 플레인 파드를 다시 띄우라고 합니다. 정적 파드는 kubectl delete pod 로는 다시 뜨지 않습니다(미러 파드일 뿐). 매니페스트를 디렉터리 밖으로 옮겼다가 20초 뒤 되돌리는 것이 확실합니다. ~/.kube/config 는 kubeadm init 때 admin.conf 를 복사해 둔 것이라 함께 바뀌지 않습니다. API 서버의 서빙 인증서는 다시 띄우지 않아도 새 파일로 바뀌는지 openssl s_client -connect 127.0.0.1:6443 로 확인해 보세요.
쓰기가 모두 실패한다 — etcd 가 찼다
kx-incident etcd-nospace 를 실행하면 클러스터에 아무것도 쓸 수 없게 됩니다. 쓰기를 되살리고, 원인이 된 쓰레기(네임스페이스 junk)를 지우고, etcd 의 용량 상한을 기본값(2GiB, 플래그 없음)으로 되돌리세요. 원인을 가리키는 한 줄을 /root/kx/etcd/nospace.txt 에 evidence: <한 줄> 로 적습니다.
etcd 는 상한에 닿으면 NOSPACE 경보를 걸고 읽기와 일부 삭제만 받습니다 — etcdctl alarm list, endpoint status -w table 의 DB SIZE·QUOTA. 쓰레기 네임스페이스를 먼저 지우려 해도 실패합니다: 네임스페이스 삭제는 「삭제 중」 표시를 쓰는 일부터 하기 때문입니다. etcd 문서의 순서는 옛 리비전을 버리고(compact <지금 리비전>) 빈자리를 돌려받고(defrag) 경보를 푼다(alarm disarm)입니다. 그다음에 쓰레기를 치우고 상한 플래그를 매니페스트에서 뺍니다.