1. 개요
kubelet은 클러스터의 모든 노드에서 도는 에이전트다. 공식 문서의 정의는 짧다 — “Pod 안의 컨테이너가 실행되고 있는지 확인하는” 것.
받는 것은 PodSpec(Pod 를 서술한 YAML/JSON)이고, 하는 일은 거기 적힌 대로 컨테이너를 띄우고 그 상태를 유지하는 것이다.
2. PodSpec
kubelet 이 받는 PodSpec 은 Pod 매니페스트의 spec: 부분이다. “이 Pod 가 어떻게 생겨야 하는가"를 적은 명세서다.
apiVersion: v1
kind: Pod
metadata:
name: web
spec: # ← 여기서부터가 PodSpec
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
restartPolicy: Always
volumes:
- name: data
emptyDir: {}
“Pod” 가 아니라 굳이 “PodSpec” 이라고 하는 이유는, Pod 객체 안에서 kubelet 이 쓰는 부분이 정해져 있기 때문이다.
| 부분 | 내용 | kubelet 은 |
|---|---|---|
metadata | 이름 · 라벨 · 네임스페이스 | 참고만 한다 |
spec | 어떻게 만들지 | 읽고 실행한다 |
status | 지금 어떤 상태인지 | 써서 보고한다 |
spec 을 읽어 만들고, 결과를 status 에 쓴다. 원하는 상태와 실제 상태가 다른 칸에 있다는 이 구조가 7절 조정 루프의 전제다.
Deployment 를 쓸 때도 PodSpec 은 그 안에 있다. spec 이 두 번 나오는데, 바깥은 Deployment 의 것이고 template 안의 것이 PodSpec 이다.
kind: Deployment
spec: # ← Deployment 의 spec — 개수, 전략
replicas: 3
template: # Pod 를 찍어낼 틀
spec: # ← PodSpec. 3개 Pod 가 전부 이걸로 만들어진다
containers:
- name: nginx
image: nginx:1.25
두 층을 다른 주체가 읽는다.
Deployment.spec replicas: 3 → 컨트롤러가 본다
Deployment.spec.template.spec containers … → 복사되어 Pod 가 되고 → kubelet 이 본다
kubelet 은 Deployment 파일 자체를 본 적이 없다. 컨트롤러가 template 을 복사해 만든 Pod 객체만 받는다. 그래서 replicas 는 kubelet 입장에서 존재하지 않는 값이다.
3. 관리 범위
공식 문서가 한 문장으로 선을 긋는다.
“The kubelet doesn’t manage containers which were not created by Kubernetes.”
같은 노드에서 docker run 으로 직접 띄운 컨테이너는 kubelet 이 모른다. 죽어도 되살리지 않고, 지표도 수집하지 않는다. 자기가 만든 것만 돌본다.
4. PodSpec 의 세 가지 출처
대부분은 API 서버에서 오지만, 공식 문서는 셋을 나열한다.
| 출처 | 설명 | 갱신 주기 |
|---|---|---|
| API 서버 | 일반적인 경로. 스케줄러가 배정한 Pod | watch — 즉시 |
| 파일 | 지정 디렉터리의 매니페스트를 읽는다 | 기본 20초 |
| HTTP 엔드포인트 | 지정 URL 을 주기적으로 확인 | 기본 20초 |
뒤의 둘로 만들어진 Pod 를 static Pod 라고 한다. API 서버를 거치지 않으므로 API 서버가 죽어 있어도 뜬다.
컨트롤 플레인 자체가 이 방식으로 뜬다. kube-apiserver, etcd, kube-scheduler 가 보통 /etc/kubernetes/manifests/ 안의 파일로 실행된다.
닭과 달걀 문제
API 서버를 띄우려면 → 누가 띄우나?
스케줄러가 배정해야 하는데 → 스케줄러도 아직 안 떴다
해결
kubelet 이 파일을 직접 읽어 띄운다 → API 서버 없이도 시작 가능
5. 담당 범위
각 장에서 kubelet 이 나오는 대목을 모으면 담당 범위가 드러난다.
| 일 | 내용 |
|---|---|
| 컨테이너 실행 | CRI 로 런타임(containerd 등)에 요청 |
| 재시작 | 컨테이너가 죽으면 restartPolicy 에 따라 되살린다 |
| 헬스 체크 | Probe 를 주기적으로 실행하고 결과에 따라 조치 |
| 볼륨 마운트 | PV·ConfigMap·Secret 을 컨테이너 파일시스템에 연결 |
| 토큰 발급 | ServiceAccount 토큰을 Pod 가 뜰 때 즉석 발급 (etcd 에 저장하지 않음) |
| 지표 수집 | 내장 cAdvisor 가 CPU·메모리 사용량을 측정 |
| 상태 보고 | 노드와 Pod 의 현재 상태를 API 서버에 계속 알린다 |
ConfigMap 갱신도 kubelet 의 일이다. 값이 바뀌면 새 폴더를 만들고 심볼릭 링크만 바꿔치기해서, 앱이 반쯤 바뀐 파일을 보는 일이 없게 한다.
6. 컨테이너 런타임과의 경계
“kubelet 이 컨테이너를 띄운다"는 말은 절반만 맞다. kubelet 은 지시를 받는 쪽이지, 컨테이너를 만드는 기술을 갖고 있지 않다.
컨테이너를 실제로 만드는 일은 리눅스 커널 기능(namespace, cgroup)을 조작하는 것이다. 이미지를 풀고, 격리된 공간을 만들고, 그 안에서 프로세스를 시작한다. 이걸 하는 별도 프로그램이 컨테이너 런타임이고, containerd 나 CRI-O 가 그것이다.
kubelet: "nginx 이미지로 컨테이너 하나 띄워줘" ← 요청만
containerd: 이미지 받고, 격리 공간 만들고, 프로세스 시작 ← 실제 작업
식당으로 치면 kubelet 은 매니저, 런타임은 주방이다. 매니저는 주문서(PodSpec)를 보고 “이거 만들어” 하고 넘길 뿐 칼을 잡지 않는다. 대신 나온 음식이 주문서와 맞는지 확인하고, 안 맞으면 다시 시킨다.
매니저와 주방 사이의 주문 규격이 CRI(Container Runtime Interface)다. 이 규격만 지키면 어떤 런타임이든 붙일 수 있다.
6.1. Docker 지원 종료의 실체
1.24 에서 “Docker 지원이 빠진다"고 했을 때 소란이 있었지만, 실제로 빠진 것은 kubelet 과 Docker 사이의 어댑터(dockershim) 하나였다. Docker 는 CRI 를 직접 말하지 못해서 중간에 통역이 필요했는데, 그 통역을 kubelet 안에 넣어두던 것을 뺀 것이다.
전: kubelet ─(dockershim)─▶ Docker ─▶ containerd ─▶ 컨테이너
후: kubelet ─────CRI──────▶ containerd ─▶ 컨테이너
Docker 자체가 내부에서 containerd 를 쓰고 있었으니, 가운데 층을 걷어내고 바로 붙인 셈이다. kubelet 코드는 그대로였고, 노드의 런타임 설정만 바꾸면 됐다.
6.2. 노드에서 확인하기
노드에 들어가 docker ps 를 쳤는데 아무것도 안 나오면서 Pod 는 잘 돌고 있다면, 런타임이 containerd 인 것이다. kubelet 은 Docker 명령어를 모르며 알 필요도 없다.
crictl ps # CRI 규격으로 런타임에 직접 묻는다 — 어느 런타임이든 동작
crictl images
crictl 은 CRI 를 말하는 도구라 런타임이 뭐든 같은 명령으로 볼 수 있다.
7. 조정 루프
kubelet 은 컨테이너를 한 번 띄우고 끝내지 않는다. PodSpec 이 말하는 상태와 실제 상태를 계속 비교해서 어긋나면 맞춘다.
컨테이너가 죽으면 “1개 실행 중 ≠ 2개 요구” 라는 차이가 생기고, kubelet 이 그걸 메운다. 이 루프가 restartPolicy 의 실체다. Probe 실패 시 재시작하는 것도 같은 루프다 — “건강하지 않은 컨테이너"를 “없는 것"으로 치고 다시 만든다.
7.1. 컨트롤러의 루프와 다른 점
Deployment 의 replicas: 3 을 맞추는 것은 kubelet 이 아니다. 같은 “비교해서 맞춘다” 구조지만 보는 대상이 다르다.
| 컨트롤러 (ReplicaSet) | kubelet | |
|---|---|---|
| 보는 것 | 클러스터 전체의 Pod 개수 | 내 노드에 배정된 Pod 안의 컨테이너 |
| 어긋남 | “3개여야 하는데 2개” | “컨테이너 2개여야 하는데 1개 죽음” |
| 조치 | Pod 객체를 새로 만든다 | 그 Pod 의 컨테이너를 재시작한다 |
| 어디서 | 컨트롤 플레인 | 각 노드 |
PodSpec 에는 “Pod 몇 개"가 없다. 그건 Deployment 의 replicas 이고 컨트롤러가 본다. kubelet 은 자기한테 온 Pod 하나하나의 안쪽만 맞춘다.
컨테이너가 죽음 → kubelet 이 재시작 (Pod 는 그대로, RESTARTS 만 올라감)
노드가 통째로 죽음 → kubelet 도 같이 죽었으니 손쓸 수 없음
→ 컨트롤러가 "개수 부족"을 보고 다른 노드에 새 Pod
kubectl delete pod → 컨트롤러가 새로 만듦. kubelet 은 사라진 것을 정리만
두 루프가 층을 나눠 맡기 때문에, 노드 하나가 죽어도 서비스가 유지된다.
8. 장애 시 동작
kubelet 이 멈춰도 이미 돌던 컨테이너는 계속 돈다. 런타임이 실행 주체이기 때문이다. 다만 아무도 돌보지 않는 상태가 된다.
| kubelet 이 살아 있을 때 | 죽었을 때 | |
|---|---|---|
| 기존 컨테이너 | 계속 실행 | 계속 실행 (돌봄 없음) |
| 컨테이너가 죽으면 | 재시작 | 그대로 방치 |
| 새 Pod 배정 | 실행 | 아무 일도 안 일어남 |
| 노드 상태 | Ready | NotReady → 일정 시간 후 Pod 재배치 |
컨트롤 플레인은 노드가 보고를 멈추면 NotReady 로 표시하고, 시간이 지나면 그 노드의 Pod 를 다른 노드로 옮긴다.
9. 관련 문서
- Probe — kubelet 이 실행하는 헬스 체크
- PersistentVolume / PVC — kubelet 이 마운트한다
- etcd — kubelet 은 여기에 직접 접근하지 않는다
- CRI / containerd — 매니저 아래의 주방. 런타임 층이 어떻게 나뉘는지
10. 출처
정의, PodSpec 출처, 관리 범위는 공식 문서에 근거한다.
“An agent that runs on each node in the cluster. It makes sure that containers are running in a Pod.”
“The kubelet takes a set of PodSpecs that are provided through various mechanisms and ensures that the containers described in those PodSpecs are running and healthy. The kubelet doesn’t manage containers which were not created by Kubernetes.”
“Other than from a PodSpec from the apiserver, there are two ways that a container manifest can be provided to the Kubelet. File: Path passed as a flag on the command line. … HTTP endpoint: HTTP endpoint passed as a parameter on the command line.”