LabHub
배우기 러닝패스 코스

Kubernetesディストリビューション — 自分で立てる

init は成功したのにノードが NotReady だ

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

빈 우분투 VM 에서 kubeadm 으로 컨트롤 플레인 한 대를 세우고, NotReady 의 원인이 CNI 라는 것을 증거로 확인한 뒤 Flannel 을 붙여 DNS 까지 살립니다. 그 과정에서 정적 파드·테인트·인증서 수명·조인 토큰이 각각 무슨 역할인지 손으로 봅니다.

왜 중요한가

k3s 와 k0s 는 런타임·CNI·저장소를 배포판이 대신 골라 줍니다. kubeadm 은 그 결정을 전부 사람에게 남기는 표준 조립 키트라서, init 이 성공했다는 말이 "클러스터가 쓸 수 있다" 는 뜻이 아닙니다. 네트워크 애드온을 고르고, 그 애드온이 요구하는 커널 설정을 맞추고, 대역이 주변 네트워크와 겹치지 않게 하는 것이 전부 운영자의 일입니다. 특히 노드가 Ready 로 바뀌었다고 끝난 것이 아니라는 점을 이 실습에서 직접 겪습니다 — CNI 설정 파일만 생겨도 Ready 가 되지만, 데몬이 죽어 있으면 CoreDNS 는 뜨지 않습니다. 인증서 1년 만료와 조인 토큰의 CA 해시는 설치 당일에는 아무 문제도 일으키지 않다가 몇 달 뒤에 사고가 되는 부분이라 미리 읽는 법을 익힙니다.

