kubeadm — 클러스터 설치와 노드 조인
클러스터를 처음 만들 때는 런타임, 컨트롤 플레인, Pod 네트워크가 차례로 준비되어야 한다. 빈 노드에서 시작해 워커 노드가 Ready가 되는 흐름을 따라간다.
클러스터 설치
섹션 제목: “클러스터 설치”지금까지의 장은 전부 클러스터가 이미 돌고 있다는 전제 위에 있었다. “선언된 상태(spec)와 실제 상태(status)의 차이를 줄이는 루프”(시작하기 전에)가 돌려면 그 루프를 돌리는 주체 — apiserver·etcd·scheduler·controller-manager·kubelet(아키텍처) — 가 먼저 어딘가에 떠 있어야 한다. 이 절은 그 주체를 맨땅에서 만드는 일이다.
이 일을 손으로 하면 인증서 수십 장을 직접 서명하고 컴포넌트마다 kubeconfig를 써야 한다. 설치의 주인공인 kubeadm — 공식 클러스터 부트스트랩 도구 — 이 그 과정을 대신해 준다. 인증서·kubeconfig·컨트롤 플레인 스태틱 Pod을 순서대로 만들어 준다. 아래 흐름의 CNI(Container Network Interface)는 Pod 네트워크를 붙이는 플러그인 규격이다.
클러스터가 만들어지는 전체 흐름
섹션 제목: “클러스터가 만들어지는 전체 흐름”한 장으로 보면 어디서 무엇이 걸리는지가 보인다.
노란 칸(CNI)이 가장 자주 빠뜨리는 단계다. 여기가 비면 노드가 영원히 NotReady다.
노드 준비 — kubeadm 이전에 해야 할 것
섹션 제목: “노드 준비 — kubeadm 이전에 해야 할 것”아래 명령은 systemd를 쓰는 Linux의 컨트롤 플레인·워커 노드에서 실행한다.
sysctl은 실행 중인 커널의 설정을 읽고 바꾸는 도구다. Pod 트래픽을 노드가 전달하려면
네트워크 구현이 요구하는 커널 기능도 켜져 있어야 한다.
| 파라미터 | 1로 설정했을 때 |
|---|---|
net.ipv4.ip_forward | 인터페이스 사이에서 IPv4 패킷 전달을 허용한다 |
net.bridge.bridge-nf-call-iptables | Linux 브리지를 통과하는 IPv4 패킷에 iptables 규칙을 적용한다 |
net.bridge.bridge-nf-call-ip6tables | 위 브리지 처리를 IPv6 패킷에 적용한다 |
브리지는 여러 인터페이스를 같은 네트워크로 연결하는 소프트웨어 스위치다.
br_netfilter는 브리지 트래픽을
IP 필터링 경로로 보내는 커널 모듈이며, 아래에서는 sysctl 설정보다 먼저 로드한다.
필요한 모듈·파라미터는 네트워크 구현에 따라 달라지므로
실습에서는 문제에서 요구한 값을, 실제 설치에서는 선택한 CNI의 요구사항을 따른다.
-
스왑 끄기 — kubelet의 기본 요구사항
터미널 창 sudo swapoff -asudo sed -i '/ swap / s/^/#/' /etc/fstab # 재부팅 후에도 유지 -
커널 모듈
터미널 창 cat <<EOF | sudo tee /etc/modules-load.d/k8s.confoverlaybr_netfilterEOFsudo modprobe overlay && sudo modprobe br_netfilter -
sysctl — 브리지 트래픽이 iptables를 타게 하고 포워딩을 켠다
터미널 창 cat <<EOF | sudo tee /etc/sysctl.d/k8s.confnet.bridge.bridge-nf-call-iptables = 1net.bridge.bridge-nf-call-ip6tables = 1net.ipv4.ip_forward = 1EOFsudo sysctl --system
현재 적용과 재부팅 후 유지
섹션 제목: “현재 적용과 재부팅 후 유지”커널의 현재 상태와 부팅 때 읽는 파일은 별개다. 위 예제가 파일 저장과 명령 실행을 함께 하는 이유다.
| 대상 | 지금 적용 | 다음 부팅에도 적용 |
|---|---|---|
| 커널 모듈 | modprobe br_netfilter | /etc/modules-load.d/*.conf에 모듈명 저장 |
| 커널 파라미터 | sysctl --system으로 설정 파일들을 읽어 적용 | /etc/sysctl.d/*.conf에 키 = 값 저장 |
sysctl -w net.ipv4.ip_forward=1만 실행하면 현재 값만 바뀐다. 반대로 파일만 저장하면
현재 커널 값은 바로 바뀌지 않는다. Kubernetes의 IPv4 forwarding 예제도
파일 저장 뒤 sysctl --system을 실행한다. 부팅 시 모듈 로드와 sysctl 적용의 관계는
systemd 매뉴얼 원본의
Configuration Format 절에서 확인할 수 있다(학습용).
컨테이너 런타임과 kubeadm 설치
섹션 제목: “컨테이너 런타임과 kubeadm 설치”kubelet은 컨테이너를 직접 만들지 않고 CRI(Container Runtime Interface) 런타임에 시킨다(아키텍처). 그래서 노드에는 containerd 같은 런타임이 먼저 깔려 있어야 한다.
여기서 SystemdCgroup이 나오는 이유 — cgroup(control group)은 리눅스가 프로세스의
CPU·메모리를 제한하는 커널 기능이고, 누가 그 cgroup 트리를 관리하느냐를 정하는 것이
cgroup 드라이버다. systemd를 쓰는 배포판에서 kubelet과 런타임이 서로 다른 드라이버를
쓰면 같은 노드를 두 관리자가 각자 나눠 관리하는 상태가 되어, 리소스 제한이 어긋나고
Pod이 이유 없이 죽는다. 그래서 양쪽을 systemd로 맞춘다.
# containerd 설치 후 — SystemdCgroup을 켜야 한다sudo containerd config default | sudo tee /etc/containerd/config.tomlsudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.tomlsudo systemctl restart containerd# 패키지 저장소 — 마이너 버전마다 URL이 다르다 ★★sudo mkdir -p /etc/apt/keyringscurl -fsSL https://pkgs.k8s.io/core:/stable:/v1.35/deb/Release.key \ | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpgecho "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \https://pkgs.k8s.io/core:/stable:/v1.35/deb/ /" \ | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get updatesudo apt-get install -y kubelet kubeadm kubectlsudo apt-mark hold kubelet kubeadm kubectl # 자동 업그레이드 방지Docker Engine을 쓸 때 — cri-dockerd
섹션 제목: “Docker Engine을 쓸 때 — cri-dockerd”Docker Engine은 CRI를 직접 구현하지 않는다. 예전에는 kubelet 안의 연동 코드(dockershim)가 그 사이를 이어 줬지만 v1.24에서 제거됐다. 지금 Docker Engine을 런타임으로 쓰려면 cri-dockerd라는 어댑터를 노드에 따로 설치한다 (공식 문서). kubelet은 cri-dockerd와 CRI로 대화하고, cri-dockerd가 그 요청을 Docker Engine 호출로 바꾼다.
| 항목 | 값 |
|---|---|
| 패키지 이름 | cri-dockerd |
| systemd 유닛 | cri-docker.service, cri-docker.socket |
| CRI 소켓 | unix:///run/cri-dockerd.sock |
sudo dpkg -i cri-dockerd_<버전>_amd64.deb # 받아 둔 .deb 파일을 설치sudo systemctl enable --now cri-docker.service # 부팅 등록 + 지금 시작systemctl is-active cri-docker.service패키지는 cri-dockerd 설치 문서가 안내하는 릴리스에서 받는다.
노드에 CRI 소켓이 둘 이상이면 kubeadm이 어느 것을 쓸지 정하지 못하므로 --cri-socket으로 지정한다.
sudo kubeadm init --cri-socket unix:///run/cri-dockerd.sock설치와 커널 파라미터 설정을 묶은 연습은 실전 과제에 있다.
kubeadm init
섹션 제목: “kubeadm init”sudo kubeadm init \ --pod-network-cidr=10.244.0.0/16 \ --apiserver-advertise-address=192.168.1.10 \ --control-plane-endpoint=k8s-api.example.com:6443 # HA를 계획한다면 지금 넣어야 한다
# 출력 마지막의 안내대로mkdir -p $HOME/.kubesudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configsudo chown $(id -u):$(id -g) $HOME/.kube/configinit이 하는 일 —
파란 두 칸이 트러블슈팅에서 계속 돌아오는 곳이다 — 인증서와 스태틱 Pod 매니페스트.
init이 만들어놓는 것 — 파일의 지도
섹션 제목: “init이 만들어놓는 것 — 파일의 지도”디렉터리etc/kubernetes/
디렉터리manifests/ 컨트롤 플레인 스태틱 Pod. 고치면 kubelet이 즉시 반영한다
- etcd.yaml
- kube-apiserver.yaml
- kube-controller-manager.yaml
- kube-scheduler.yaml
디렉터리pki/ 인증서와 키
- ca.crt
- ca.key
- apiserver.crt
- apiserver.key
디렉터리etcd/
- ca.crt
- server.crt
- server.key
- admin.conf 관리자 kubeconfig.
~/.kube/config로 복사한다 - kubelet.conf
- controller-manager.conf
- scheduler.conf
디렉터리var/lib/
디렉터리kubelet/
- config.yaml kubelet 설정 (staticPodPath, cgroupDriver …)
디렉터리etcd/ etcd 데이터 디렉터리
- …
디렉터리var/log/
디렉터리pods/
- …
디렉터리containers/
- …
CNI 설치 — 이걸 안 하면 노드가 NotReady
섹션 제목: “CNI 설치 — 이걸 안 하면 노드가 NotReady”CNI(Container Network Interface)는 Pod 간 네트워크를 만드는 플러그인이다.
kubeadm은 이걸 설치해 주지 않는다 — 없으면 kubelet이 NetworkReady=false로 남는다.
kubectl get nodes# NAME STATUS ROLES AGE VERSION# controlplane NotReady control-plane 1m v1.35.0
kubectl describe node controlplane | grep -A5 Conditions# Ready False ... container runtime network not ready:# NetworkReady=false ... cni plugin not initialized# 예: Calico — 현행 공식 문서의 표준은 Tigera Operator 방식이다kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.31.6/manifests/operator-crds.yamlkubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.31.6/manifests/tigera-operator.yamlcurl -O https://raw.githubusercontent.com/projectcalico/calico/v3.31.6/manifests/custom-resources.yaml# custom-resources.yaml의 ipPools[].cidr를 --pod-network-cidr와 맞춘 뒤kubectl create -f custom-resources.yaml
# 잠시 후kubectl get nodes # Readykubectl get pods -n calico-system # 오퍼레이터 방식은 kube-system이 아니라 여기 뜬다워커 노드 조인
섹션 제목: “워커 노드 조인”조인은 처음 만나는 두 쪽이 서로를 믿을 근거를 만드는 일이다. 새 노드는 apiserver에게 “나를 이 클러스터의 노드로 등록해 달라”고 해야 하는데, 아직 아무 인증서도 없다. 그래서 두 값이 오간다 —
- 토큰(
--token) — 새 노드가 자신을 증명하는 임시 암호. 이걸로 최소 권한의 부트스트랩 kubeconfig를 받아 정식 kubelet 인증서를 발급받는다 - CA 해시(
--discovery-token-ca-cert-hash) — 반대 방향의 증명이다. 새 노드가 “지금 접속한 이 apiserver가 진짜 그 클러스터인가”를 확인하는 지문 — 가짜 apiserver에 조인해 버리는 것을 막는다
# init 출력에 있던 명령을 워커에서 실행sudo kubeadm join 192.168.1.10:6443 \ --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:1234...토큰은 파일이 아니다 — admin.conf 같은 kubeconfig는 노드의 파일이지만, 부트스트랩 토큰은
클러스터 안 kube-system 네임스페이스의 Secret으로 저장된다. 그래서 컨트롤 플레인에서
kubeadm token list로 조회되고, kubeconfig를 복사하는 일과는 아무 관련이 없다.
토큰은 24시간 뒤 만료된다. 잃어버렸다면 —
# 조인 명령을 통째로 다시 만든다 ★ 이게 제일 편하다kubeadm token create --print-join-command
# 개별로 만들 때kubeadm token listkubeadm token createopenssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt \ | openssl rsa -pubin -outform der 2>/dev/null \ | openssl dgst -sha256 -hex | sed 's/^.* //'- kubeadm init으로 컨트롤 플레인을 만들고, 네트워크를 설치한 뒤 join으로 노드를 연결한다.
- init 이후 CNI 설치와 Node Ready를 확인한다.
- 노드 조인 토큰과 CA 해시를 구분하고, 이후 작업은 노드 유지보수에서 이어 간다.