클러스터는 처음 어떻게 태어나나 — kubeadm이 컨트롤 플레인을 세우는 과정
클라우드에선 "클러스터 만들기" 버튼 한 번이다(EKS/GKE/AKS가 알아서 만든다). 그런데 그 뒤에서 무슨 일이 일어나는가? 한 팀이 베어메탈에 직접 클러스터를 세우려다 막혔다 — kube-apiserver, etcd, scheduler, controller-manager, CNI, 인증서를 손으로 하나씩 세팅하려니 2주가 걸렸다. kubeadm은 이 절차를 자동화한다. "버튼 한 번"이 가리키는 것이 바로 kubeadm(또는 그와 비슷한 부트스트랩 도구)이다.
이 글이 푸는 것은: 빈 서버 여러 대에서 시작해 한 클러스터가 처음 어떻게 태어나는가다. 핵심은 kubeadm이 인증서·etcd·컨트롤 플레인·CNI·노드 참여를 순서대로 어떻게 엮는가다.
kubeadm이 자동화하는 것들 — 손으로 하면 무서운 일들
클러스터 부트스트랩에 필요한 것들:
- 인증서/키: apiserver·etcd·kubelet 서로를 신뢰하게 만드는 X.509 인증서와 CA.
- etcd 클러스터: 상태 저장소 시작 (정적(정적) 파드로).
- 컨트롤 플레인 컴포넌트: apiserver, scheduler, controller-manager를 정적 파드로.
- kubeconfig: 각 컴포넌트가 apiserver에 접속하는 자격.
- kubelet: 각 노드에서 도는 에이전트.
- CNI: Pod 네트워크(이건 kubeadm이 안 깔아줌 — 별도 apply).
- 노드 참여(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이 하는 일:
- 토큰으로 컨트롤플레인을 인증받아 접속.
- CA 인증서를 받아 신뢰 체인 확립(이 apiserver가 진짜임을 확인).
- 이 노드의 kubelet이 apiserver에 등록 → Node 객체 생성.
- 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. 운영 알림 필수.
생각해 볼 문제
- kubeadm init 직후 노드가 NotReady인 가장 흔한 원인은? 어떻게 진단하고 고치나?
- 정적 파드 매니페스트 파일을 삭제하면 어떤 일이 벼어지나? (apiserver.yaml을 지우면?)
- join 토큰이 만료됐다. 새 노드를 합류시키려면? 명령은?
- 닭-알 문제(apiserver가 etcd를, etcd가 apiserver를 필요)를 정적 파드가 어떻게 푸는가?
- 인증서 1년 만료를 놓쳤다. 클러스터는 어떻게 되나? 복구는?
- kind가 "kubeadm과 가깝다"고 한 이유를 정적 파드 관점에서 설명하라.
참고
- Kubernetes 공식 문서 - Creating a cluster with kubeadm - 접근 2026-07-12 (init/join 절차)
- Kubernetes 공식 문서 - Static Pods - 접근 2026-07-12 (정적 파드 부트스트랩)
- Kubernetes 공식 문서 - kubeadm CNI 설치 안내 - 접근 2026-07-12
- kubeadm 인증서 관리 - kubeadm certs - 접근 2026-07-12 (만료/갱신)
- Kubernetes 공식 문서 - Highly Available Clusters with kubeadm - 접근 2026-07-12
'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 |
