LabHub
배우기 러닝패스 코스

KCNA — Kubernetes and Cloud Native Associate

From VMs to Containers — What We Gave Up and What We Gained

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

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

Concept map: 공유한 채 · 배포 단위와 실행 단위가 어긋난다 · 애플리케이션이 보는 세상만 · 둘 중 하나만으로는 컨테이너가 되지 않습니다.

왜 이게 필요했나

가상머신은 하드웨어를 흉내 냅니다. 게스트 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 부터 두드려 봅니다.