단계

  1. net.ipv4.ip_forward = 1/etc/sysctl.d/k8s.conf 에 적어 적용하고, containerd config default/etc/containerd/config.toml 을 만든 뒤 runc 의 SystemdCgroup 을 true 로 바꿔 containerd 를 재시작하세요. 그다음 /root/kubeadm/runtime.jsoncontainerd_version(containerd --version 의 세 번째 칸), systemd_cgroup(실행 중인 containerd 가 보고하는 값, 불리언), ip_forward(지금 커널 값, 숫자), swap_total_kb(/proc/meminfo 의 SwapTotal, 숫자)를 적으세요.
  2. kubeadm init--kubernetes-version v1.36.4 --pod-network-cidr 172.20.0.0/16 --service-cidr 172.21.0.0/16 로 실행하고 출력 전체를 /root/kubeadm/init.log 에 저장하세요. /etc/kubernetes/admin.conf/root/.kube/config 로 복사해 kubectl 이 새 클러스터를 보게 합니다.
  3. CNI 를 설치하기 전에 지금 상태를 /root/kubeadm/notready.json 에 기록하세요. 필드: node_uid, ready_status(Ready 조건의 status), ready_message(Ready 조건의 message), taints(노드 테인트를 키:효과 문자열로 담은 배열), coredns_phase(CoreDNS 파드 하나의 phase), cni_conf_count(/etc/cni/net.d 에서 점으로 시작하지 않는 파일 수), observed_at(기록한 UTC 시각, 2026-01-01T00:00:00Z 형식).
  4. Flannel v0.28.9 릴리스의 kube-flannel.yml/root/kubeadm/kube-flannel.yml 로 받아 net-conf.jsonNetwork 를 이 클러스터 파드 대역으로 바꾸고 적용하세요(br_netfilter 는 아직 올리지 않습니다). 30초쯤 지켜본 뒤 /root/kubeadm/cni-symptom.jsonnode_ready(Ready 조건 status), coredns_ready_replicas(coredns Deployment 의 readyReplicas, 없으면 0), flannel_pod_uid, flannel_restarts(kube-flannel 컨테이너 restartCount), flannel_error(flannel 로그에서 원인을 말하는 한 줄)를 적으세요.
  5. br_netfilter 모듈을 /etc/modules-load.d/k8s.conf 에 적어 부팅 때 올라오게 하고 지금도 올리세요. net.bridge.bridge-nf-call-iptables = 1/etc/sysctl.d/k8s.conf 에 더해 적용합니다. Flannel DaemonSet 이 준비되고 CoreDNS 두 개가 Available 이 되며, 노드에서 dig @<kube-dns 서비스 IP> kubernetes.default.svc.cluster.local 이 kubernetes 서비스 IP 를 돌려줘야 합니다.
  6. /root/kubeadm/static-pods.jsonmanifests(/etc/kubernetes/manifests 의 파일 이름 정렬 배열), owner_kind(kube-scheduler 파드의 ownerReferences 종류), etcd_data_dir(etcd 매니페스트가 hostPath 로 붙인 데이터 경로), service_cluster_ip_range(kube-apiserver 매니페스트의 같은 이름 플래그 값)를 적으세요. 그리고 kube-scheduler 파드를 kubectl delete 로 지운 뒤 되돌아온 것을 확인해 deleted_uid, new_uid, container_id_before, container_id_after(containerStatuses[0].containerID)를 같은 파일에 더합니다.
  7. default 네임스페이스에 파드 probe(이미지 registry.k8s.io/pause:3.10.2, restartPolicy Never)를 만들고 Pending 인 것을 확인해 /root/kubeadm/pending.jsonpod_uid, phase, message(PodScheduled 조건의 message), observed_at(UTC, 2026-01-01T00:00:00Z 형식)을 적으세요. 그다음 노드에서 node-role.kubernetes.io/control-plane:NoSchedule 테인트를 빼 probe 가 Running 이 되게 합니다. 파드는 지우고 다시 만들지 않습니다.
  8. kubeadm certs check-expiration 과 openssl 로 확인해 /root/kubeadm/certs.jsonapiserver_not_after, ca_not_after(둘 다 UTC 2026-01-01T00:00:00Z 형식), apiserver_valid_days, ca_valid_days(각 인증서 notBefore 부터 notAfter 까지 일수, 정수)를 적으세요. 그리고 kubeadm token create --ttl 3h --description worker-join 으로 새 토큰을 만들고, CA 공개키 해시를 직접 계산해 /root/kubeadm/join.txt 에 한 줄 kubeadm join <API 엔드포인트> --token <토큰> --discovery-token-ca-cert-hash sha256:<해시> 를 적으세요.
  9. /root/kubeadm/report.jsonkubernetes_version(서버 gitVersion), pod_cidr, service_cidr, dns_service_ip(kube-dns 서비스 IP), cgroup_driver(kubelet 이 쓰는 드라이버), notready_cause(3단계 NotReady 의 원인: cni · runtime · certificate 중 하나), flannel_blocker(4단계에서 flannel 을 막은 커널 모듈 이름), join_token_id(8단계 토큰의 앞 6자리)를 적으세요.

참고

init 전에 런타임부터 맞춘다

net.ipv4.ip_forward = 1/etc/sysctl.d/k8s.conf 에 적어 적용하고, containerd config default/etc/containerd/config.toml 을 만든 뒤 runc 의 SystemdCgroup 을 true 로 바꿔 containerd 를 재시작하세요. 그다음 /root/kubeadm/runtime.jsoncontainerd_version(containerd --version 의 세 번째 칸), systemd_cgroup(실행 중인 containerd 가 보고하는 값, 불리언), ip_forward(지금 커널 값, 숫자), swap_total_kb(/proc/meminfo 의 SwapTotal, 숫자)를 적으세요.

파일에 적는 것과 실행 중인 데몬이 쓰는 것은 다릅니다. containerd config dump 는 설정 파일을 합쳐 보여 줄 뿐이고, 재시작하지 않은 데몬이 무엇을 쓰는지는 CRI 에 물어야 합니다(crictl info). ip_forward 는 sysctl --system 으로 파일을 다시 읽게 해야 지금 값이 바뀝니다.

대역을 비켜서 init 한다

