앞 글에서 쿠버네티스의 핵심 기능으로 스케줄링, 셀프힐링, 오토스케일링을 꼽았는데, 정작 이걸 누가 어떻게 수행하는지는 짚지 않았습니다. 트러블슈팅이나 클러스터 설계 단계에서 막히지 않으려면 내부 구조를 알아둘 필요가 있습니다.
이 글에서는 쿠버네티스 클러스터의 전체 구조와 각 컴포넌트의 역할을 정리합니다.
클러스터의 두 영역 — 컨트롤 플레인과 워커 노드
쿠버네티스 클러스터는 크게 컨트롤 플레인(Control Plane) 과 워커 노드(Worker Node) 로 나뉩니다.
컨트롤 플레인은 클러스터의 두뇌입니다. "Pod를 어디에 띄울지", "지금 상태가 원하는 상태와 맞는지", "새 요청이 들어왔는데 권한은 있는지" 같은 판단을 내리는 곳이죠. 워커 노드는 실제 작업이 수행되는 곳입니다.. 컨트롤 플레인이 "여기서 이걸 실행해"라고 지시하면, 워커 노드가 컨테이너를 띄우고 관리합니다.

이 그림에서 한 가지 포인트가 있습니다. 모든 화살표가 kube-apiserver를 중심으로 연결되어 있다는 점입니다.
사용자(kubectl)도, 내부 컴포넌트(스케줄러, 컨트롤러)도, 워커 노드(kubelet)도 전부 API 서버를 거쳐 통신합니다. 컴포넌트끼리 직접 통신하는 경우는 없습니다.
처음엔 "비효율적이지 않나?" 싶었는데, 이렇게 하면 모든 접근을 한 곳에서 인증·인가·감사할 수 있어서 보안과 일관성 면에서 훨씬 낫다고 합니다.
Control-Plane 컴포넌트
kube-apiserver — 모든 요청이 지나가는 관문
kube-apiserver는 클러스터의 "현관문"입니다. kubectl apply -f deployment.yaml을 치면 그 요청이 가장 먼저 도착하는 곳이 바로 여기입니다. 그리고 이 현관문은 단순히 요청을 통과시키는 게 아니라, 4단계 검문을 거칩니다.
API 서버가 요청을 처리하는 순서는 이렇습니다.
- 인증(Authentication) — 요청한 사람이 누구인지 확인합니다. 인증서, 토큰, OIDC 등 여러 방식을 지원합니다.
- 인가(Authorization) — 그 사람이 이 작업을 할 권한이 있는지 RBAC 정책으로 확인합니다.
- 어드미션 컨트롤(Admission Control) — 요청 내용을 최종 검증하거나 수정합니다. "CPU limits가 없으면 거부한다" 같은 정책을 여기서 적용합니다.
- etcd에 저장 — 통과한 요청을 etcd에 기록합니다.
중요한 건 API 서버 자체는 상태를 저장하지 않는다(stateless) 는 점입니다. 모든 데이터는 etcd에 있고, API 서버는 요청을 받아서 검증하고 etcd에 넘기는 중계자 역할만 합니다. 그래서 API 서버를 여러 대 띄워서 로드밸런서 뒤에 두는 수평 확장이 가능합니다.
# API 서버 주소 확인
kubectl cluster-info
# 사용 가능한 API 리소스 목록
kubectl api-resources
etcd — 클러스터의 기억 장치
etcd는 클러스터의 모든 상태를 저장하는 분산 키-값 스토어입니다. Pod 목록, Service 정보, ConfigMap, Secret, 노드 상태까지 전부 여기에 들어 있습니다. etcd 데이터가 날아가면 클러스터는 말 그대로 기억상실 상태가 됩니다.
공식 문서를 찾아보니, etcd는 Raft라는 합의 알고리즘으로 여러 노드 간 데이터 일관성을 유지합니다. 과반수(quorum)가 살아 있어야 쓰기가 가능하므로, 프로덕션에서는 홀수 노드로 구성하는 게 기본입니다.
| 노드 수 | 허용 장애 노드 수 | 용도 |
| 1 | 0 | 개발·테스트 전용 |
| 3 | 1 | 소규모 프로덕션 |
| 5 | 2 | 일반 프로덕션 권장 |
4대를 넣으면 허용 장애 수가 3대일 때와 동일하기 때문에 홀수로 구성하는 것이 권장됩니다.
etcd에는 kube-apiserver만 직접 접근할 수 있고, 다른 컴포넌트가 etcd에 직접 읽고 쓰는 일은 없습니다. 이 덕분에 접근 제어와 감사 로그를 API 서버 한 곳에서 관리할 수 있습니다.
프로덕션 운영에서 가장 중요한 건 정기 백업입니다. etcd가 죽으면 클러스터 전체가 멈추기 때문입니다.
# etcd 스냅샷 백업
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot.db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key
⚠️ 주의: etcd는 쓰기마다 디스크에 즉시 반영(fsync)합니다. 디스크 지연이 크면 리더 선출 타임아웃이 발생할 수 있어서, SSD 사용이 사실상 필수입니다.
kube-scheduler — Pod를 어느 노드에 띄울지 결정
Pod를 만들면 처음에는 spec.nodeName이 비어 있는 상태로 etcd에 저장됩니다. kube-scheduler는 이런 미배치 Pod를 감지하고, "어떤 노드에서 실행하면 좋겠다"를 결정합니다.
결정 과정은 두 단계입니다.
- Filtering
먼저 실행이 불가능한 노드를 걸러냅니다. CPU/메모리가 부족한 노드, 노드셀렉터와 일치하지 않는 노드, Taint 때문에 접근할 수 없는 노드가 여기서 탈락합니다. - Scoring
남은 노드에 점수를 매겨서 가장 높은 노드를 선택합니다. 리소스가 얼마나 균등하게 분배되는지, 이미지가 이미 캐시되어 있는지 같은 요소가 점수에 반영됩니다.
스케줄러가 결정하면 해당 Pod의 spec.nodeName에 노드를 기록하고, 그 노드의 kubelet이 이를 감지해서 컨테이너를 실행합니다. 스케줄러가 직접 컨테이너를 띄우는 건 아닙니다. "어디서 실행할지"만 정하는 거죠.
Pod가 Pending 상태로 오래 머무르면 스케줄러가 배치할 노드를 못 찾은 겁니다.
# Pending 원인 확인
kubectl describe pod <pod-name>
# Events 섹션에 "0/3 nodes are available: ..." 같은 메시지가 나옵니다
kube-controller-manager — 선언적 상태를 유지하는 관리자
앞 글에서 쿠버네티스의 핵심이 "선언적 관리"라고 했는데, 그 선언을 실제로 이행하는 게 바로 kube-controller-manager입니다.
내부에는 수십 개의 컨트롤러가 들어 있고, 각각 같은 패턴으로 동작합니다. 현재 상태를 관찰하고, 원하는 상태와 비교해서, 차이가 있으면 조치를 취합니다. 이걸 컨트롤 루프(reconciliation loop) 라고 합니다.
예를 들어 ReplicaSet Controller는 "Pod 3개를 유지해라"라는 선언이 있으면, 현재 Pod 수를 확인하고, 2개밖에 없으면 1개를 더 만들고, 4개가 있으면 1개를 지웁니다. 이걸 이벤트가 발생할 때마다, 그리고 주기적으로 반복합니다.
주요 컨트롤러 몇 개만 짚어보면 이렇습니다.
| 컨트롤러 | 하는 일 |
| ReplicaSet Controller | 지정된 수의 Pod를 유지 |
| Deployment Controller | 롤링 업데이트와 롤백 관리 |
| Node Controller | 노드 상태 모니터링, 응답 없으면 NotReady 표시 |
| Job Controller | 일회성 작업 완료까지 Pod 관리 |
| EndpointSlice Controller | Service에 연결된 Pod IP 목록 관리 |
# Pod 하나를 강제로 지워보면 컨트롤러가 바로 새 걸 만드는 걸 확인할 수 있습니다
kubectl create deployment web --image=nginx --replicas=3
kubectl delete pod <pod-name>
kubectl get pods -w # 새 Pod가 즉시 생성되는 걸 볼 수 있음
이게 셀프힐링의 원리입니다. 마법처럼 보이지만 실은 컨트롤러가 루프를 돌면서 차이를 감지하고 자동으로 조치하고 있는 것 뿐입니다.
Worker Node 컴포넌트
kubelet — 노드의 Pod 관리자
kubelet은 각 워커 노드에서 돌아가는 에이전트입니다. 컨트롤 플레인이 "이 노드에서 이 Pod를 실행해"라고 결정하면, kubelet이 그 지시를 받아서 컨테이너 런타임(containerd 등)에 실제 컨테이너 실행을 요청합니다.
다른 컴포넌트는 Pod 안에서 돌아가는 것도 있는데, kubelet만은 systemd 데몬으로 노드에 직접 실행됩니다. 컨테이너를 관리하는 주체가 자기 자신도 컨테이너면 순환 참조가 되니까요.
kubelet이 하는 일은 크게 세 가지입니다.
- Pod 실행
API 서버에서 이 노드에 배치된 Pod 명세를 받아 컨테이너 런타임에 실행을 지시합니다. - 헬스 체크
Probe를 통해 컨테이너가 살아 있는지(livenessProbe), 트래픽을 받을 준비가 됐는지(readinessProbe), 아직 시작 중인지(startupProbe)를 계속 확인합니다. livenessProbe가 실패하면 컨테이너를 재시작하고, readinessProbe가 실패하면 Service 엔드포인트에서 빼버립니다. - 상태 보고
노드의 리소스 상황과 Pod 상태를 주기적으로 API 서버에 보고합니다.
# 노드에 SSH 접속해서 kubelet 상태 확인
systemctl status kubelet
journalctl -u kubelet -f
⚠️ 주의: 앱 시작 시간이 긴 경우(JVM, 모델 로딩 등) livenessProbe만 설정하면 시작 중에 실패로 판정되어 무한 재시작에 빠질 수 있습니다. startupProbe를 함께 설정해서 시작 완료를 기다린 뒤 livenessProbe가 동작하도록 구성해야 합니다.
kube-proxy — Service 트래픽을 Pod로 전달
Pod는 재시작될 때마다 IP가 바뀝니다. 그런데 다른 Pod나 외부에서 이 Pod에 안정적으로 접근하려면 고정된 주소가 필요하겠죠? 그래서 Service라는 추상화가 있고, kube-proxy가 그 Service의 고정 IP(ClusterIP)로 들어온 트래픽을 실제 Pod로 전달하는 네트워크 규칙을 관리합니다.
여기서 오해하기 쉬운 게 있는데, kube-proxy가 트래픽을 직접 중계하는 건 아닙니다. kube-proxy는 iptables나 nftables 규칙을 작성하고, 실제 패킷 처리는 OS 커널이 합니다. 그래서 kube-proxy가 잠시 재시작돼도 이미 작성된 규칙은 유지되어 트래픽이 끊기지 않습니다.
v1.36 기준으로 동작 모드는 iptables와 nftables 두 가지입니다.
| 모드 | 특징 |
| iptables (기본값) | 단순하지만 Service가 수천 개를 넘으면 선형 탐색으로 인해 성능이 떨어짐 |
| nftables (v1.33 GA) | iptables보다 성능이 우수. 대규모 클러스터에 권장 (kernel 5.13 이상 필요) |
IPVS 모드는 v1.35에서 deprecated된 후 v1.36에서 완전히 제거됐습니다.
기존에 IPVS를 쓰고 있었다면 nftables로 마이그레이션해야 v1.36 이상으로 올릴 수 있습니다.
Container Runtime — 실제로 컨테이너를 띄우는 엔진
kubelet은 컨테이너를 직접 만들지 않습니다. CRI(Container Runtime Interface)라는 표준 인터페이스를 통해 컨테이너 런타임에 요청하는 방식입니다.
대표적인 런타임은 containerd와 CRI-O가 있고, 대부분의 클러스터에서는 containerd를 씁니다.
예전에는 Docker 자체가 런타임이었는데(dockershim), v1.24에서 제거됐습니다. Docker가 내부적으로 containerd를 쓰고 있어서, 중간 계층(dockershim)을 걷어내고 containerd에 직접 연결하는 게 효율적이었기 때문입니다.
Pod가 생성되기까지의 전체 흐름
지금까지 다룬 컴포넌트가 어떻게 협력하는지, kubectl apply -f deployment.yaml을 쳤을 때의 흐름을 따라가 보겠습니다.
- kubectl → kube-apiserver에 Deployment 생성 요청 전송
- kube-apiserver → 인증·인가·어드미션 컨트롤 통과 → etcd에 Deployment 저장
- Deployment Controller → 새 Deployment 감지 → ReplicaSet 생성
- ReplicaSet Controller → ReplicaSet 감지 → Pod 오브젝트 생성 (아직 nodeName 없음)
- kube-scheduler → 미배치 Pod 감지 → 노드 선택 → spec.nodeName 기록
- kubelet (해당 노드) → 자기 노드에 배치된 Pod 감지 → 컨테이너 런타임에 컨테이너 실행 지시
- kube-proxy → Service가 있으면 해당 Pod로의 네트워크 규칙 업데이트
처음 이 흐름을 봤을 때, 단순히 "Pod를 하나 띄운다"는 것도 이렇게 여러 단계를 거치는구나 싶었습니다. 하지만 이 분리 덕분에 각 컴포넌트가 자기 역할만 하면 되고, 하나가 죽어도 나머지는 정상 동작합니다. 예를 들어 스케줄러가 잠시 죽어도 이미 배치된 Pod는 영향 없이 돌아가고, 새 Pod 배치만 지연될 뿐입니다.
정리
쿠버네티스 클러스터는 컨트롤 플레인(판단)과 워커 노드(실행)로 나뉘고, 모든 통신은 kube-apiserver를 중심으로 이루어집니다.
API 서버가 요청을 검증하고, etcd가 상태를 저장하고, 스케줄러가 배치를 결정하고, 컨트롤러 매니저가 선언적 상태를 유지하고, kubelet이 실제 컨테이너를 실행합니다.
'DevOps > kubernetes' 카테고리의 다른 글
| 6. ConfigMap과 Secret - 설정을 이미지에서 분리 (0) | 2026.06.13 |
|---|---|
| 5. Service와 Ingress - 네트워킹 정리 (0) | 2026.06.06 |
| 4. StatefulSet 부터 CronJob 까지 (0) | 2026.05.30 |
| 3. Pod에서 Deployment 까지 (0) | 2026.05.16 |
| 1. 쿠버네티스란 무엇일까? (0) | 2026.04.25 |