클러스터는 처음 어떻게 태어나나 — kubeadm이 컨트롤 플레인을 세우는 과정

클라우드에선 "클러스터 만들기" 버튼 한 번이다(EKS/GKE/AKS가 알아서 만든다). 그런데 그 뒤에서 무슨 일이 일어나는가? 한 팀이 베어메탈에 직접 클러스터를 세우려다 막혔다 — kube-apiserver, etcd, scheduler, controller-manager, CNI, 인증서를 손으로 하나씩 세팅하려니 2주가 걸렸다. kubeadm은 이 절차를 자동화한다. "버튼 한 번"이 가리키는 것이 바로 kubeadm(또는 그와 비슷한 부트스트랩 도구)이다.

이 글이 푸는 것은: 빈 서버 여러 대에서 시작해 한 클러스터가 처음 어떻게 태어나는가다. 핵심은 kubeadm이 인증서·etcd·컨트롤 플레인·CNI·노드 참여를 순서대로 어떻게 엮는가다.

kubeadm이 자동화하는 것들 — 손으로 하면 무서운 일들

클러스터 부트스트랩에 필요한 것들:

  1. 인증서/키: apiserver·etcd·kubelet 서로를 신뢰하게 만드는 X.509 인증서와 CA.
  2. etcd 클러스터: 상태 저장소 시작 (정적(정적) 파드로).
  3. 컨트롤 플레인 컴포넌트: apiserver, scheduler, controller-manager를 정적 파드로.
  4. kubeconfig: 각 컴포넌트가 apiserver에 접속하는 자격.
  5. kubelet: 각 노드에서 도는 에이전트.
  6. CNI: Pod 네트워크(이건 kubeadm이 안 깔아줌 — 별도 apply).
  7. 노드 참여(join): 워커/추가 컨트롤플레인 노드가 클러스터에 합류.

kubeadm은 1 ~ 5, 7을 자동화한다. 6(CNI)만 사용자가 직접 깔아야 한다 — 이것이 "kubeadm init 했는데 노드가 NotReady인" 현상의 원인(자주 묻는 함정).

kubeadm init — 첫 컨트롤 플레인 노드 만들기

# 컨트롤 플레인 노드에서
sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --kubernetes-version=v1.36.x
  • --pod-network-cidr: Pod IP 대역. CNI(Flannel은 보통 10.244)이 요구하는 대역과 맞춰야.
  • --kubernetes-version: 구체 버전("최신" 금지).

kubeadm init이 하는 일:

flowchart TD
    A["1. 사전 점검<br/>(컨테이너 런타임, 포트, 메모리)"] --> B["2. CA/인증서 생성<br/>(apiserver, etcd, kubelet용)"]
    B --> C["3. kubeconfig 작성<br/>(각 컴포넌트 자격)"]
    C --> D["4. etcd 정적 파드 시작"]
    D --> E["5. 컨트롤플레인 정적 파드 시작<br/>(apiserver, scheduler, cm)"]
    E --> F["6. kubelet이 이 노드를 클러스터에 등록"]
    F --> G["7. join 명령 출력<br/>(다른 노드용)"]

성공하면 마지막에 join 명령과 토큰이 출력된다:

Your Kubernetes control-plane has initialized successfully!
...
kubeadm join <control-plane-ip>:6443 --token <token> \
    --discovery-token-ca-cert-hash sha256:<hash>

그리고 사용자에게 두 가지 후속을 지시한다:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

admin.conf가 곧 kubeconfig — kubectl이 apiserver에 접속하는 자격.

정적 파드(static pod) — apiserver/etcd가 apiserver 없이 어떻게 뜨나

역설이 있다: apiserver가 etcd에 의존하는데, etcd를 띄우는 건 apiserver다. 닭과 알. 정적 파드가 이 순환을 끊는다.

kubelet은 /etc/kubernetes/manifests/ 디렉터리의 매니페스트 파일을 직접 읽어 컨테이너를 띄운다 — apiserver를 거치지 않는다. 그래서 kubelet이 etcd/apiserver 매니페스트를 읽어 그것들을 띄우고, 그 다음에야 apiserver가 살아나 다른 Pod를 관리할 수 있다.

# Kubernetes 1.36 — 정적 파드 매니페스트 위치 (컨트롤플레인 노드)
ls /etc/kubernetes/manifests/
etcd.yaml  kube-apiserver.yaml  kube-controller-manager.yaml  kube-scheduler.yaml

이 파일들이 있으면 kubelet이 항상 그 컨테이너들을 살려둔다. 죽으면 다시 띄운다 — apiserver가 죽어도.

kind도 내부적으로는 같은 원리를 쓴다 — 컨테이너 안에 kubelet이 정적 매니페스트로 컨트롤플레인을 띄운다. 그래서 kind가 "실제 부트스트랩"과 가깝다.

CNI 설치 — kubeadm이 안 해주는 유일한 단계

