LabHub
배우기 러닝패스 코스

쿠버네티스 배포판 — 직접 세운다 · kubeadm 으로 표준 조립 · 이론

kubeadm 이 일부러 비워 둔 것들

LabHub 에서 이어서 보기

한 줄 요약

kubeadm 은 "쿠버네티스를 설치해 주는 도구" 가 아니라 컨트롤 플레인을 모범 사례대로 조립해 주는 도구이고, 런타임·네트워크·대역·커널 설정은 끝까지 운영자의 결정으로 남습니다.

왜 이게 필요했나

k3s 나 k0s 를 먼저 써 본 사람은 kubeadm init 이 성공한 직후 당황합니다. 초록색 성공 문구가 나왔는데 노드는 NotReady 이고 CoreDNS 는 Pending 입니다. 고장이 아닙니다. kubeadm 은 일부러 네트워크 애드온을 고르지 않습니다. [Creating a cluster with kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/) 문서가 굵게 적어 둔 문장이 바로 "CNI 기반 파드 네트워크를 배포해야 하며, 네트워크를 설치하기 전에는 클러스터 DNS 가 뜨지 않는다" 입니다.

이렇게 설계한 이유는 kubeadm 의 자리가 "다른 설치 도구의 부품" 이기 때문입니다. 문서는 kubeadm 이 Ansible·Terraform 같은 프로비저닝 시스템이나 더 큰 설치 도구의 빌딩 블록으로 쓰이기를 기대한다고 말합니다. 부품이 CNI·스토리지·인그레스를 멋대로 골라 버리면 그 위에 올라가는 도구가 다시 걷어내야 합니다. 그래서 kubeadm 은 인증서·kubeconfig·정적 파드 매니페스트·부트스트랩 토큰처럼 어느 환경에서도 같아야 하는 것만 만들고, 환경마다 달라지는 것은 비워 둡니다. 그 대가로 사람이 알아야 할 것이 늘어납니다.

어떻게 동작하나

설치는 세 층으로 나뉩니다.

호스트 준비      swap · ip_forward · 컨테이너 런타임(CRI) · cgroup 드라이버kubeadm init    preflight → 인증서(/etc/kubernetes/pki) → kubeconfig → 정적 파드 매니페스트                → kubelet 이 매니페스트를 보고 etcd·apiserver·controller-manager·scheduler 를 띄움                → CoreDNS·kube-proxy 애드온, 부트스트랩 토큰, control-plane 테인트사람의 몫        CNI 설치(대역을 init 과 맞춰서) · 테인트 정리 · 워커 조인 · 인증서 갱신 계획

호스트 준비. [Installing kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/) 에 따르면 kubelet 은 기본값으로 swap 이 있으면 시작을 거부합니다. 패키지 저장소는 pkgs.k8s.io 에 마이너 판마다 따로 있고, 문서는 설치 뒤 apt-mark hold 로 kubelet·kubeadm·kubectl 을 일반 업그레이드에서 빼라고 합니다. 업그레이드는 kubeadm 절차를 따라야 하기 때문입니다. [Container Runtimes](https://kubernetes.io/docs/setup/production-environment/container-runtimes/) 문서는 리눅스 커널이 기본으로 인터페이스 사이 IPv4 전달을 막는다는 것, 그리고 systemd 가 init 인 호스트에서 cgroupfs 드라이버를 쓰면 cgroup 관리자가 둘이 되어 자원 압박 때 노드가 불안정해질 수 있다는 것을 설명합니다. containerd 2.x 에서는 plugins.'io.containerd.cri.v1.runtime' 아래 runc 옵션의 SystemdCgroup = true 가 그 스위치입니다.

여기서 판이 중요합니다. KubeletCgroupDriverFromCRI 기능은 1.34 에서 stable 이 되었고, CRI 가 RuntimeConfig 호출을 지원하면 kubelet 은 자기 설정의 cgroupDriver 를 무시하고 런타임이 알려 주는 값을 씁니다. containerd 는 2.0 부터 지원합니다. 이 실습 VM(containerd 2.3.5, kubelet 1.36.4)에서 실측하면 kubelet 로그에 Using cgroup driver setting received from the CRI runtime 이 찍힙니다. 즉 지금은 containerd 설정이 사실상 유일한 진실입니다.

init. preflight 는 막는 것과 경고만 하는 것이 섞여 있습니다. 1.36.4 에서 실측하면 ip_forward 가 0 일 때는 [ERROR FileContent--proc-sys-net-ipv4-ip_forward] 로 막지만 br_netfilter 는 보지 않습니다. 통과하면 kubeadm 은 /etc/kubernetes/manifests 에 매니페스트 네 개를 쓰고, kubelet 이 그 디렉터리를 직접 읽어 컨트롤 플레인을 띄웁니다. API 서버가 아직 없는데 API 서버를 띄워야 하는 닭과 달걀 문제를 정적 파드로 푸는 것입니다. API 서버에 보이는 컨트롤 플레인 파드는 kubelet 이 등록한 거울(mirror) 파드라 소유자가 Node 이고, 지워도 kubelet 이 다시 등록합니다.

대역. --pod-network-cidr 는 controller-manager 가 노드마다 podCIDR 을 나눠 주게 하고, --service-cidr 는 API 서버의 --service-cluster-ip-range 가 됩니다. 문서는 파드 네트워크가 호스트 네트워크와 겹치면 문제가 생기니 겹치지 않는 대역을 골라 init 과 네트워크 플러그인 YAML 양쪽에 똑같이 넣으라고 합니다.

현장에서 만나는 모습

이 실습 VM 에서 그대로 재현한 장면입니다. Flannel v0.28.9 매니페스트를 적용하면 8초 만에 노드가 Ready 로 바뀝니다. 그런데 CoreDNS 는 한참 ContainerCreating 입니다. 이유는 순서에 있습니다. Flannel 파드의 초기화 컨테이너가 /etc/cni/net.d/10-flannel.conflist 를 먼저 복사하고, kubelet 은 설정 파일이 생긴 것만 보고 네트워크가 준비됐다고 판단합니다. 정작 flanneld 본체는 이렇게 죽고 재시작을 반복합니다.

E0914 22:51:43.944688  1 main.go:292] Failed to check br_netfilter:  stat /proc/sys/net/bridge/bridge-nf-call-iptables: no such file or directory

