컨테이너 내부 구조 정리
커널 격리 장치인 namespace와 cgroup부터 gVisor, Kata 같은 샌드박스 런타임까지 컨테이너를 이루는 층을 표로 정리했다.
컨테이너란
컨테이너(Container)란 호스트 커널을 그대로 공유하면서 namespace로 격리하고 cgroup으로 자원을 제한한 프로세스
- 격리(Isolation): namespace가 프로세스에 보이는 자원 범위를 분리
- 자원 제한(Resource Limit): cgroup이 CPU·메모리·IO 사용량에 상한 부여
- 이미지(Image): 애플리케이션과 의존성을 레이어(layer)로 묶은 읽기 전용 패키지
- 실행: 이미지를 풀어 rootfs를 만들고 격리된 프로세스로 기동
- 커널: 별도 게스트 커널 없이 호스트 커널을 그대로 사용
VM과의 차이
가상머신은 게스트 OS와 커널을 통째로 얹고, 컨테이너는 커널을 공유한 프로세스 하나라는 점이 갈림
| 비교 항목 | 가상머신(VM) | 컨테이너 |
|---|---|---|
| 격리 단위 | 게스트 OS 전체 | 프로세스 |
| 커널 | 게스트 커널 별도 | 호스트 커널 공유 |
| 기동 | 초 단위 부팅 | 즉시 기동 |
| 오버헤드 | OS 사본만큼 큼 | 프로세스 수준 |
| 이미지 크기 | GB 단위 | MB~수백MB |
| 격리 경계 | 하이퍼바이저 | 커널 namespace |
| 집적도 | 호스트당 수십 대 | 호스트당 수백~수천 개 |
격리 기술 정리
격리는 namespace, 자원 제한은 cgroup, 두 커널 기능이 컨테이너의 토대
namespace 종류
프로세스가 보는 자원 종류마다 별도 namespace로 나뉘며, 커널 5.6부터 8종으로 확정
| namespace | CLONE 플래그 | 격리 대상 |
|---|---|---|
| Mount(mnt) | CLONE_NEWNS | 마운트 포인트·파일시스템 뷰 |
| PID | CLONE_NEWPID | 프로세스 ID 공간 |
| Network(net) | CLONE_NEWNET | 네트워크 장치·스택·포트 |
| IPC | CLONE_NEWIPC | System V IPC·POSIX 메시지 큐 |
| UTS | CLONE_NEWUTS | 호스트명·NIS 도메인명 |
| User | CLONE_NEWUSER | UID·GID 매핑 |
| Cgroup | CLONE_NEWCGROUP | cgroup 루트 디렉터리 뷰 |
| Time | CLONE_NEWTIME | boot·monotonic 시계 |
- Time namespace가 마지막으로 5.6(2020)에 합류해 8종 완성
- User namespace로 컨테이너 내부 root를 호스트 비특권 UID에 매핑 → 권한 격리 강화
- 런타임은 clone()·unshare()로 이 namespace들을 조합해 컨테이너 생성
cgroup v1 vs v2
자원 제어 계층을 컨트롤러별로 흩어놓던 v1에서, 하나의 트리로 합친 v2로 이동
| 비교 항목 | cgroup v1 | cgroup v2 |
|---|---|---|
| 계층 구조 | 컨트롤러별 독립 계층 | 단일 통합 계층(unified hierarchy) |
| 프로세스 배치 | 임의 노드 가능 | 리프(leaf) cgroup에만 배치 |
| 컨트롤러 | 서브시스템별 분리 마운트 | cpu·io·memory·pids 통합 |
| 인터페이스 | 컨트롤러마다 상이 | min·low·high·max 일관 명명 |
| freezer | 별도 서브시스템 | cgroup.freeze로 통합 |
| 기본값 | 초기 배포판 기본 | 현재 최신 배포판·systemd 기본, systemd 258부터 v1 지원 제거 |
- 최신 배포판과 systemd는 v2를 기본으로 이동, RHEL 10은 v2 전용
- 쿠버네티스도 v2 기준으로 정렬 → 노드 커널·배포판 버전 확인 필요
런타임 계층 구조
docker 명령 한 줄이 실제 컨테이너 프로세스가 되기까지 여러 계층을 거침
graph LR
CLI["docker CLI"] --> D["dockerd<br>(Docker 데몬)"]
D --> C["containerd<br>(고수준 런타임)"]
C --> S["containerd-shim<br>(컨테이너별 상주)"]
S --> R["runc<br>(저수준 런타임)"]
R --> P["컨테이너 프로세스<br>namespace+cgroup"]
| 계층 | 역할 |
|---|---|
| docker CLI | 사용자 명령 파싱 후 dockerd에 API 요청 |
| dockerd | 이미지 빌드·네트워크·볼륨 등 상위 기능, 실행은 containerd에 위임 |
| containerd | 이미지 pull·스냅샷·컨테이너 수명주기 관리(고수준 런타임) |
| containerd-shim | 컨테이너별 상주 프로세스, 데몬 재시작에도 컨테이너 유지 |
| runc | namespace·cgroup 세팅 후 컨테이너 프로세스 exec(저수준 런타임) |
OCI 스펙
표준화 주체는 OCI(Open Container Initiative), 스펙 3종으로 계층 간 인터페이스를 규정
| 스펙 | 정의 대상 |
|---|---|
| image-spec | 이미지 매니페스트·레이어·config 포맷 |
| runtime-spec | 디스크에 푼 filesystem bundle 실행 방식 |
| distribution-spec | 레지스트리 이미지 배포 API |
- runc는 runtime-spec의 참조 구현(reference implementation), 1.0.0은 2021-06-22 릴리스
- containerd는 직접 실행하지 않고 OCI bundle을 만들어 runc·kata에 위임
- 흐름: 이미지 pull → snapshotter로 레이어 언팩 → OCI bundle 생성 → runc가 컨테이너 생성
Docker vs containerd 차이점
Docker는 빌드·CLI·API까지 포함한 상위 플랫폼, containerd는 그 안에 든 코어 런타임
| 비교 항목 | Docker | containerd |
|---|---|---|
| 범위 | 빌드·CLI·네트워크·볼륨·API 포함 플랫폼 | 이미지·컨테이너 수명주기 중심 코어 런타임 |
| 계층 | containerd를 내부에 포함(상위) | Docker·k8s가 호출하는 하위 런타임 |
| CRI 지원 | 자체 미구현(dockershim 필요) | CRI 플러그인 내장(containerd 1.1+) |
| k8s 위치 | 1.24부터 kubelet 직접 미지원 | kubelet이 CRI로 직접 호출 |
| 데몬 | dockerd | containerd(+shim) |
| 기본 CLI | docker | ctr(디버깅)·nerdctl(docker 호환) |
| 이미지 | OCI 이미지 | 동일 OCI 이미지(호환) |
dockershim 제거
Docker Engine이 CRI를 구현하지 않아 kubelet에 붙이던 어댑터가 dockershim, 이 어댑터가 1.24에서 제거됨
| 항목 | 내용 |
|---|---|
| 배경 | Docker Engine이 CRI 미구현 → kubelet에 dockershim 어댑터 필요 |
| deprecated | 쿠버네티스 1.20(2020)에서 사용 중단 예고 |
| 제거 | 1.24(2022, Stargazer)에서 dockershim 제거 |
| 대안 | containerd·CRI-O 직접 사용, 또는 cri-dockerd 어댑터로 Docker Engine 유지 |
| 오해 | "Docker 이미지 못 쓴다"가 아님 → 이미지는 OCI 표준이라 그대로 동작 |
도구 정리
같은 containerd를 두고도 목적별로 CLI가 갈리며, k8s 노드 디버깅은 crictl이 표준
| 도구 | 커뮤니티 | 용도 |
|---|---|---|
| docker | Docker | dockerd 대상 종합 CLI |
| ctr | containerd | containerd 직접 제어·디버깅, 기능 최소 |
| nerdctl | containerd | docker 호환 명령, compose·rootless 지원 |
| crictl | Kubernetes | CRI 런타임(containerd·CRI-O) 공통 디버깅, Pod 단위 조회 |
이미지와 스토리지
이미지는 읽기 전용 레이어 스택, 컨테이너는 그 위에 쓰기 레이어를 얹은 상태
- 레이어: Dockerfile 명령 단위로 나뉜 읽기 전용 tar, 해시로 식별·재사용
- 공유: 여러 이미지가 동일 하위 레이어를 공유 → 저장·전송량 절감
- overlayfs: 읽기 전용 레이어 위에 쓰기 레이어를 겹쳐 단일 뷰로 제공하는 유니온 파일시스템
- copy-on-write: 하위 레이어 파일 수정 시 upperdir로 복사 후 변경 → 원본 레이어 불변
- 컨테이너 삭제: upperdir(쓰기 레이어)만 사라지고 이미지 레이어는 보존
| overlayfs 구성 | 역할 |
|---|---|
| lowerdir | 이미지 레이어(읽기 전용), 여러 겹 가능 |
| upperdir | 컨테이너 쓰기 레이어(변경분 저장) |
| merged | lower+upper 병합 뷰, 컨테이너가 보는 rootfs |
| workdir | overlay 내부 원자적 작업용 디렉터리 |
이미지 구성
OCI 이미지는 매니페스트(manifest)가 config와 레이어 blob을 다이제스트로 참조하는 구조
| 구성 | 역할 |
|---|---|
| manifest | config·레이어 목록과 다이제스트(digest) 참조 |
| config | 엔트리포인트·env·레이어 순서 등 실행 메타데이터 |
| layer blob | 파일시스템 변경분을 담은 tar 아카이브 |
| index | 멀티 아키텍처용 매니페스트 목록(선택) |
- 다이제스트: 콘텐츠 해시(sha256)로 식별 → 같은 레이어는 레지스트리·노드에서 한 번만 저장
- 태그(tag)는 매니페스트를 가리키는 가변 포인터, 실제 무결성은 다이제스트가 보장
런타임 선택지
저수준 런타임은 OCI runtime-spec만 지키면 교체 가능, 격리 강도와 성능이 트레이드오프
| 런타임 | 격리 방식 | 특징 |
|---|---|---|
| runc | namespace+cgroup(호스트 커널 공유) | OCI 참조 구현, 오버헤드 최소·기본값 |
| gVisor | 유저스페이스 커널(Sentry)로 시스템콜 가로채기 | 격리 강함, 시스템콜·IO 오버헤드 큼 |
| Kata Containers | 경량 VM(하이퍼바이저 KVM) per Pod | 커널 분리로 격리 최상, Pod당 130~200MB·기동 지연 가산 |
- runc는 커널을 공유하므로 커널 취약점이 곧 탈출 경로 → 격리 강화가 필요하면 gVisor·Kata 선택
- 런타임 교체는 쿠버네티스 RuntimeClass로 워크로드별 지정 가능
결론
- 컨테이너는 namespace(격리)·cgroup(제한)·레이어 이미지로 조합한 격리 프로세스
- docker CLI→dockerd→containerd→shim→runc 계층을 거쳐 실제 프로세스가 됨
- 쿠버네티스는 1.24에서 dockershim을 제거 → containerd가 CRI로 kubelet에 직결
- Docker는 "빌드까지 포함한 상위 플랫폼", containerd는 "그 안의 코어 런타임"으로 이해하면 됌