LabHub

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

가상머신에서 컨테이너로 — 무엇을 버리고 무엇을 얻었나

LabHub 에서 이어서 보기

한 줄 요약

컨테이너는 '가벼운 가상머신'이 아닙니다. 커널을 공유한 채 리눅스의 두 기능(namespace 와 cgroup)으로
프로세스를 가둔 것이고, 그래서 빠른 대신 격리 강도는 VM 보다 약합니다. 이 한 문장이 KCNA 전 범위의 뿌리입니다.

왜 이게 필요했나

가상머신은 하드웨어를 흉내 냅니다. 게스트 OS 가 통째로 하나 더 뜨고, 부팅에 수십 초가 걸리고,
커널 이미지와 시스템 파일이 디스크와 메모리를 먹습니다. 서비스 하나를 배포하려고 운영체제 하나를
같이 배포하는 셈이었습니다.

문제는 '무거움'이 아니라 배포 단위와 실행 단위가 어긋난다는 데 있었습니다. 개발자가 만든 건
애플리케이션인데, 운영에 넘기는 건 VM 이미지였습니다. 그 사이에서 "제 노트북에서는 됩니다"가 태어납니다.

컨테이너는 질문을 뒤집었습니다. 커널은 이미 호스트에 있으니 그대로 쓰고, 애플리케이션이 보는 세상만
바꿔 주면 되지 않나? 그 '보는 세상'을 바꾸는 커널 기능이 이미 있었습니다.

어떻게 동작하나

namespace — 무엇을 보는가

| namespace | 가리는 것 | 컨테이너에서 체감되는 모습 |
| --- | --- | --- |
| PID | 프로세스 목록 | 내 프로세스가 PID 1 로 보인다 |
| NET | 네트워크 인터페이스·포트 | 컨테이너마다 독립된 IP 와 포트 공간 |
| MNT | 마운트 트리 | 이미지의 파일시스템이 루트로 보인다 |
| UTS | 호스트명 | 컨테이너 이름이 호스트명이 된다 |
| IPC | 공유 메모리·세마포어 | 다른 컨테이너의 IPC 가 안 보인다 |
| USER | UID/GID 매핑 | 안에서는 root, 밖에서는 비특권 사용자 |

cgroup — 얼마나 쓰는가

namespace 가 '시야'를 자른다면 cgroup 은 '몫'을 자릅니다. CPU 시간, 메모리 상한, 블록 IO,
PID 개수를 그룹 단위로 제한합니다. 메모리 상한을 넘으면 커널의 OOM killer 가 그 안에서 프로세스를 죽입니다.
쿠버네티스의 resources.limits.memory 는 결국 이 cgroup 값으로 내려갑니다.

둘 중 하나만으로는 컨테이너가 되지 않습니다. namespace 만 있으면 옆 컨테이너가 메모리를 다 먹어
같이 죽고, cgroup 만 있으면 서로의 프로세스와 파일이 다 보입니다.

OCI — 누가 표준을 들고 있나

초기에는 "컨테이너 = Docker" 였습니다. 벤더 하나가 형식과 실행 방식을 다 쥐고 있으면 생태계가 자라지
못하므로 OCI(Open Container Initiative)가 세 가지를 규격으로 떼어냈습니다.

런타임은 두 층이다

kubelet --(CRI/gRPC)--> containerd --(OCI Runtime Spec)--> runc --> 리눅스 커널

containerd 는 고수준 런타임입니다. 이미지를 받아 풀고, 스냅샷을 관리하고, 생명주기를 추적합니다.
runc 는 저수준 런타임입니다. config.json 을 읽어 namespace 와 cgroup 을 만들고 프로세스를 exec 한 뒤
즉시 사라집니다. 컨테이너가 도는 동안 runc 프로세스는 존재하지 않습니다.

containerd 의 설계 원칙은 네 가지로 정리됩니다 — 심플함(하나의 일만 잘한다), 플러그인 기반,
OCI 호환, gRPC API. 모든 기능이 플러그인이라 스냅샷터를 overlayfs 에서 devmapper 로 갈아끼울 수 있습니다.

12-factor — 컨테이너에 담기 좋은 앱의 모양

12-factor 는 컨테이너보다 먼저 나왔지만, 지금은 사실상 "컨테이너로 잘 굴러가는 앱의 조건"으로 읽힙니다.
KCNA 에서 자주 나오는 항목은 셋입니다.

현장에서 만나는 모습

저자의 홈랩 클러스터는 원래 런타임으로 cri-dockerd 를 쓰고 있었습니다. 클러스터를 새로 세우면서
containerd 1.7.27 로 옮겼는데, 이건 취향 문제가 아니라 계층을 하나 걷어낸 것입니다.
cri-dockerd 경로에서는 kubelet → cri-dockerd → dockerd → containerd → runc 로 중간 다리가 둘 더 있었고,
containerd 를 직접 쓰면 kubelet → containerd → runc 로 끝납니다. 같은 일을 하는데 프로세스가 둘 줄어듭니다.

또 하나. 그 홈랩은 재구축 뒤 커널을 6.14 로 맞췄습니다. 컨테이너가 커널을 공유한다는 사실이
운영에서 실제로 의미하는 게 이겁니다 — 노드 커널 버전이 곧 컨테이너가 쓸 수 있는 기능의 상한입니다.
eBPF 기반 네트워킹처럼 커널 기능에 기대는 스택은 커널이 낮으면 아예 켜지지 않습니다.

이어서 읽을 것

이 모듈에는 실습이 없습니다. 다음 읽을거리에서 kubelet 과 containerd 사이의 통역사인 CRI 를 보고,
그다음 모듈에서 진짜 클러스터에 붙어 kubectl api-resources 부터 두드려 봅니다.