LabHub
学习 学习路径 课程

CKA — Kubernetes 管理员

kubeadm 不替你做的事 — 准备、版本偏差、接口

在 LabHub 中继续学习

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

한 줄 요약

kubeadm 은 컨트롤 플레인을 조립하는 도구이지 노드를 준비하는 도구가 아닙니다. swap·IP 포워딩·컨테이너 런타임·cgroup 드라이버·포트는 사람이 맞춰 두어야 하고, 업그레이드는 버전 스큐 정책이 정한 순서(kube-apiserver 가 먼저)를 따라야 하며, CNI·CSI·CRI 는 네트워크·스토리지·런타임을 코어에서 떼어 내 교체 가능하게 만든 인터페이스입니다.

概念图: 조립 · 준비 · 고쳐 주지는 · 강제

왜 이게 필요했나

kubeadm init 은 시작하자마자 preflight 검사를 돌립니다. kubeadm 설치 문서는 요구사항을 이렇게 적습니다 — 머신당 2GB 이상의 RAM, 컨트롤 플레인 머신은 2 CPU 이상, 모든 머신 사이의 완전한 네트워크 연결, 노드마다 고유한 hostname·MAC 주소·product_uuid, 그리고 특정 포트의 개방. 바이너리는 glibc 에 동적 링크되어 있어 Alpine 처럼 glibc 가 없는 배포판에서는 호환 계층이 필요하고, kubeadm 은 지원하는 커널 버전인지 SystemVerification 검사로 확인합니다.

이 검사가 실패하면 kubeadm 은 멈추지만, 통과하도록 고쳐 주지는 않습니다. 그래서 준비 항목을 모르면 "왜 안 되지" 에서 시작해 검색으로 하나씩 메우게 되고, 시험장에서는 그 시간이 없습니다. 업그레이드도 마찬가지입니다. kubeadm 은 스큐 정책을 강제하지만 어느 순서로 어느 노드를 올려야 하는지는 사람이 알고 있어야 합니다.

어떻게 동작하나

설치 전 인프라 준비

swap. kubelet 의 기본 동작은 노드에 swap 이 감지되면 시작을 거부하는 것입니다. 그러니 swapoff -a 로 끄고, 재부팅 뒤에도 유지되도록 /etc/fstab 이나 systemd.swap 설정에서 빼야 합니다. 굳이 swap 을 두려면 kubelet 설정에 failSwapOn: false 를 주어야 하고, 그래도 워크로드는 기본 swapBehaviorNoSwap 때문에 swap 을 쓰지 않습니다.

커널 네트워크 설정. 컨테이너 런타임 문서에 따르면 리눅스 커널은 기본적으로 인터페이스 사이의 IPv4 패킷 라우팅을 허용하지 않습니다. 그래서 /etc/sysctl.d/k8s.confnet.ipv4.ip_forward = 1 을 적고 sysctl --system 으로 적용합니다. 문서는 대부분의 네트워크 구현체가 필요하면 이 값을 스스로 바꾸지만 일부는 관리자에게 맡기며, "다른 sysctl 값이나 커널 모듈 적재를 기대하는 구현체도 있으니 네트워크 구현체의 문서를 보라" 고 덧붙입니다. 예전 안내서에 있던 overlay·br_netfilter 모듈 적재와 bridge-nf-call 계열 sysctl 은 현재 공식 페이지의 필수 목록에는 없고 CNI 구현체가 요구하는지에 따라 결정합니다. 시험이나 현장에서 옛 절차를 그대로 외우기보다, 지금 문서가 요구하는 것(ip_forward)과 구현체가 요구하는 것을 구분해 두는 편이 안전합니다.

컨테이너 런타임. 쿠버네티스는 CRI 로 런타임과 대화합니다. 런타임을 지정하지 않으면 kubeadm 이 알려진 소켓 경로를 훑어 자동 감지하고, 여러 개가 있거나 하나도 없으면 오류를 내며 지정을 요구합니다. Docker Engine 은 CRI 를 구현하지 않으므로 cri-dockerd 를 따로 설치해야 하고, kubelet 의 Docker 내장 지원(dockershim)은 1.24 에서 제거됐습니다.

