1.2 컨테이너 기초 다지기
Kubernetes가 실행 단위로 다루는 것은 결국 컨테이너다. 이미지가 어떻게 구성되고, 어떤 표준으로 유통되며, 노드 위에서 무엇이 그것을 실행하는지 정리해 두면 이후의 모든 Part가 편해진다. 이 절은 Kubernetes 안으로 들어가기 전에 컨테이너 쪽 지형을 한 번에 정리한다.
이미지와 레이어, 레지스트리
컨테이너 이미지는 읽기 전용 레이어를 쌓아 올린 파일시스템 묶음이다. Dockerfile의 FROM, RUN, COPY 같은 명령이 각각 레이어를 만들고, 완성된 이미지는 이 레이어들의 순서 있는 목록과 실행 설정(entrypoint, 환경 변수 등)을 담은 메타데이터로 구성된다. 레이어 구조 덕분에 같은 베이스 이미지를 쓰는 이미지들은 공통 레이어를 한 번만 저장하고 전송하며, 이미지를 다시 빌드할 때도 바뀐 레이어부터만 다시 만든다.
컨테이너를 실행하면 이 읽기 전용 스택 위에 쓰기 가능한 레이어가 하나 추가된다. 컨테이너 안에서 파일을 바꿔도 이미지는 변하지 않고, 컨테이너를 지우면 그 변경도 함께 사라진다. 그래서 유지해야 할 데이터는 컨테이너 밖의 볼륨(Part VI. 스토리지)으로 분리한다.
이미지는 레지스트리라는 저장소 서버를 통해 유통된다. Docker Hub, ghcr.io, 각 클라우드 벤더의 레지스트리가 여기에 해당한다. 이미지 참조는 레지스트리/저장소:태그 형태이고, 태그를 생략하면 latest로 해석된다. 태그는 같은 이름으로 나중에 다른 이미지를 가리키게 될 수 있는 가변 포인터라서, 엄밀한 재현성이 필요한 자리에서는 내용 해시로 고정되는 digest 참조(@sha256:...)를 쓴다.
OCI 표준
2015년 Docker를 비롯한 여러 회사가 OCI(Open Container Initiative)를 설립해 컨테이너의 포맷과 실행 방식을 표준화했다. 이때 Docker는 자사의 런타임 코드를 runc라는 이름으로 기증했다. 표준은 세 스펙으로 나뉜다.
| 스펙 | 정의하는 것 |
|---|---|
| image-spec | 이미지 포맷. 레이어, manifest, 설정 파일의 구조 |
| runtime-spec | 컨테이너 실행 방식. 번들 설정(config.json)과 라이프사이클 |
| distribution-spec | 레지스트리와 주고받는 push/pull HTTP API |
이 표준 덕분에 빌드 도구, 레지스트리, 런타임을 서로 다른 구현으로 조합해도 호환된다. Docker로 빌드한 이미지를 임의의 레지스트리에 올리고 containerd가 내려받아 실행하는 조합이 자연스럽게 성립하는 근거가 이 세 문서다.
런타임 지형: containerd, CRI-O, runc
노드에서 컨테이너를 실행하는 소프트웨어는 두 층으로 나뉜다. 위층의 containerd와 CRI-O는 이미지 pull, 스토리지 관리, 컨테이너 라이프사이클 관리를 담당하는 high-level 런타임이다. 아래층의 runc는 OCI runtime-spec의 참조 구현으로, 커널 기능을 호출해 실제 컨테이너 프로세스를 만드는 low-level 런타임이다. containerd와 CRI-O 둘 다 프로세스 생성 자체는 runc에 위임한다.
kubelet(각 노드에서 Pod를 관리하는 Kubernetes 에이전트)은 이 high-level 런타임과 CRI(Container Runtime Interface)라는 gRPC 인터페이스로 대화한다. CRI를 구현한 런타임이면 무엇이든 노드 런타임 자리에 들어갈 수 있다.
flowchart TD
K["kubelet"] -->|"CRI (gRPC)"| HL["containerd 또는 CRI-O"]
HL -->|"OCI runtime-spec"| R["runc"]
R -->|"namespace, cgroup"| P["컨테이너 프로세스"]
runc가 커널에서 사용하는 재료는 크게 둘이다. namespace는 프로세스가 보는 범위(PID, 네트워크, 마운트, 호스트명 등)를 격리하고, cgroup은 CPU와 메모리 같은 자원 사용량을 제한한다. 즉 컨테이너는 별도의 가상 머신이 아니라 커널이 격리해 준 일반 프로세스다. 커널 수준의 상세 동작은 이 노트의 범위 밖이므로, 여기서는 이 두 문장 요약이면 충분하다.
Docker와 Kubernetes, 그리고 dockershim
Docker는 이미지 빌드, CLI, API 데몬을 묶은 도구 모음이고, 그 내부에서 컨테이너 실행을 실제로 담당하는 부분이 containerd다. containerd는 2017년 Docker에서 CNCF로 기증돼 독립 프로젝트가 됐고, 지금은 Kubernetes 노드 런타임으로 가장 널리 쓰인다.
초기 Kubernetes에는 CRI가 없었고 kubelet이 Docker Engine을 직접 호출했다. CRI가 도입된 뒤에도 Docker Engine 자체는 CRI를 구현하지 않았기 때문에, kubelet 안에 dockershim이라는 변환 계층을 내장해 호환을 유지했다. Kubernetes 프로젝트 입장에서 이 구조는 특정 제품을 위한 예외를 코드베이스 안에 계속 유지하는 부담이었고, 결국 1.20에서 deprecated로 예고된 뒤 1.24에서 dockershim이 제거됐다. 이후의 노드는 containerd나 CRI-O를 CRI로 직접 사용한다.
개발 머신에서 Docker로 빌드하고, 레지스트리에 올리고, Kubernetes 노드의 containerd가 내려받아 실행하는 흐름은 지금도 표준적인 조합이다. Docker는 노드 런타임이라는 한 자리에서만 물러났고, 빌드와 개발 도구로는 그대로 남아 있다.
정리
이미지가 레이어 스택이라는 것, OCI의 세 스펙(image, runtime, distribution)이 포맷과 실행과 유통을 각각 표준화한다는 것, 그리고 kubelet이 CRI로 containerd나 CRI-O를 호출하고 실제 프로세스 생성은 runc가 namespace와 cgroup으로 수행한다는 것까지가 이 절의 뼈대다. 1.24의 dockershim 제거로 달라진 자리는 노드 런타임뿐이고, Docker는 빌드와 개발 도구로 여전히 그 지형 안에 있다.