kubeadm init--kubernetes-version v1.36.4 --pod-network-cidr 172.20.0.0/16 --service-cidr 172.21.0.0/16 로 실행하고 출력 전체를 /root/kubeadm/init.log 에 저장하세요. /etc/kubernetes/admin.conf/root/.kube/config 로 복사해 kubectl 이 새 클러스터를 보게 합니다.

이 VM 은 호스트 쿠버네티스 안에서 돌고, 호스트가 파드에 10.244.0.0/16 · 서비스에 10.96.0.0/12 를 씁니다. 두 대역이 겹치면 안쪽 kube-proxy 가 바깥 DNS 주소를 가로챕니다. --kubernetes-version 을 빼면 kubeadm 이 인터넷에서 최신 판 번호부터 찾으러 갑니다.

init 은 성공했는데 노드가 NotReady 다

CNI 를 설치하기 전에 지금 상태를 /root/kubeadm/notready.json 에 기록하세요. 필드: node_uid, ready_status(Ready 조건의 status), ready_message(Ready 조건의 message), taints(노드 테인트를 키:효과 문자열로 담은 배열), coredns_phase(CoreDNS 파드 하나의 phase), cni_conf_count(/etc/cni/net.d 에서 점으로 시작하지 않는 파일 수), observed_at(기록한 UTC 시각, 2026-01-01T00:00:00Z 형식).

노드의 Ready 조건 message 가 원인을 직접 말합니다. CoreDNS 가 Pending 인 이유는 파드의 PodScheduled 조건에 있고, 그 이유가 노드에 붙은 어느 테인트인지 대조해 보세요. 채점기는 이 기록이 CNI 설치보다 먼저 쓰였는지를 시각으로 대조합니다.

CNI 를 깔았더니 노드만 Ready 가 됐다

Flannel v0.28.9 릴리스의 kube-flannel.yml/root/kubeadm/kube-flannel.yml 로 받아 net-conf.jsonNetwork 를 이 클러스터 파드 대역으로 바꾸고 적용하세요(br_netfilter 는 아직 올리지 않습니다). 30초쯤 지켜본 뒤 /root/kubeadm/cni-symptom.jsonnode_ready(Ready 조건 status), coredns_ready_replicas(coredns Deployment 의 readyReplicas, 없으면 0), flannel_pod_uid, flannel_restarts(kube-flannel 컨테이너 restartCount), flannel_error(flannel 로그에서 원인을 말하는 한 줄)를 적으세요.

릴리스 자산 주소는 https://github.com/flannel-io/flannel/releases/download/v0.28.9/kube-flannel.yml 입니다. 매니페스트 기본 Network 는 10.244.0.0/16 이라 이 VM 에서는 그대로 쓸 수 없습니다. 노드 Ready 는 CNI 설정 파일이 생겼다는 뜻일 뿐, 데몬이 살아 있다는 뜻이 아닙니다. 재시작 중인 컨테이너의 로그는 --previous 로 볼 수 있습니다.

br_netfilter 를 올려 CNI 를 살린다

br_netfilter 모듈을 /etc/modules-load.d/k8s.conf 에 적어 부팅 때 올라오게 하고 지금도 올리세요. net.bridge.bridge-nf-call-iptables = 1/etc/sysctl.d/k8s.conf 에 더해 적용합니다. Flannel DaemonSet 이 준비되고 CoreDNS 두 개가 Available 이 되며, 노드에서 dig @<kube-dns 서비스 IP> kubernetes.default.svc.cluster.local 이 kubernetes 서비스 IP 를 돌려줘야 합니다.

CrashLoopBackOff 는 재시작 간격을 점점 늘리므로, 원인을 고친 뒤에도 한참 기다릴 수 있습니다. DaemonSet 이 관리하는 파드는 지워도 곧바로 다시 만들어집니다. 모듈을 올린 뒤 sysctl 을 다시 읽어야 bridge 항목이 생깁니다.

지워도 돌아오는 컨트롤 플레인 파드