[Flannel README](https://github.com/flannel-io/flannel) 에도 Flannel 은 br_netfilter 가 있어야 시작하고, kubeadm 이 1.30 부터 그 모듈을 확인하지 않는다고 적혀 있습니다. 모듈을 올린 뒤에도 CrashLoopBackOff 의 재시작 간격 때문에 실측 74초를 더 기다렸습니다. Ready 는 "설정 파일이 있다" 이지 "데이터 경로가 돈다" 가 아닙니다. 판정은 CoreDNS 가 실제로 이름을 돌려주는지로 해야 합니다.

두 번째 장면은 대역입니다. 이 VM 은 호스트 쿠버네티스 안에서 돌고, VM 파드의 주소가 호스트 파드 대역인 10.244.x 입니다. Flannel 매니페스트의 기본 Network 가 정확히 10.244.0.0/16 이라, 매니페스트를 고치지 않고 적용하면 안쪽 파드 대역이 바깥과 겹칩니다. 그래서 이 실습은 172.20.0.0/16 · 172.21.0.0/16 을 쓰고, 매니페스트 한 줄을 init 값에 맞춥니다.

세 번째는 containerd 드롭인입니다. 저자의 GPU 노드(containerd 1.7.27)에서는 conf.d 드롭인 하나가 CRI 플러그인 설정을 통째로 덮어 런타임 설정이 기본값으로 돌아간 일이 있었습니다. 같은 실험을 이 VM 의 containerd 2.3.5 에서 해 보니 드롭인이 필드 단위로 합쳐져 SystemdCgroup = true 가 남았습니다. 판마다 병합 규칙이 달라질 수 있으니 파일을 눈으로 읽고 믿지 말고 containerd config dumpcrictl info 로 확인하는 습관이 답입니다.

실무에서 진짜 중요한 것

인증서는 1년짜리입니다. [Certificate Management with kubeadm](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-certs/) 에 따르면 kubeadm 이 만든 클라이언트 인증서는 1년 뒤 만료되고, 기본값은 잎 인증서 8760h·CA 87600h 입니다. 이 VM 에서도 kubeadm certs check-expiration 이 잎 364일·CA 9년을 보여 줍니다. kubelet 인증서는 자동 회전되므로 목록에 없습니다. 1년 동안 업그레이드를 한 번도 안 한 클러스터가 어느 날 API 호출이 전부 거절되는 사고가 여기서 나옵니다.

조인 명령의 해시는 보안 장치입니다. --discovery-token-ca-cert-hash 는 CA 공개키의 sha256 이고, 조인하는 노드가 "이 API 서버가 정말 우리 CA 로 서명됐는가" 를 확인하는 값입니다. 토큰은 kube-system 의 bootstrap-token-<id> Secret 으로 저장되고, BootstrapSigner 가 kube-public 의 cluster-info ConfigMap 에 토큰별 HMAC 서명을 붙입니다. 해시 검증을 끄는 옵션도 있지만 문서는 가능하면 다른 방식을 쓰라고 권합니다.

한 대짜리 클러스터라면 테인트를 기억합니다. kubeadm 은 보안상 컨트롤 플레인 노드에 node-role.kubernetes.io/control-plane:NoSchedule 을 붙여 일반 파드를 받지 않습니다. 노드가 하나뿐이면 모든 워크로드가 Pending 이 됩니다.

판을 박습니다. --kubernetes-version 을 빼면 kubeadm 이 인터넷에서 최신 판 번호부터 찾습니다(실측: remote version is much newer: v1.37.0; falling back to: stable-1.36). 폐쇄망이라면 그 한 줄에서 막힙니다.

다음 실습에서 할 것

containerd 설정부터 시작해 kubeadm init 으로 컨트롤 플레인을 세우고, NotReady 를 증거로 남긴 뒤 Flannel 을 붙입니다. 노드가 Ready 인데 DNS 가 안 되는 상태를 직접 겪고 br_netfilter 로 되살립니다. 마지막으로 정적 파드를 지워 보고, 테인트를 풀고, 인증서 만료일과 조인 명령을 직접 계산해 보고서로 정리합니다.