런타임 유닉스 소켓
containerd unix:///var/run/containerd/containerd.sock
CRI-O unix:///var/run/crio/crio.sock
Docker Engine (cri-dockerd) unix:///var/run/cri-dockerd.sock

cgroup 드라이버. kubelet 과 런타임은 둘 다 cgroup 으로 자원을 제한하고, 그때 쓰는 드라이버가 cgroupfssystemd 둘입니다. 문서가 "critical" 이라고까지 말하는 규칙은 kubelet 과 런타임이 같은 드라이버를 써야 한다는 것입니다. kubelet 의 기본은 cgroupfs 이지만, init 시스템이 systemd 인 배포판에서는 cgroup 관리자가 둘이 되어 자원 압박 아래에서 노드가 불안정해질 수 있으므로 systemd 드라이버를 씁니다. cgroup v2 를 쓴다면 systemd 드라이버가 답입니다. kubelet 쪽은 KubeletConfigurationcgroupDriver: systemd, 런타임 쪽은 각 런타임 문서의 설정입니다. 쿠버네티스 1.37 에서는 KubeletCgroupDriverFromCRI 기능 게이트가 켜져 있고 런타임이 RuntimeConfig CRI RPC 를 지원하면 kubelet 이 런타임에서 드라이버를 자동으로 알아내지만, containerd 1.y 처럼 그 RPC 가 없는 런타임에서는 여전히 kubelet 자신의 설정을 씁니다.

포트. 포트와 프로토콜 문서의 표를 그대로 옮기면 이렇습니다.

어디 포트 용도
컨트롤 플레인 6443 kube-apiserver
컨트롤 플레인 2379-2380 etcd 클라이언트 API
컨트롤 플레인 10250 kubelet API
컨트롤 플레인 10259 kube-scheduler
컨트롤 플레인 10257 kube-controller-manager
워커 10250 kubelet API
워커 10256 kube-proxy
워커 30000-32767 NodePort Service (TCP·UDP)

클러스터 생애주기 — 스큐가 순서를 정한다

버전 스큐 정책 은 프로젝트가 최근 세 개의 minor 릴리스 브랜치를 유지하고, 1.19 이후 버전은 약 1년의 패치 지원을 받는다고 적습니다. 구성요소 사이의 허용 차이는 이렇습니다.

이 규칙에서 업그레이드 순서가 나옵니다. kube-apiserver 를 먼저 올리고(minor 를 건너뛸 수 없습니다), 그다음 controller-manager 와 scheduler, 마지막에 kubelet 과 kube-proxy 를 올립니다. 반대로 하면 "kubelet 이 apiserver 보다 새롭다" 는 금지 상태를 지나게 됩니다. 문서는 업그레이드 전에 현재 minor 의 최신 패치로 올리고, 목표 minor 의 최신 패치로 가라고 권합니다.

kubeadm 클러스터 업그레이드 문서가 정한 절차는 세 단계입니다 — 첫 컨트롤 플레인 노드, 추가 컨트롤 플레인 노드, 워커 노드.

  1. 첫 컨트롤 플레인에서 kubeadm upgrade plan 으로 올릴 수 있는 버전과 구성요소 설정 상태를 확인한 뒤 kubeadm upgrade apply v1.X.y 를 실행합니다. 이 명령은 API 서버 도달 여부·모든 노드 Ready·컨트롤 플레인 상태를 검사하고, 스큐 정책을 강제하며, 이미지를 준비하고, 컨트롤 플레인 구성요소를 교체하되 하나라도 올라오지 않으면 되돌리고, 새 CoreDNS·kube-proxy 매니페스트를 적용합니다. kubeadm 이 관리하는 인증서도 이때 갱신됩니다.
  2. 추가 컨트롤 플레인 노드에서는 kubeadm upgrade node 를 씁니다. 클러스터의 ClusterConfiguration 을 가져와 static Pod 매니페스트와 kubelet 설정을 올립니다.
  3. 각 노드의 kubelet 을 minor 업그레이드할 때는 먼저 drain 합니다. 그다음 kubelet·kubectl 패키지를 올리고, 워커에서는 kubeadm upgrade node 로 kubelet 설정을 갱신한 뒤 kubelet 을 재시작하고, kubectl uncordon 으로 되돌립니다.