/root/kubeadm/static-pods.jsonmanifests(/etc/kubernetes/manifests 의 파일 이름 정렬 배열), owner_kind(kube-scheduler 파드의 ownerReferences 종류), etcd_data_dir(etcd 매니페스트가 hostPath 로 붙인 데이터 경로), service_cluster_ip_range(kube-apiserver 매니페스트의 같은 이름 플래그 값)를 적으세요. 그리고 kube-scheduler 파드를 kubectl delete 로 지운 뒤 되돌아온 것을 확인해 deleted_uid, new_uid, container_id_before, container_id_after(containerStatuses[0].containerID)를 같은 파일에 더합니다.

정적 파드는 API 서버가 아니라 kubelet 이 디렉터리를 보고 띄웁니다. API 서버에 보이는 것은 kubelet 이 만든 거울(mirror) 파드라, 지워도 kubelet 이 거울만 다시 등록합니다. 컨테이너가 재시작됐는지는 containerID 로 판단하세요.

한 대짜리 클러스터에 파드가 안 올라간다

default 네임스페이스에 파드 probe(이미지 registry.k8s.io/pause:3.10.2, restartPolicy Never)를 만들고 Pending 인 것을 확인해 /root/kubeadm/pending.jsonpod_uid, phase, message(PodScheduled 조건의 message), observed_at(UTC, 2026-01-01T00:00:00Z 형식)을 적으세요. 그다음 노드에서 node-role.kubernetes.io/control-plane:NoSchedule 테인트를 빼 probe 가 Running 이 되게 합니다. 파드는 지우고 다시 만들지 않습니다.

kubeadm 은 컨트롤 플레인 노드에 일반 파드가 오지 않게 테인트를 붙입니다. 노드가 한 대뿐이면 그 결정이 곧 '아무 데도 못 간다' 가 됩니다. 테인트를 빼는 문법은 키:효과 뒤에 빼기표를 붙이는 것입니다. 스케줄러는 테인트가 바뀌면 Pending 파드를 다시 시도합니다.

1년 뒤와 두 번째 노드를 준비한다

kubeadm certs check-expiration 과 openssl 로 확인해 /root/kubeadm/certs.jsonapiserver_not_after, ca_not_after(둘 다 UTC 2026-01-01T00:00:00Z 형식), apiserver_valid_days, ca_valid_days(각 인증서 notBefore 부터 notAfter 까지 일수, 정수)를 적으세요. 그리고 kubeadm token create --ttl 3h --description worker-join 으로 새 토큰을 만들고, CA 공개키 해시를 직접 계산해 /root/kubeadm/join.txt 에 한 줄 kubeadm join <API 엔드포인트> --token <토큰> --discovery-token-ca-cert-hash sha256:<해시> 를 적으세요.

잎 인증서와 CA 의 기본 수명은 kubeadm 설정의 certificateValidityPeriod·caCertificateValidityPeriod 로 정해집니다. 해시는 CA 인증서의 공개키를 DER 로 뽑아 sha256 한 값입니다. 엔드포인트는 kube-public 의 cluster-info ConfigMap 에 적힌 server 주소와 같아야 합니다.

무엇을 직접 골랐는지 보고

/root/kubeadm/report.jsonkubernetes_version(서버 gitVersion), pod_cidr, service_cidr, dns_service_ip(kube-dns 서비스 IP), cgroup_driver(kubelet 이 쓰는 드라이버), notready_cause(3단계 NotReady 의 원인: cni · runtime · certificate 중 하나), flannel_blocker(4단계에서 flannel 을 막은 커널 모듈 이름), join_token_id(8단계 토큰의 앞 6자리)를 적으세요.

앞 단계에서 남긴 기록과 지금의 클러스터를 근거로 적습니다. 채점기는 같은 값을 클러스터와 앞 단계 기록에서 다시 계산합니다. kubelet 의 드라이버는 kubelet 로그의 'cgroup driver setting received from the CRI runtime' 줄이나 /var/lib/kubelet/config.yaml 에서 확인할 수 있습니다.