지난 아티클에서 kubectl apply 명령어를 실행하면 Kubernetes 클러스터의 컨트롤 플레인 내부에서 각 컴포넌트가 어떤 일을 하는지 살펴봤다.
혹시 Pod 생성을 위한 컨트롤 플레인 내부 컴포넌트들의 상호작용을 복습하고 싶다면 지난 아티클을 살펴보고 오는 것을 추천한다. 마무리 섹션에 전체적인 워크플로우를 정리해두었다.
이번 아티클에서는 Kubernetes 클러스터의 워커 노드가 컨트롤 플레인으로부터 생성할 Pod의 Spec을 전달받아 컨테이너를 실행하는 과정을 알아보려 한다.
Kubernetes의 가장 기본 단위 워크로드인 Pod가 실제로 어떻게 생성되는지 흐름과 함께 살펴보고, 각 단계의 디버깅 포인트도 짚어볼 예정이므로 클러스터 운영을 더욱 깊이 있게 수행하는 데에 도움이 될 것이다.
1. Kubelet의 핵심 역할
Kubernetes 클러스터의 각 워커 노드에서 Pod 생성의 중추 역할을 수행하는 에이전트가 바로 kubelet이다.
클러스터를 구성하는 모든 노드에 설치되는 kubelet은 주기적으로 컨트롤 플레인의 k8s API Server를 살펴본다. 그러다 (k8s Scheduler에 의해) 자신의 노드 이름이 할당된 새로운 Pod Spec이 나타나면 이를 감지하여 로컬로 가져오고, 해당 Spec에 맞는 컨테이너가 실행되도록 로컬에 설치된 컨테이너 런타임(예: containerd)과 통신한다.
kubelet은 단순히 Pod Spec을 한 번 가져오고 전달하는 것으로 끝내지 않는다. k8s API Server로부터 전달받은 Pod Spec과 자신의 노드에서 실제로 실행 중인 컨테이너의 상태를 끊임없이 비교한다. 이를 Sync Loop(동기화 루프)라고 한다.
즉, Desired State(바라는 상태)와 Actual State(현재 상태)를 계속 비교하다가 두 상태 간의 차이를 발견했다면 조정하는 것이다. Spec 상 계속 실행되어야 하는 컨테이너가 노드 내에서는 Fail 상태라면, kubelet이 이를 알아차리고 Spec에 명시된 대로 재시작하는 등 조치를 취하는 것이다.
kubelet이 이렇게 컨테이너의 상태를 알 수 있는 것은 kubelet 내부에 있는 PLEG(Pod Lifecycle Event Generator)라는 모듈 덕분이다. 이름에서 알 수 있듯이 컨테이너의 상태를 Pod 레벨의 이벤트로 생성하는 역할을 수행하는데, PLEG가 생성하는 Pod 상태 관련 이벤트가 kubelet의 Sync Loop에서 현재 Pod 상태로 참조된다.

2. CRI(Container Runtime Interface)와 컨테이너 런타임
위 섹션에서 kubelet이 컨테이너 생성을 위해 컨테이너 런타임과 통신한다고 언급했었다. 이때 kubelet은 컨테이너 런타임과 직접 소통하지 않고 중간에 표준 규격 인터페이스를 둔다. 이를 CRI(Container Runtime Interface)라고 한다.
왜 직접 컨테이너 런타임과 통신하지 않고 중간에 인터페이스를 추가로 두는지 궁금할 수 있다. CRI의 등장 배경은 결합도와 깊은 연관이 있다.
과거 Kubernetes는 Docker에 의존적이었다. 하지만 Docker에 포함된 기능들이 Kubernetes 입장에서는 오히려 불필요하고 무거웠다. 게다가 이후 containerd와 같은 다른 컨테이너 런타임들이 등장하면서 이를 지원할 필요도 생겼다. 그로 인해 어떤 런타임이든 표준만 지키면 연동할 수 있도록 CRI라는 추상화 계층을 도입한 것이다.
kubelet은 CRI에 맞춰 정의된 gRPC 프로토콜을 통해 컨테이너 런타임에 아래와 같은 요청을 순차적으로 보낸다.
- 컨테이너 실행에 필요한 이미지 확인 후 없으면 레지스트리에서 다운로드
- 컨테이너의 샌드박스(보안을 위해 호스트와 논리적으로 분리된 영역) 준비 및 컨테이너 생성
- 생성된 컨테이너 프로세스를 실제로 실행
아마 CRI를 보면서 OCI(Open Container Initiative)가 떠오를 수도 있을 것이다. 실제로 이 둘은 컨테이너를 위한 표준 규격인 점에서는 같지만, 활동하는 영역이 다르다. 더 이상 헷갈리지 않도록 제대로 짚고 넘어가자.
먼저 CRI는 kubelet과 컨테이너 런타임 사이의 통신 규약이다. 주로 Pod를 위한 샌드박스 생성/삭제, 컨테이너 이미지 및 컨테이너 관리 등에 사용된다. CRI를 지원하는 대표적인 도구가 바로 containerd이며, 이를 고수준 런타임이라고 부르기도 한다.
반면 OCI는 컨테이너 런타임과 운영체제 간의 규격이다. 리눅스 커널의 기능을 사용하여 프로세스를 격리하고 실행할 때 사용된다. OCI를 지원하는 대표적인 도구로 runc가 있는데, 이를 저수준 런타임이라고 부르기도 한다.
지금까지 살펴본 구성요소를 흐름으로 정리하자면 아래와 같다.