문서는 컨테이너 스펙 해시가 바뀌므로 업그레이드 뒤 모든 컨테이너가 재시작된다고 미리 알려 줍니다. 실패했을 때는 같은 명령을 다시 실행해도 됩니다(멱등), 버전을 바꾸지 않은 채 kubeadm upgrade apply --force 로 상태를 맞출 수도 있고, /etc/kubernetes/tmp 아래에 etcd 와 매니페스트 백업이 남습니다.

노드를 안전하게 비우기 문서에 따르면 kubectl drain 은 파드를 우아하게 종료시키며 PodDisruptionBudget 을 존중하고, DaemonSet 파드가 있으면 --ignore-daemonsets 가 필요합니다. drain 이 성공적으로 끝났다는 것은 (제외된 시스템 파드를 빼고) 모든 파드가 안전하게 쫓겨났다는 뜻이고, 그 뒤에 kubectl uncordon 으로 스케줄링을 다시 엽니다. 한 번에 한 노드씩 실행하되 여러 터미널에서 병렬로 돌려도 PDB 는 함께 지켜집니다.

CNI·CSI·CRI — 무엇을 떼어 냈나

세 인터페이스는 이름이 비슷하지만 떼어 낸 층이 다릅니다.

인터페이스 떼어 낸 것 누가 누구를 부르나
CRI 컨테이너 런타임 kubelet 이 gRPC 클라이언트로 런타임을 부른다
CNI 파드 네트워크 구현 컨테이너 런타임이 CNI 플러그인을 적재해 파드 네트워크 모델을 구현한다
CSI 스토리지 시스템 CSI 드라이버가 스토리지를 표준 인터페이스로 노출하고, 파드는 csi 볼륨 타입으로 쓴다

CRI 문서는 CRI 가 kubelet 과 런타임 사이의 주 gRPC 프로토콜이며 v1.23 에서 안정화됐고, 1.26 부터 kubelet 이 v1 CRI API 를 요구하므로 지원하지 않는 런타임에서는 노드가 등록되지 않는다고 적습니다. 엔드포인트는 kubelet 의 --container-runtime-endpoint 로 지정합니다. 네트워크 플러그인 문서는 CNI 플러그인이 쿠버네티스 네트워크 모델을 구현하는 데 필수이고, CNI 스펙 v0.4.0 이상(권장 v1.0.0)과 호환되어야 하며, 1.24 에서 kubelet 의 cni-bin-dir·network-plugin 플래그가 제거되어 CNI 관리는 kubelet 이 아니라 런타임의 몫이 되었다고 설명합니다. 볼륨 문서의 CSI 절은 CSI 가 임의의 스토리지 시스템을 컨테이너 워크로드에 노출하는 표준 인터페이스이며, PVC 참조·generic ephemeral 볼륨·CSI ephemeral 볼륨 세 방식으로 쓸 수 있고, 이전 방식인 FlexVolume 은 1.23 부터 폐기됐다고 적습니다.

현장에서 만나는 모습

자원 압박에서만 노드가 흔들린다. 평소에는 멀쩡한데 메모리가 차면 kubelet 이 재시작되거나 파드가 이상하게 죽는 노드는 cgroup 드라이버 불일치를 의심합니다. 문서가 정확히 이 증상을 적어 두었습니다 — systemd 가 init 인데 kubelet 과 런타임이 cgroupfs 를 쓰면 cgroup 관리자가 둘이 되어 자원 뷰가 갈립니다. 문서는 클러스터에 이미 합류한 노드의 드라이버를 바꾸는 것도 민감한 작업이라고 경고하니, 합류 전에 맞추는 편이 훨씬 쌉니다.

drain 없이 kubelet 을 올렸다. 패키지만 갈아 끼우고 kubelet 을 재시작하면 그 노드의 컨테이너가 전부 재시작되는데, drain 을 하지 않았으니 PDB 도 우아한 종료도 지켜지지 않습니다. 문서가 "minor 업그레이드는 먼저 drain" 이라고 못 박은 이유입니다.

다음 퀴즈에서 확인할 것

퀴즈에서는 swap 에 대한 kubelet 의 기본 동작, cgroup 드라이버를 systemd 로 맞추는 이유, 현재 공식 문서가 요구하는 커널 설정, 업그레이드 순서, kubelet 의 허용 스큐, 그리고 CNI 플러그인을 적재하는 주체가 누구인지 묻습니다.