클러스터의 두뇌와 손 — 컨트롤 플레인과 노드가 어떻게 갈리는가
한 클러스터에서 노드 3대가 동시에 재부팅됐다. Pod들은 전부 죽었고, 운영자는 "클러스터가 죽었다"고 보고했다. 하지만 정확히 무엇이 죽은 걸까? 재부팅된 노드 위의 kubelet은 다시 살아나자마자 apiserver에 "내가 돌아왔다, 내 담당 Pod들을 다시 띄워 달라"고 물었고, 90초 안에 Pod들이 복구됐다. 죽은 건 Pod였지 클러스터가 아니었다.
이 사례가 보여주는 건 Kubernetes의 가장 중요한 분리 — 결정하는 층(컨트롤 플레인)과 실행하는 층(노드) — 이다. 이 분리를 이해하면 "무엇이 죽었는지, 그래서 무엇이 복구되는지"를 정확히 읽을 수 있다. 이 글은 그 두 층이 정확히 어떤 부품으로 이뤄지고 어떻게 통신하는지를 다룬다.
컨트롤 플레인 — 결정하는 층의 네 부품
컨트롤 플레인(control plane)은 클러스터의 "두뇌"다. 사용자의 선언을 받아들이고, 현재 상태를 저장하고, 무엇을 어디에 둘지 결정한다. 이 일을 하는 부품이 네 개다.
flowchart TD
USER["사용자 / kubectl"] --> APIS["kube-apiserver"]
APIS <-->|"읽기/쓰기"| ETCD["etcd<br/>상태 저장소"]
APIS --> SCHED["kube-scheduler<br/>Pod를 어느 노드에?"]
APIS --> CM["kube-controller-manager<br/>내장 컨트롤러 묶음"]
APIS -. watch .-> CM
APIS -. watch .-> SCHED
kube-apiserver — 모든 것의 관문
kube-apiserver는 클러스터에 가하는 모든 읽기·쓰기가 지나가는 유일한 관문이다. kubectl이든, 컨트롤러든, kubelet이든 — 상태를 바꾸려면 반드시 apiserver를 거친다. 직접 etcd를 고치는 것은 apiserver만 허용된다.
왜 이렇게 단일화하는가? 인증(authn)·인가(authz)·어드미션 컨트롤(admission)이라는 세 검문소를 일관되게 적용하기 위해서다. 아무리 내부 컴포넌트라도 apiserver를 거치면 같은 규칙을 통과한다. 그래서 보안 모델이 한 곳에서 성립한다(03-k8s-security 영역의 핵심).
apiserver는 상태를 저장(running)만 할 뿐, 스스로 아무 결정도 하지 않는다. "replicas: 3"을 받아 etcd에 적을 뿐, 3개를 만드는 건 컨트롤러의 일이다. 이 "바보 같은 관문" 설계가 의도적이다 — apiserver가 똑똑해지면 모든 로직이 한 곳으로 몰려 단일 장애점이 된다.
etcd — 단일 진실의 원천
etcd는 모든 클러스터 상태를 저장하는 분산 키-값 저장소다. "어떤 Deployment가 있고, 어떤 Pod가 어느 노드에 있고, 어떤 Service가 있다"는 사실 전부가 etcd 안에 있다./apiserver를 제외한 그 누구도 직접 etcd를 건드리지 못하므로, etcd는 사실상 단일 진실의 원천(single source of truth)이다.
etcd는 RAFT 합의 알고리즘으로 다중 복제된다. 보통 컨트롤 플레인 노드 3대에 각각 etcd 인스턴스가 돈다. 쿼럼(과반수, 3대 중 2대)이 살아 있으면 클러스터 상태는 안전하다.
etcd가 클러스터에서 가장 중요한 컴포넌트라는 말은 과장이 아니다. Pod가 죽으면 복구되지만, etcd가 영구 손실되면 무엇을 복구해야 할지 자체를 잃는다. 그래서 etcd 정기 백업은 Kubernetes 운영의 첫 번째 생존 규칙이다(13-node-administration에서 재회).
# Kubernetes 1.36 — etcd가 돌고 있는지 (정적 파드로 배포된 경우)
kubectl get pods -n kube-system | grep etcd
확인할 것: etcd-<컨트롤플레인노드명> 정적 파드가 Running.
etcd-k8s-study-control-plane 1/1 Running 0 2m
kube-scheduler — "이 Pod를 어디에?"
kube-scheduler는 아직 노드가 정해지지 않은 Pod(Pending 상태)를 보고, "이 녀석은 어느 노드에 넣는 게 최적인가?"를 결정한다. 주의 — 스케줄러는 결정만 한다. "노드 A에 넣자"고 정한 뒤, apiserver에 그 결정을 기록(Pod의 nodeName 갱신)하면 끝이다. 실제로 그 노드에 Pod를 띄우는 건 그 노드의 kubelet이다.
스케줄링 결정은 두 단계로 나뉜다:
- 필터링(Filter): 조건에 안 맞는 노드를 빼낸다. 리소스가 부족한 노드, taint가 있는 노드, 아키텍처가 안 맞는 노드.
- 점수매기기(Score): 남은 노드들에 점수를 매겨 최적을 고른다. 자원 균형, 친화도(affinity), 토폴로지 분산 등.
(스케줄링의 세부 전략 — nodeSelector, affinity, taint/toleration, topology spread — 은 08-scheduling에서 깊이 다룬다.)
kube-controller-manager — 내장 컨트롤러의 묶음
kube-controller-manager는 Kubernetes가 기본 제공하는 수많은 컨트롤러(Node Controller, ReplicaSet Controller, Endpoint Controller 등)를 하나의 프로세스로 묶어 돌린다. 각각이 별도 프로세스일 필요는 없기 때문에 묶은 것이다 — 핵심은 각 컨트롤러가 01장에서 본 조정 루프를 돌린다는 점이다.
이 중 Node Controller가 노드 장애 상황에서 핵심 역할을 한다. 노드가 일정 시간(기본 40초) 응답하지 않으면 Unknown으로 표시하고, 더 긴 시간(기본 5분)이 지나면 그 노드의 Pod를 다른 곳으로 옮길 준비를 한다. 처음의 "재부팅된 노드" 시나리오에서 Pod가 복구된 배경이다.
노드 — 실행하는 층의 세 부품
노드(node)는 결정을 실행하는 곳이다. 컨테이너를 실제로 띄우고, 네트워크 규칙을 깔고, 상태를 보고한다. 노드 하나에는 세 부품이 돈다.
kubelet — 노드의 감독관
kubelet은 각 노드에서 도는 Kubernetes의 에이전트다. apiserver를 watch하며 "이 노드에 배정된 Pod가 있나?"를 계속 묻는다. 자기에게 배정된 Pod가 생기면, 컨테이너 런타임(CRI)에 "이 컨테이너들을 만들어라"고 지시한다.
kubelet의 중요한 성질: 능동적이지 않게 결정하지 않는다. apiserver가 "이 Pod를 네 노드에 둬"라고 하면 그것을 실행할 뿐, "이 Pod를 어디 둘까"는 결정하지 않는다(그건 스케줄러). 그리고 Pod가 죽으면 컨테이너 런타임에 "다시 띄워라"고 한다 — 이것도 조정 루프다, 노드 수준의.
flowchart LR
APIS["kube-apiserver"] -. "watch: 내 담당 Pod?" .-> KL["kubelet"]
KL -->|"CRI gRPC"| RT["컨테이너 런타임<br/>containerd/CRI-O"]
RT --> C["컨테이너 실행"]
KL -->|"상태 보고"| APIS
kubelet은 Pod 상태를 apiserver에 주기적으로 보고한다. 우리가 kubectl get pod로 보는 Running, CrashLoopBackOff 같은 상태는 전부 kubelet이 보낸 것이다.
컨테이너 런타임 — 실제로 컨테이너를 만드는 자
컨테이너 런타임이 실제로 컨테이너를 만들고 실행한다. Kubernetes 1.36 기준으로 사실상 두 가지 — containerd(가장 흔함), CRI-O(레드햇 계열) — 가 쓰인다.
dockershim은 1.24에서 제거됐다. 과거 "Docker"를 런타임으로 쓰던 예시는 이제 역사다. Docker로 만든 이미지*는 여전히 containerd/CRI-O에서 돈다(OCI 이미지 표준). 혼동 포인트: "Docker를 못 쓴다"가 아니라 "Docker *엔진을 런타임으로 못 쓴다"다. 이미지는 그대로.
Kubernetes가 런타임과 소통하는 표준 인터페이스가 CRI(Container Runtime Interface)다. kubelet은 CRI gRPC API로 런타임에 "컨테이너 만들어/시작해/상태봐"라고 말한다. CRI 덕분에 Kubernetes는 런타임을 containerd든 CRI-O든 갈아끼울 수 있다.
kube-proxy — 네트워크(Service) 규칙의 설치자
kube-proxy는 각 노드에서 Service(클러스터 내부 IP)의 동작을 구현한다. Service가 가리키는 Pod 목록(Endpoints)이 바뀌면, kube-proxy가 노드의 iptables(또는 IPVS) 규칙을 갱신해 "이 Service IP로 온 트래픽은 저 Pod들로 가라"고 설정한다.
kube-proxy가 다루는 건 Service → Pod 라우팅이다. Pod 간 통신 자체는 kube-proxy가 아니라 CNI(네트워킹 영역 03-cni-model에서)가 다룬다. 이 둘을 헷갈리면 네트워크 장애를 잘못된 곳에서 찾는다 — 핵심 함정이자 02-k8s-networking 영역 전체의 출발점이다.
세 인터페이스 — CRI, CNI, CSI
Kubernetes가 각 영역(런타임, 네트워크, 스토리지)을 "인터페이스"로 뽑아낸 것이 확장성의 비밀이다. 각 인터페이스는 "이 규격만 지키면 누구나 갈아끼울 수 있다"는 약속이다.
| 인터페이스 | 다루는 것 | 구현체 예 | 언제 교체하나 |
|---|---|---|---|
| CRI (Container Runtime Interface) | 컨테이너 생성·실행·상태 | containerd, CRI-O | 런타임을 바꿀 때 |
| CNI (Container Network Interface) | Pod 네트워크(오버레이, IP 할당) | Calico, Cilium, Flannel | 네트워크 데이터플레인을 바꿀 때 |
| CSI (Container Storage Interface) | 영구 볼륨 생성/연결 | 각 클라우드/온프렘 드라이버 | 스토리지 백엔드를 바꿀 때 |
이 설계가 갖는 의미: Kubernetes 코어는 "어떤 런타임·네트워크·스토리지"를 강제하지 않는다. CRI/CNI/CSI 규격을 지키는 플러그인이면 어떤 것이든 끼워진다. 그래서 같은 Kubernetes가 AWS의 EBS와 베어메탈의 NFS를 모두 다루고, containerd와 CRI-O를 모두 쓸 수 있다. KubeEdge의 edged(엣지 kubelet)도 결국 CRI 인터페이스를 통해 엣지의 런타임을 지휘한다.
# Kubernetes 1.36, kind — 노드에 깔린 CNI 플러그인 확인
kubectl get pods -n kube-system | grep -E 'kindnet|calico|cilium'
확인할 것: kind 기본은 kindnetd. 다른 CNI(Calico/Cilium)를 쓰면 그 이름이 보인다.
kindnet-xxxxx 1/1 Running 0 3m
컨트롤 플레인과 노드의 통신 — 누가 누구를 당기는가
두 층이 어떻게 통신하는지 정확히 아는 것이 장애 추적의 기본이다. 핵심: 노드가 컨트롤 플레인을 향해 능동적으로 연결한다. (반대가 아니다.)
flowchart RL
KL["노드의 kubelet"] -->|"watch (능동 연결)"| APIS["컨트롤 플레인 apiserver"]
KP["노드의 kube-proxy"] -->|"watch"| APIS
KL -->|"상태 보고 (능동)"| APIS
kubelet과 kube-proxy는 apiserver에 능동적으로 연결(watch)해 자기 일거리를 당겨온다. 컨트롤 플레인이 노드로 "push"하지 않는다. 이 구조의 결과:
- 노드가 일시적으로 단절돼도, 이미 받아온 일거리(배정된 Pod)는 로컬에서 계속 돈다.
- 단절이 길어지면, Node Controller가 그 노드를
Unknown으로 표시하고(기본 40초), Pod들을 다른 곳으로 옮긴다(기본 5분).
이 "노드가 당겨오는" 설계가 잠시 단절돼도 Pod가 사라지지 않는 이유고, KubeEdge가 엣지 자율(오프라인에서도 동작)을 구현할 수 있는 토대다. 엣지의 kubelet은 클라우드 apiserver와 잠시 끊겨도 캐시된 일거리를 로컬에서 계속 수행한다(04-kubeedge 영역의 핵심).
직접 확인하기
# Kubernetes 1.36, kind — 컨트롤 플레인 구성 요소 보기
kubectl get componentstatuses 2>/dev/null || kubectl get cs 2>/dev/null
참고:
componentstatuses는 버전에 따라 deprecation 논의가 있었다. 1.36에서 실측 후 동작을 문서에 반영할 것.
# 노드의 역할과 런타임 확인
kubectl get nodes -o wide
확인할 것: 각 노드의 ROLES(control-plane vs <none>)와 CONTAINER-RUNTIME.
NAME STATUS ROLES AGE VERSION ... CONTAINER-RUNTIME
k8s-study-control-plane Ready control-plane 5m v1.36.x ... containerd://1.7.x
k8s-study-worker Ready <none> 5m v1.36.x ... containerd://1.7.x
# kubelet이 노드에서 실제로 돌고 있는지 (kind 노드 컨테이너 안)
docker exec k8s-study-control-plane crictl ps | head
확인할 것: kubelet이 CRI를 통해 런타임에서 컨테이너 목록을 가져온다. crictl은 CRI 진단 도구.
kind 노드가 도커 컨테이너이므로 이 명령이 동작한다. 비-kind 환경에서는 노드에 직접 접속해
crictl ps실행. 미검증 환경은 주석으로 명시.
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "컨트롤 플레인이 노드에 명령을 내린다" | 노드의 kubelet이 apiserver를 watch해 능동으로 일거리를 당겨온다. push가 아니라 pull |
| "etcd가 죽으면 Pod도 즉시 죽는다" | 실행 중 Pod는 노드에서 계속 돈다. 단 새 결정·복구가 멈춘다 |
| "스케줄러가 Pod를 노드에 띄운다" | 스케줄러는 어느 노드인지만 결정. 실행은 그 노드의 kubelet→런타임 |
| "kube-proxy가 Pod 간 통신을 담당한다" | kube-proxy는 Service→Pod 라우팅. Pod 간 통신은 CNI. 다른 층 |
| "1.24 이후엔 Docker 이미지를 못 쓴다" | OCI 이미지는 그대로. 못 쓰는 건 Docker 엔진을 런타임으로. containerd/CRI-O가 같은 이미지를 돌린다 |
| "CNI/CRI/CSI는 Kubernetes 기능이다" | 규격(인터페이스)이지 기능이 아니다. 구현은 각 플러그인이 제공 |
요약 — 이 글의 결론
- 컨트롤 플레인은 결정, 노드는 실행. apiserver(관문)·etcd(저장)·scheduler(배치 결정)·controller-manager(조정 루프 묶음)가 결정하고, kubelet(감독)·런타임(실행)·kube-proxy(Service 규칙)가 노드에서 실행한다.
- kube-apiserver는 유일한 관문. 모든 상태 변경이 지나가며 인증·인가·어드미션 검문소를 일관 적용. 직접 etcd를 고치는 건 apiserver뿐.
- etcd는 단일 진실의 원천. 가장 중요한 부품 — 손실되면 "무엇을 복구할지"를 잃는다. 백업이 생존 규칙.
- 노드가 컨트롤 플레인을 능동으로 당겨본다(watch). 잠시 단절돼도 배정된 Pod는 로컬에서 계속 돈다 — 이것이 KubeEdge 엣지 자율의 토대.
- CRI·CNI·CSI 세 인터페이스가 런타임·네트워크·스토리지를 탈부착 가능하게 만든다. Kubernetes 코어는 특정 백엔드를 강제하지 않는다.
- kube-proxy(Service 층)와 CNI(Pod 간 층)는 다른 층. 이 혼동이 네트워크 장애 추적의 가장 흔한 함정 — 02-k8s-networking의 출발점.
생각해 볼 문제
- etcd 3대 중 2대가 죽었다. 클러스터는 읽기/쓰기 어디까지 가능한가? 쿼럼 관점에서 설명하라.
- kubelet이 apiserver와 10분간 단절됐다. 그동안 그 노드의 Pod는 어떻게 되는가? 컨트롤 플레인은 그 노드를 어떻게 처리하는가?
- 스케줄러가 죽으면 어떤 일이 벌어지나? 이미 돌고 있는 Pod는? 새로 만들려는 Pod는?
- kube-proxy가 다운된 노드에서 Service로 들어오는 트래픽은 어떻게 되나? (직접 생각해 보고 02 영역에서 확인)
- 왜 Kubernetes는 컨트롤러들을 한 프로세스(controller-manager)로 묶었을까? 분리하면 어떤 장단점이 생기나?
- dockershim 제거(1.24)가 "Docker 이미지를 못 쓴다"는 오해로 번졌다. 이 오해가 생긴 기술적 이유는 무엇인가?
참고
- Kubernetes 공식 문서 - Kubernetes Components - 접근 2026-07-12 (컨트롤 플레인·노드 부품)
- Kubernetes 공식 문서 - kubelet - 접근 2026-07-12
- Kubernetes 공식 문서 - Container Runtime Interface (CRI) - 접근 2026-07-12
- Kubernetes 공식 문서 - dockershim removal FAQ - 접근 2026-07-12 (1.24 런타임 전환)
- Node Controller 동작 시간 파라미터 - kube-controller-manager flags - 접근 2026-07-12 (node-monitor-period, pod-eviction-timeout)
- etcd RAFT 합의 - etcd docs - 접근 2026-07-12
'Tech Artifacts > Kubernetes' 카테고리의 다른 글
| K8s - 03. api machinery (0) | 2026.07.13 |
|---|---|
| K8s - 01. overview (1) | 2026.07.13 |