3. CNI(Container Network Interface)와 CSI(Container Storage Interface)
컨테이너가 실행되었다고 해서 Pod 생성이 끝난 것은 아니다. Pod가 다른 Pod와 통신할 수 있는 IP 주소가 필요하고, Pod가 재시작되어도 데이터가 유지될 수 있는 저장소가 필요할 수도 있다. Kubernetes는 이를 위해 CNI(Container Network Interface)와 CSI(Container Storage Interface)라는 표준 인터페이스를 사용한다.
CNI와 CSI가 등장한 배경은 CRI 등장 배경과 유사하다. 세상에는 정말 다양한 네트워크 솔루션(예: Cilium, Calico 등)과 스토리지 솔루션(NFS, Ceph 등)들이 존재한다. 이런 모든 솔루션에 대응하는 코드를 kubelet에 일일이 집어 넣는 것은 너무나 많은 시간과 노력이 소요되는 일이다.
그래서 Kubernetes에서는 표준 규격을 만들고, 각 솔루션 개발 업체에서는 그 규격에 맞는 플러그인을 만들도록 한 것이다.
CNI에 대해 먼저 살펴보자. kubelet은 컨테이너가 생성되면 네트워크 설정이 필요함을 감지하고, 노드에 설치된 CNI 대응 플러그인을 호출한다. 그럼 해당 플러그인은 아래와 같은 작업을 수행하는 것이다.
- 클러스터 전체 네트워크 대역에 맞는 IP를 할당
- 호스트와 컨테이너 사이에 가상 네트워크 인터페이스를 생성 후 연결
- 해당 Pod가 다른 노드의 Pod와 통신할 수 있도록 라우팅 테이블 구성
다음은 CSI의 차례다. Pod Spec에 PVC(Persistent Volume Claim)가 정의되어 있다면 kubelet은 노드에 설치된 CSI 대응 플러그인을 통해 볼륨을 연결한다. 그 과정은 아래와 같다.
- (필요할 경우) 외부 스토리이지에 실제 볼륨 생성(Provisioning)
- 생성된 볼륨을 해당 워커 노드에 연결 (Attaching)
- 노드에 연결된 볼륨을 컨테이너 내부의 특정 경로에 마운트 (Mounting)
4. Health Check와 Status 업데이트
CNI와 CSI 플러그인의 작업까지 정상적으로 마치면 노드 입장에서 해당 컨테이너는 Running 상태가 된다. 하지만 Kubernetes 입장에서 이 Pod가 정상 동작한다고 인식하기 위해서는 몇 가지 과정이 더 필요하다.
만약 Pod Spec에 Startup Probe가 명시되었다면 해당 체크가 통과되어야 컨테이너가 동작을 이어가게 된다. Pod Spec에 Readiness Probe가 명시되어있다면 kubelet은 해당 체크까지 모두 통과된 것을 확인한 이후에 k8s API Server에 Pod가 실행 중(Running)이고 준비되었다(Ready)는 것을 알린다.
k8s API Server로 들어온 Pod 상태 정보가 etcd에 기록되면, 비로소 우리는 kubectl get pod 명령어로 해당 Pod의 상태가 STATUS: Running, READY: 1/1이라는 것을 확인할 수 있다.
마무리
우리는 k8s API Server와 kubelet 간의 관계, CRI와 컨테이너 런타임, CNI와 CSI에 대해서까지 살펴봤다. 컨테이너와 Pod가 워커 노드 안에서 어떻게 생성되는지 흐름으로 살핀 것은 문제가 생겼을 때 제대로 고치기 위함이다. 이제 각 단계별로 장애 징후와 디버깅 포인트를 정리해보겠다.
kubelet과 k8s API Server 통신
- 증상: Pod가 특정 노드에 할당되었으나 계속
Pending상태. Events에 기록 없음 - 디버깅 포인트:
- 해당 노드에 kubelet이 살아있는가?
- kubelet 로그에 k8s API Server 연결 거부(Connection Refused)가 있는가?
- 노드 인증서가 만료되지는 않았는가?
CRI 및 컨테이너 런타임
- 증상: Pod가
ContainerCreating상태로 멈췄거나ImagePullBackOff오류 발생 - 디버깅 포인트:
- 해당 노드에서
crictl ps또는crictl images명령어가 응답하는가? (컨테이너 런타임 확인) - 해당 노드의 컨테이너 런타임 로그 확인
- 해당 노드에서
CNI
- 증상: Pod가
Running상태이지만 IP가 할당되지 않음, 혹은NetworkNotReady오류 발생 - 디버깅 포인트:
- CNI 플러그인이 정상 동작 중인가?
- 해당 노드에 CNI 설정 파일이
/etc/cni/net.d/에 올바르게 존재하는가?
CSI
- 증상: Pod가
ContainerCreating상태에서FailedMount또는FailedAttachVolume이벤트가 반복 - 디버깅 포인트:
- 클라우드 제공자의 스토리지 서비스 사용 시 권한 문제 확인
- 실제 노드 내부 경로에 마운트 포인트가 생성되었는지 확인
위와 같이 각 단계별로 문제가 생겼을 때 어떤 증상이 보이는지, 문제 원인을 어떻게 찾을 수 있는지 디버깅 포인트까지 짚어봤으므로 앞으로 Kubernetes 클러스터를 더욱 깊이 있게 운영하는 데에 도움이 될 것이다.
AI와 협업하며 클러스터를 운영하는 경우가 점점 더 많아지고 있다. 이럴 때일수록 본 아티클에서 다룬 내용처럼 튼튼한 기본기를 쌓아야 AI와 함께 일하고 AI의 작업을 검토할 수도 있을 것이다