kubeadm init 직후 노드를 보면 NotReady다. 왜? CNI가 없어서 — Pod 네트워크를 구성할 플러그인이 없으면 kubelet은 "이 노드에 Pod 네트워크가 준비 안 됐다"고 본다.

kubectl get nodes
NAME            STATUS     ROLES           AGE   VERSION
cp              NotReady   control-plane   30s   v1.36.x

CNI 설치:

# 예: Calico 또는 Flannel (1.36 호환 버전)
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
# 또는 Flannel
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

설치 후 노드가 Ready로 바뀐다. CNI 설치를 빼먹으면 클러스터가 영원히 NotReady — kubeadm 입문의 1번 함정.

CNI 파일 URL은 항상 실측 시점의 공식 문서에서 가져올 것. (위 예는 일반적 형태이며 버전 명시 필요.) "최신" 링크를 맹신하지 말 것 — 1.36 호환성 확인.

kubeadm join — 워커/추가 노드 합류

join 명령으로 다른 노드가 클러스터에 합류한다:

# 워커 노드에서
sudo kubeadm join <control-plane-ip>:6443 --token <token> \
    --discovery-token-ca-cert-hash sha256:<hash>

join이 하는 일:

  1. 토큰으로 컨트롤플레인을 인증받아 접속.
  2. CA 인증서를 받아 신뢰 체인 확립(이 apiserver가 진짜임을 확인).
  3. 이 노드의 kubelet이 apiserver에 등록 → Node 객체 생성.
  4. kubelet이 watch 시작, 이 노드에 Pod를 받을 준비.

토큰은 만료된다(기본 24시간). 만료 후 join하려면 kubeadm token create --print-join-command로 새 토큰 발급.

join이 인증서 핀(핑거프린트)을 검증하는 이유: 중간자 공격을 막기 위해. 그래서 --discovery-token-ca-cert-hash가 있다 — "내가 연결하는 apiserver가 우리 CA 서명 인증서를 쓰는지" 확인.

멀티 컨트롤 플레인 — HA 부트스트랩의 복잡함

컨트롤 플레인 한 대는 단일 장애점이다. HA를 위해 보통 3대로 둔다. kubeadm도 멀티 컨트롤플레인을 지원하나 부트스트랩이 복잡해진다:

  • etcd를 클러스터로 띄워야(3개가 서로 합의).
  • apiserver 앞에 로드밸런서를 둬야(join 토큰이 LB를 가리키게).
  • kubeadm init--control-plane-endpoint로 LB 주소 명시.

이 복잡함 때문에 실무의 HA 클러스터는 kubeadm 직접 대신 Cluster API, kubeone, 또는 매니지드(EKS 등)를 흔히 쓴다. kubeadm은 빌딩 블록이지 턴키 HA 솔루션이 아니다.

인증서 만료 — 숨겨진 정기 작업

kubeadm이 만드는 인증서는 기본 1년 만료다. 만료되면 apiserver·etcd 접속이 안 된다. 만료 전 kubeadm certs renew로 갱신해야 한다 — 자주 잊히는 정기 작업. 운영 클러스터에선 만료 알림이 필수다.

# 만료일 확인
sudo kubeadm certs check-expiration
# 갱신
sudo kubeadm certs renew all

직접 확인하기 (kind로 유사하게)

# Kubernetes 1.36 — kind 클러스터는 kubeadm 기반
kind create cluster --image kindest/node:v1.36.x
# 노드 안의 컨트롤플레인 매니페스트 (정적 파드)
docker exec k8s-study-control-plane ls /etc/kubernetes/manifests/
etcd.yaml  kube-apiserver.yaml  kube-controller-manager.yaml  kube-scheduler.yaml

확인할 것: 정적 파드 매니페스트 4종이 있다 — kubelet이 이걸 읽어 컨트롤플레인을 띄운다.

실제 kubeadm init은 VM/베어메탈에서 실행하는 작업이라 kind로는 유사 확인만. kind 노드 컨테이너가 내부적으로 kubeadm 출력물을 재사용한다.

etcd stacked vs external — 토폴로지 선택

kubeadm HA 클러스터의 etcd를 어떻게 둘까. 두 토폴로지:

  • Stacked(적층): etcd가 컨트롤플레인 노드 각각에 정적 파드로. 노드 3대 = etcd 3개 + apiserver 3개가 같은 머신. 단순, 자원 효율. 단 etcd 장애가 컨트롤플레인 장애로 직결.
  • External(외부): etcd를 별도 머신에. 컨트롤플레인과 분리. 복잡하지만 etcd 장애 격리. 대규모/중요 클러스터에.
flowchart TD
    subgraph ST["Stacked (기본)"]
        N1["노드1: apiserver + etcd"]
        N2["노드2: apiserver + etcd"]
        N3["노드3: apiserver + etcd"]
    end
    subgraph EX["External"]
        CP["컨트롤플레인 노드들<br/>(apiserver만)"]
        ET["별도 etcd 클러스터<br/>(전용 머신)"]
        CP --> ET
    end

