콘텐츠로 이동
Study NoteCKA

kubeadm — 클러스터 설치와 노드 조인

결론부터
kubeadm init으로 컨트롤 플레인을 만들고, 네트워크를 설치한 뒤 join으로 노드를 연결한다.

클러스터를 처음 만들 때는 런타임, 컨트롤 플레인, Pod 네트워크가 차례로 준비되어야 한다. 빈 노드에서 시작해 워커 노드가 Ready가 되는 흐름을 따라간다.

지금까지의 장은 전부 클러스터가 이미 돌고 있다는 전제 위에 있었다. “선언된 상태(spec)와 실제 상태(status)의 차이를 줄이는 루프”(시작하기 전에)가 돌려면 그 루프를 돌리는 주체 — apiserver·etcd·scheduler·controller-manager·kubelet(아키텍처) — 가 먼저 어딘가에 떠 있어야 한다. 이 절은 그 주체를 맨땅에서 만드는 일이다.

이 일을 손으로 하면 인증서 수십 장을 직접 서명하고 컴포넌트마다 kubeconfig를 써야 한다. 설치의 주인공인 kubeadm — 공식 클러스터 부트스트랩 도구 — 이 그 과정을 대신해 준다. 인증서·kubeconfig·컨트롤 플레인 스태틱 Pod을 순서대로 만들어 준다. 아래 흐름의 CNI(Container Network Interface)는 Pod 네트워크를 붙이는 플러그인 규격이다.

클러스터가 만들어지는 전체 흐름

섹션 제목: “클러스터가 만들어지는 전체 흐름”

한 장으로 보면 어디서 무엇이 걸리는지가 보인다.

노드 준비부터 kubeadm init·CNI 설치·워커 join까지 클러스터 구축 순서

노란 칸(CNI)이 가장 자주 빠뜨리는 단계다. 여기가 비면 노드가 영원히 NotReady다.

노드 준비 — kubeadm 이전에 해야 할 것

섹션 제목: “노드 준비 — kubeadm 이전에 해야 할 것”

아래 명령은 systemd를 쓰는 Linux의 컨트롤 플레인·워커 노드에서 실행한다. sysctl은 실행 중인 커널의 설정을 읽고 바꾸는 도구다. Pod 트래픽을 노드가 전달하려면 네트워크 구현이 요구하는 커널 기능도 켜져 있어야 한다.

파라미터1로 설정했을 때
net.ipv4.ip_forward인터페이스 사이에서 IPv4 패킷 전달을 허용한다
net.bridge.bridge-nf-call-iptablesLinux 브리지를 통과하는 IPv4 패킷에 iptables 규칙을 적용한다
net.bridge.bridge-nf-call-ip6tables위 브리지 처리를 IPv6 패킷에 적용한다

브리지는 여러 인터페이스를 같은 네트워크로 연결하는 소프트웨어 스위치다. br_netfilter는 브리지 트래픽을 IP 필터링 경로로 보내는 커널 모듈이며, 아래에서는 sysctl 설정보다 먼저 로드한다. 필요한 모듈·파라미터는 네트워크 구현에 따라 달라지므로 실습에서는 문제에서 요구한 값을, 실제 설치에서는 선택한 CNI의 요구사항을 따른다.

  1. 스왑 끄기 — kubelet의 기본 요구사항

    터미널 창
    sudo swapoff -a
    sudo sed -i '/ swap / s/^/#/' /etc/fstab # 재부팅 후에도 유지
  2. 커널 모듈

    터미널 창
    cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
    overlay
    br_netfilter
    EOF
    sudo modprobe overlay && sudo modprobe br_netfilter
  3. sysctl — 브리지 트래픽이 iptables를 타게 하고 포워딩을 켠다

    터미널 창
    cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
    net.bridge.bridge-nf-call-iptables = 1
    net.bridge.bridge-nf-call-ip6tables = 1
    net.ipv4.ip_forward = 1
    EOF
    sudo 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 절에서 확인할 수 있다(학습용).

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.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
터미널 창
# 패키지 저장소 — 마이너 버전마다 URL이 다르다 ★★
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.35/deb/Release.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "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 update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl # 자동 업그레이드 방지

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

설치와 커널 파라미터 설정을 묶은 연습은 실전 과제에 있다.

터미널 창
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/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

init이 하는 일 —

kubeadm init이 내부에서 수행하는 일곱 단계

파란 두 칸이 트러블슈팅에서 계속 돌아오는 곳이다 — 인증서와 스태틱 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.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.31.6/manifests/tigera-operator.yaml
curl -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 # Ready
kubectl 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 list
kubeadm token create
openssl 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 해시를 구분하고, 이후 작업은 노드 유지보수에서 이어 간다.