kubeadm init 은 인증서 생성 · kubeconfig 작성 · 컨트롤 플레인 static Pod 기동 · 부트스트랩 RBAC 적용을 순서대로 수행한다. 중간에 끊기면 앞 단계의 산출물은 남고 뒤 단계는 비어 있는 어중간한 상태가 되며, 이 상태에서 같은 명령을 다시 돌리면 남아 있던 인증서와 새로 만든 인증서가 섞여 원인 추적이 어려워진다. 이 문서는 그 상태에서 나온 대표적인 증상 셋과, 되살릴지 재설치할지의 판단 기준을 정리한 것이다.
Unable to connect to the server: tls: failed to verify certificate:
x509: certificate signed by unknown authority (possibly because of
"crypto/rsa: verification error" while trying to verify candidate
authority certificate "kubernetes")
컨트롤 플레인 컴포넌트끼리는 정상 통신하는데 kubectl 만 이 에러를 낸다면, CA 파일이 없는 것이 아니라 admin.conf 에 박혀 있는 certificate-authority-data 가 현재 apiserver 의 PKI 와 어긋난 것이다. kubeadm init 이 중간에 실패해 PKI 일부가 재생성됐거나, kubeconfig 를 손으로 다시 만든 뒤에 흔히 나온다.
/etc/kubernetes/admin.conf 를 ~/.kube/config 로 다시 복사하는 것만으로는 낫지 않는다. 원본 admin.conf 자체가 옛 CA 기준이기 때문이다. 현재 PKI 기준으로 다시 만들어야 한다.
cp /etc/kubernetes/admin.conf /root/admin.conf.bak
kubeadm init phase kubeconfig admin
cp -f /etc/kubernetes/admin.conf $HOME/.kube/config
chmod 600 $HOME/.kube/config
kubectl get nodes
kubeadm init phase kubeconfig admin 은 기존 파일이 있으면 Using existing kubeconfig file 이라고만 하고 덮어쓰지 않는다. 다시 만들려면 먼저 백업하고 원본을 지운 뒤 실행해야 한다.
두 CA 가 같은지 직접 대조할 수도 있다.
openssl x509 -in /etc/kubernetes/pki/ca.crt -noout -subject -issuer -fingerprint
grep certificate-authority-data /etc/kubernetes/admin.conf | awk '{print $2}' | base64 -d | openssl x509 -noout -fingerprint
kubectl --insecure-skip-tls-verify 로 넘기지 않는다. 원인을 덮어 둔 채 클러스터를 계속 쓰게 되고, 나중에 워커 조인이나 인증서 갱신에서 같은 불일치가 다시 터진다.
static Pod 로그에 다음이 반복된다.
"Failed to watch" err="failed to list *v1.Node: nodes is forbidden:
User \"system:kube-scheduler\" cannot list resource \"nodes\" in API group \"\"
at the cluster scope"
부트스트랩 RBAC 단계가 적용되지 않아 system:kube-scheduler ClusterRoleBinding 이 없거나 깨진 상태다. 인증(누구인지)은 통과했고 인가(무엇을 할 수 있는지)만 막혔다는 점이 x509 에러와 다르다.
kubectl get clusterrolebinding system:kube-scheduler -o yaml
NotFound 이면 다시 만든다.
kubectl create clusterrolebinding system:kube-scheduler \
--clusterrole=system:kube-scheduler \
--user=system:kube-scheduler
정석은 부트스트랩 RBAC 단계 전체를 다시 태우는 것이다. 개별 바인딩을 손으로 만드는 것보다 누락을 덜 남긴다.
kubeadm init phase bootstrap-token
scheduler 는 static Pod 이므로 kubelet 이 알아서 재기동한다. 즉시 확인하려면 crictl logs 로 forbidden 이 사라졌는지 본다.
--root-dir 또는 KubeletConfiguration 으로 kubelet 의 작업 경로를 /var/lib/kubelet 밖으로 옮긴 구성에서는 정리 절차가 달라진다. 옮긴 경로 아래 pods 를 지우려 하면 다음이 난다.
rm: cannot remove 'pods/<uid>/volumes/kubernetes.io~projected/kube-api-access-xxxxx':
Device or resource busy
projected volume 과 secret 이 아직 tmpfs 로 마운트돼 있어서 그렇다. 지우기 전에 풀어야 한다.
systemctl stop kubelet
crictl --runtime-endpoint unix:///run/containerd/containerd.sock rm -fa
mount | grep '/kubelet/pods' | awk '{print $3}' | xargs -r -n1 umount
rm -rf <KUBELET_ROOT>/pods
kubeadm reset 도 기본 경로만 정리하므로, 옮긴 경로는 위처럼 직접 치워야 한다. 이 정리를 빼먹으면 다음 init 에서 kubelet 이 옛 Pod 디렉터리를 다시 읽어 invalid capacity 0 on image filesystem 같은 이상한 증상으로 나타난다.
한두 단계만 어긋난 경우에는 kubeadm init phase 로 그 단계만 다시 태우는 것이 빠르다. 그러나 PKI 를 건드렸고 RBAC 도 손으로 만들었으며 kubelet 을 여러 번 재기동한 뒤라면, 어느 산출물이 어느 세대인지 추적하는 비용이 재설치보다 크다. 그때는 아래 순서로 깨끗이 지우고 다시 시작한다.
kubeadm reset -f
systemctl stop kubelet containerd
rm -rf /etc/kubernetes /var/lib/etcd
rm -rf /var/lib/kubelet/*
rm -rf /etc/cni/net.d /var/lib/cni
systemctl start containerd kubelet
그다음 런타임이 실제로 컨테이너를 띄울 수 있는지부터 확인하고, 그 뒤에 init 을 돌린다.
ctr --namespace k8s.io run --rm docker.io/library/busybox:latest bbtest sh -c 'echo OK'
kubeadm init \
--apiserver-advertise-address=<CONTROL_PLANE_IP> \
--pod-network-cidr=10.244.0.0/16 \
--cri-socket=unix:///run/containerd/containerd.sock \
--kubernetes-version v1.33.7
--kubernetes-version 을 생략하면 kubeadm 이 원격 최신 버전을 조회하고, 그것이 자신이 지원하는 범위를 넘으면 remote version is much newer ... falling back to: stable-1.33 처럼 임의로 떨어뜨린다. 그 결과 kubelet · kubeadm 패키지 버전과 컨트롤 플레인 이미지 버전이 어긋나므로 항상 명시한다.
init 직후 노드가 NotReady 인 것은 정상이며, CNI 를 올리면 Ready 가 된다. CNI Pod 가 Pending 에서 움직이지 않으면 노드 자체가 아직 스케줄 불가 상태인지(scheduler CrashLoop, taint) 먼저 보고, Init:1/3 이나 install-cni CrashLoop 이면 CNI 쪽 문제로 갈라서 본다.
인터넷이 제한된 환경에서는 elrepo-release 같은 부가 저장소가 아예 없어 No match for argument 로 끝난다. 이런 환경에서는 RPM 을 직접 내려받아 dnf install ./elrepo-release-*.rpm 으로 설치한다. 설치 여부는 트랜잭션 출력의 @commandline 표기로 확인할 수 있다.
워커 조인 토큰은 24시간 만에 만료된다. 재발급과 CA 해시 확인은 다음으로 한다.
kubeadm token create --print-join-command