kubeadm 기본은 Stacked(단순). External은 별도 etcd 설정이 필요해 복잡하지만, etcd를 독립적으로 스케일/백업/장애 격리할 수 있다. etcd가 클러스터의 단일 진실 원천(03장)이므로, 그 안정성이 전체 클러스터 안정성 — External이 중요 클러스터에서 가치.

클러스터 CA 부트스트랩 — 신뢰의 씨앗

kubeadm init가 만드는 CA(인증 기관)가 클러스터 전체 신뢰의 씨앗. 이 CA로:

  • apiserver 인증서 서명.
  • kubelet 클라이언트 인증서 서명.
  • etcd 피어 인증서 서명.
  • ServiceAccount 토큰 서명.

CA가 모든 신원의 근원. 그래서 CA 개인 키 보호가 최우선 보안. CA 키가 탈취되면 — 공격자가 임의의 유효한 인증서를 발급해 클러스터 전체 신뢰 붕괴. kubeadm은 CA를 클러스터 안에 두지만, 고보안 환경에선 CA를 외부 (또는 HSM)로 옮겨 관리. 이것이 03-k8s-security 영역(07-cert-manager)과의 연결 — CA 관리가 인증서 자동화의 근간.

업그레이드 워크플로우 — 인증서 갱신과 같이

kubeadm 클러스터 업그레이드 시 인증서 갱신이 같이 일어나는 경우가 많다:

sudo kubeadm upgrade apply v1.36.x --certificate-renewal=true

업그레이드가 인증서를 재발급(만료 임박이면). 이것이 "업그레이드했더니 인증서 만료 알림이 사라졌다"의 이유. 단, 인증서 갱신 없이 업그레이드만 하면 만료 카운트다운은 계속. 두 정기 작업(버전업 + 인증서 갱신)을 같이 관리하는 것이 kubeadm 운영의 실무.

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"kubeadm init이 모든 걸 깐다" CNI는 안 깔아줌. 별도 apply 필수. 안 깔면 NotReady
"정적 파드도 apiserver가 관리한다" kubelet이 매니페스트를 직접 읽어 띄움. apiserver 없이도 동작
"join 토큰은 영원하다" 기본 24시간 만료. 만료 후 재발급 필요
"kubeadm으로 HA를 한 번에" 복잡. etcd 클러스터 + LB + endpoint 설정 필요. 빌딩 블록일 뿐
"인증서는 한 번이면 끝" 1년 만료. 정기 갱신(kubeadm certs renew) 필수
"NotReady면 kubeadm을 다시 해야" 보통 CNI 누락. CNI부터 확인

요약 — 이 글의 결론

  • kubeadm이 부트스트랩을 자동화 — 인증서·kubeconfig·etcd·컨트롤플레인·kubelet 등록을 순서대로. 손으로 하면 주 단위 작업을 분 단위로.
  • 정적 파드가 닭-알 순환을 끊는다 — kubelet이 /etc/kubernetes/manifests/를 직접 읽어 apiserver/etcd를 띄운다. apiserver 없이도 부팅 가능.
  • CNI는 kubeadm이 안 깔아준다 — 별도 apply 필수. 안 하면 노드가 영원히 NotReady. 입문 1호 함정.
  • join은 토큰 + CA 핑거프린트 검증으로 인증·신뢰 확립. 토큰은 24시간 만료. 중간자 공격 방어.
  • HA 부트스트랩은 복잡(etcd 클러스터 + LB). kubeadm은 빌딩 블록이지 턴키 아님. 실무 HA엔 Cluster API/매니지드 흔히 사용.
  • 인증서 1년 만료가 숨겨진 정기 작업. 만료 전 kubeadm certs renew. 운영 알림 필수.

생각해 볼 문제

  1. kubeadm init 직후 노드가 NotReady인 가장 흔한 원인은? 어떻게 진단하고 고치나?
  2. 정적 파드 매니페스트 파일을 삭제하면 어떤 일이 벼어지나? (apiserver.yaml을 지우면?)
  3. join 토큰이 만료됐다. 새 노드를 합류시키려면? 명령은?
  4. 닭-알 문제(apiserver가 etcd를, etcd가 apiserver를 필요)를 정적 파드가 어떻게 푸는가?
  5. 인증서 1년 만료를 놓쳤다. 클러스터는 어떻게 되나? 복구는?
  6. kind가 "kubeadm과 가깝다"고 한 이유를 정적 파드 관점에서 설명하라.

참고

'Tech Artifacts > Kubernetes' 카테고리의 다른 글

Kubernetes - 16. crd controllers  (0) 2026.07.13
Kubernetes - 15. helm  (0) 2026.07.13
Kubernetes - 13. node administration  (0) 2026.07.13
Kubernetes - 12. autoscaling  (0) 2026.07.13
Kubernetes - 11. resource qos  (0) 2026.07.13