LabHub

KCNA — 쿠버네티스·클라우드 네이티브 입문 · 클라우드 네이티브와 컨테이너 기초 · 이론

CRI 는 왜 생겼고 shim 은 무엇을 붙들고 있나

LabHub 에서 이어서 보기

한 줄 요약

CRI 는 "쿠버네티스가 런타임에게 말을 거는 표준 문법"이고, shim 은 "containerd 가 죽어도 컨테이너는
살아 있게 붙들고 있는 얇은 프로세스"입니다. 둘 다 어댑터를 없애기 위해 만든 규약의 산물입니다.

왜 이게 필요했나

CRI 가 없던 시절 쿠버네티스가 쓸 수 있는 런타임은 Docker 하나뿐이었고, 쿠버네티스는 Docker Engine 과
대화하려고 자기 코드 안에 어댑터를 들고 있었습니다. 그게 dockershim 입니다.

이 코드는 그 시절엔 옳았습니다. 없었으면 초기 채택이 불가능했으니까요. 문제는 런타임이 늘어날 때
드러납니다. 런타임이 하나 늘 때마다 쿠버네티스 본체에 어댑터를 하나씩 더 넣어야 한다면, 유지보수 부담이
쿠버네티스 쪽에 무한히 쌓입니다.

그래서 순서를 뒤집었습니다. 어댑터를 걷어내려면 먼저 규약을 만들어야 한다. CRI(Container Runtime
Interface)라는 gRPC 인터페이스를 정의하고, 런타임 쪽이 그 인터페이스를 구현하게 했습니다.
dockershim 은 쿠버네티스 v1.24 에서 제거됐고, 공식 FAQ 는 이 코드가 애초에 임시 해법으로 의도된
것이었다고 밝힙니다. 그 자리를 containerd 와 CRI-O 가 채웠습니다.

여기서 시험에 자주 나오는 오해 하나를 끊어야 합니다. **dockershim 제거는 "Docker 이미지가 안 된다"는
뜻이 아닙니다.** 이미지 형식은 OCI Image Spec 이라 docker build 로 만든 이미지는 모든 CRI 구현에서
그대로 돕니다. 사라진 건 이미지가 아니라 kubelet 안의 어댑터 코드입니다.

어떻게 동작하나

CRI 가 제공하는 두 서비스

파드 하나가 뜰 때 실제로 벌어지는 순서

1. kubelet 이 RunPodSandbox 를 호출한다
2. containerd 가 pause 컨테이너를 만든다 — 이게 파드의 네트워크 네임스페이스 주인이다
3. CNI 플러그인이 불려 나가 그 네임스페이스에 IP 를 붙인다
4. kubelet 이 PullImageCreateContainerStartContainer 를 호출한다
5. containerd 가 shim 을 통해 컨테이너를 실행한다

pause 컨테이너가 왜 있느냐가 KCNA 단골입니다. 파드 안 컨테이너들이 IP 와 포트 공간을 공유하려면
누군가 그 네트워크 네임스페이스를 먼저 만들고 끝까지 붙들고 있어야 합니다. 앱 컨테이너가 재시작으로
전부 사라지는 순간에도 네임스페이스가 유지돼야 IP 가 안 바뀝니다. pause 는 pause() 시스템 콜로
무한 대기만 하므로 메모리를 약 1MB 밖에 쓰지 않고, PID 네임스페이스의 init 역할로 좀비 프로세스도 수거합니다.

shim — containerd 재시작과 컨테이너 수명을 끊어놓는 장치

containerd --> containerd-shim-runc-v2 --> runc --> 컨테이너 프로세스

shim 의 책임은 넷입니다.

컨테이너 프로세스의 부모는 containerd 가 아니라 shim 이고, shim 은 스스로를 데몬화해 containerd 와
다른 세션에서 돕니다. 그래서 containerd 를 재시작해도 컨테이너 프로세스의 PID 는 바뀌지 않습니다.
"컨테이너가 이상하다"고 containerd 를 재시작하는 건 대부분 아무것도 고치지 못합니다 — 관리 평면만
새로 뜰 뿐, 컨테이너를 실제로 붙들고 있는 건 shim 이기 때문입니다.

shim 은 컨테이너 종류에 따라 갈아끼웁니다. containerd-shim-runc-v2(runc), containerd-shim-kata-v2(경량 VM),
containerd-shim-runsc-v1(gVisor), containerd-shim-wasm(WebAssembly). RuntimeClass 오브젝트가
이 선택을 파드 단위로 노출한 것입니다.

현장에서 만나는 모습

저자의 홈랩은 클러스터를 재구축하면서 CNI 를 Calico 에서 Cilium 1.20.1 로 바꾸고 kube-proxy 를
아예 설치하지 않았습니다(--skip-phases=addon/kube-proxy). 그리고 그게 실제로 동작하는지를 주장이 아니라
측정으로 확인했습니다 — kube-proxy 파드 0 개, 노드의 iptables KUBE- 체인 0 개. 대신 eBPF 맵에
10.96.0.1:443/TCP → 10.0.0.120:6443/TCP 같은 매핑이 직접 들어 있었습니다.

여기서 계층 얘기가 현실이 됩니다. CNI 를 바꾸는 것과 CRI 를 바꾸는 것은 다른 층의 작업입니다.
CNI 는 3번 단계(네임스페이스에 IP 붙이기)만 담당하고, 컨테이너를 만드는 1·4·5 단계는 그대로입니다.
그래서 CNI 를 통째로 갈아도 kubelet 과 containerd 사이의 대화는 한 글자도 바뀌지 않습니다.

한 가지 더. 이 계층 분리 덕분에 exec 만 안 되는 고장이 존재합니다. exec/attach/port-forward 는
CRI gRPC 위로 흐르지 않고, containerd 가 스트리밍 URL 을 돌려주면 클라이언트가 그 URL 로 노드에
새로 접속합니다. 그래서 파드는 멀쩡히 도는데 kubectl exec 만 타임아웃 나는 상황이 생깁니다.

다음 퀴즈에서 확인할 것

이 모듈은 퀴즈로 마무리합니다. 다음 모듈부터는 실제 클러스터에 붙어, 지금까지 이야기한 조각들이
API 오브젝트로 어떻게 드러나는지 직접 봅니다.