Skip to content
Published on

컨테이너 완벽 가이드 — 내부 구조: Namespaces, cgroups, OCI Runtime, runc, containerd, overlayfs, seccomp, Capabilities (2025)

Authors

들어가며 — 컨테이너는 마법이 아니다

docker run nginx. 한 줄의 명령어로 Nginx가 격리된 환경에서 돌아간다. 자기 파일시스템을 가지고, 자기 PID 1을 가지고, 자기 네트워크 인터페이스를 가진다. 호스트의 다른 프로세스와 분리되어 있고, CPU와 메모리가 제한된다.

이것이 마법처럼 보이지만, 사실은 Linux 커널의 여러 기능을 정교하게 조합한 결과다. namespaces가 격리를, cgroups가 자원 제어를, OCI image spec이 배포 형식을, runc가 실행을, containerd가 라이프사이클을, overlayfs가 효율적인 파일시스템을, seccomp/capabilities가 보안을 담당한다. 이 모든 컴포넌트가 한 자리에 모여 "컨테이너"라는 추상화를 만든다.

이 글은 그 컴포넌트 하나하나를 깊이 다룬다. 1979년 chroot에서 시작해 2025년의 rootless 컨테이너까지, 컨테이너 내부 구조의 모든 것을 1,400줄로 정리한다. 모든 절은 독립적으로 읽을 수 있다.

이 글은 Linux 인프라 시리즈의 다음 단계이다:

이 시리즈를 모두 읽으면 "내 코드가 부팅된 Linux 위에서, 컨테이너 안에서, 어떻게 살아가는가"의 풍경 전체가 그려진다.


1. 컨테이너가 무엇인가 — 정의와 오해

1.1 마케팅의 정의

"컨테이너는 가상 머신보다 가벼운 격리 단위." 거의 모든 기업 자료가 이렇게 시작한다. 이는 진실이지만 너무 얕다.

1.2 더 정확한 정의

컨테이너는 Linux 커널의 namespaces와 cgroups를 활용해 만든 "프로세스 그룹의 격리된 실행 환경"이다.

핵심 단어는 프로세스 그룹격리:

  • 프로세스 그룹: 컨테이너는 별도의 OS가 아니라 호스트 커널 위에서 도는 프로세스들이다. 보통 한 컨테이너에 한 프로세스 (또는 작은 그룹).
  • 격리: 그 프로세스들이 자기만의 PID 공간, 네트워크, 파일시스템, 사용자, 호스트 이름 등을 가진다. 호스트와 다른 컨테이너에서 보이지 않는다.

1.3 가상 머신과의 차이

기준가상 머신컨테이너
격리 단위전체 OS프로세스 그룹
커널자기 커널호스트와 공유
부팅 시간수 초~분밀리초~초
메모리 오버헤드수백 MB+수 MB
격리 강도매우 강보통
사용처OS 다양성, 강한 격리마이크로서비스, 빠른 배포

핵심 차이: 컨테이너는 호스트 커널을 공유한다. 이는 가벼움의 원천이자 격리 약점의 원천이다.

1.4 컨테이너가 아닌 것

흔한 오해:

  • ❌ "컨테이너는 미니 VM이다" — 아니다. 별도 커널이 없다.
  • ❌ "컨테이너는 chroot의 후속이다" — 부분적으로만. chroot는 파일시스템만 격리.
  • ❌ "Docker == 컨테이너" — Docker는 컨테이너 도구일 뿐. 컨테이너 표준(OCI)이 따로 있다.
  • ❌ "컨테이너는 가상화이다" — 컨테이너는 가상화가 아니라 OS 수준 격리.

이 오해들을 푸는 것이 이 글의 첫 목표.


2. 역사 — chroot에서 Docker까지

2.1 1979 — chroot

Bell Labs의 Version 7 Unix가 chroot 시스템 콜을 도입. 단순한 아이디어: "이 프로세스의 root 디렉토리를 다른 곳으로 바꾼다." 결과적으로 그 프로세스는 새 root 위 파일만 볼 수 있다.

chroot /new/root /bin/sh

이는 첫 파일시스템 격리. 그러나 매우 약한 격리 — root 권한이 있으면 빠져나올 수 있고, 다른 자원 (네트워크, PID 등)은 그대로 보인다.

2.2 2000 — FreeBSD Jails

Poul-Henning Kamp가 FreeBSD에 도입. chroot의 한계를 보완:

  • 파일시스템 + 사용자 + 네트워크 + PID 모두 격리
  • jail 안의 root가 호스트의 root가 아님

이는 진정한 컨테이너의 조상. Linux보다 13년 앞섰다.

2.3 2004 — Solaris Zones

Sun Microsystems가 Solaris 10에 도입. FreeBSD jail의 더 발전된 버전. 자원 제어, 라이브 마이그레이션 같은 기능 포함. 컨테이너가 산업적으로 진지하게 다뤄진 첫 사례.

2.4 2006 — Google이 cgroups 발표

Google의 Paul Menage와 Rohit Seth가 "process containers" 패치를 LKML에 발표. 곧 "control groups" (cgroups)로 이름이 바뀜. 핵심: 프로세스 그룹의 자원 사용을 제한하는 메커니즘.

이는 Google의 Borg (내부 컨테이너 시스템)의 토대였다. Linux 2.6.24 (2008)에 머지.

2.5 2008 — LXC (Linux Containers)

IBM의 Daniel Lezcano와 Serge Hallyn이 작성. cgroups와 namespaces를 묶은 첫 사용자 공간 도구. "컨테이너"라는 단어가 Linux 세계에 정착.

LXC는 강력했지만 사용이 어려웠다. CLI가 복잡하고, 이미지 표준이 없었고, 개발자 워크플로우와 안 맞았다.

2.6 2013 — Docker

Solomon Hykes와 dotCloud 팀이 발표. LXC를 wrapping해서 사용자 친화적 인터페이스를 만들었다. 핵심 혁신:

  • Image: 컨테이너 파일시스템을 layer로 표현, hash로 식별.
  • Dockerfile: 이미지를 만드는 declarative 스크립트.
  • Registry: 이미지를 공유하는 중앙 저장소.
  • CLI: docker run이 모든 것을 처리.

Docker의 진짜 혁신은 기술이 아니라 UX. cgroups + namespaces + LXC + AUFS는 다 있었다. Docker가 이를 누구나 쓸 수 있게 만들었다.

2.7 2014-2015 — Kubernetes와 OCI

Google이 Borg의 외부 버전으로 Kubernetes 발표 (2014). 컨테이너를 클러스터 단위로 관리.

Docker, CoreOS, Red Hat, Google 등이 모여 OCI (Open Container Initiative)를 만듦 (2015). 컨테이너의 표준 포맷:

  • OCI Image Spec: 이미지 레이아웃과 manifest.
  • OCI Runtime Spec: 컨테이너 실행 인터페이스.
  • OCI Distribution Spec: registry 프로토콜.

이로써 컨테이너가 Docker만의 것이 아니게 됨.

2.8 2016 — runc, containerd, CRI-O

Docker가 자기 코어를 분리:

  • runc: 실제 컨테이너 실행. OCI Runtime Spec 구현.
  • containerd: 더 높은 수준의 컨테이너 라이프사이클 관리.
  • Docker daemon: containerd의 wrapper.

곧 Red Hat의 CRI-O (Kubernetes용 더 가벼운 대안), 그리고 다른 alternative들이 등장.

2.9 2017+ — Kubernetes 시대

Kubernetes가 컨테이너 오케스트레이션의 사실상 표준이 됨. 모든 메이저 클라우드가 managed Kubernetes 제공. 컨테이너가 인프라의 기본 단위가 됨.

2.10 최근 — Rootless, Wasm

  • Rootless 컨테이너: root 없이 컨테이너 실행. user namespace의 활용.
  • WebAssembly 컨테이너: WASM을 컨테이너의 대안으로 (또는 보완으로). Krustlet, wasmCloud.
  • gVisor: Google의 사용자 공간 커널로 컨테이너 보안 강화.
  • Kata Containers: 컨테이너를 microVM에 두기 (가벼운 VM).
  • Firecracker: AWS Lambda의 microVM. 컨테이너와 VM 사이의 새 카테고리.

★ Insight ─────────────────────────────────────

  • Docker의 진짜 혁신은 마케팅이다: 모든 기술은 이미 있었다. Docker가 한 일은 (1) Dockerfile로 빌드를 declarative하게, (2) Docker Hub로 배포를 쉽게, (3) docker run으로 사용을 단순하게 — 즉 UX. 기술적 혁신보다 사회적 혁신이었다.
  • OCI 표준화의 가치: Docker가 패권을 잡을 수도 있었다. OCI 표준화 덕분에 누구나 호환 가능한 컨테이너를 만들 수 있다. containerd, CRI-O, podman, BuildKit 등 모두 OCI 표준 위에서. 표준이 생태계를 살렸다.
  • Borg → Kubernetes의 서사: Google이 10년 동안 내부에서 쓴 Borg의 교훈을 Kubernetes에 담았다. "Pod, Service, Label, Selector" 같은 개념이 모두 Borg에서 왔다. Google이 가장 잘 아는 분야의 외부 공개가 산업 표준이 된 사례. ─────────────────────────────────────────────────

3. Linux Namespaces — 격리의 토대

namespaces는 "프로세스가 보는 것"을 격리하는 메커니즘이다. 같은 자원이 다른 namespace에서는 다르게 보인다. Linux는 8가지 namespace를 지원한다.

3.1 mnt (Mount) Namespace

각 mnt namespace는 자기만의 마운트 테이블을 가진다. 한 namespace에서 마운트한 것이 다른 namespace에서 안 보인다.

unshare -m /bin/bash
mount -t tmpfs tmpfs /tmp
ls /tmp  # 새 빈 tmpfs
exit
ls /tmp  # 호스트의 원래 tmpfs

컨테이너가 자기 root filesystem을 가지는 토대. chroot보다 정교한 파일시스템 격리.

CLONE_NEWNS 플래그로 생성. (역사적 이유로 NS는 namespace의 줄임말 — mount는 첫 namespace였음.)

3.2 PID Namespace

각 PID namespace는 자기만의 프로세스 ID 공간을 가진다. 한 namespace에서 PID 1인 프로세스가 호스트에서는 PID 12345일 수 있다.

unshare -p -f /bin/bash
ps  # 자기만 PID 1로 보임

컨테이너가 "자기만의 init"을 가지는 토대. 컨테이너 안의 프로세스는 호스트의 다른 프로세스를 못 본다.

CLONE_NEWPID 플래그.

3.3 net Namespace

각 net namespace는 자기만의 네트워크 인터페이스, 라우팅 테이블, ARP 캐시, 방화벽 규칙을 가진다.

ip netns add my-ns
ip netns exec my-ns ip link  # 자기만의 lo만 있음

컨테이너가 자기 IP, 자기 라우팅 테이블을 가지는 토대. veth pair (다음 절)와 결합해서 진짜 네트워킹 가능.

CLONE_NEWNET 플래그.

3.4 uts Namespace

UTS = UNIX Time-Sharing. 호스트 이름과 도메인 이름을 격리.

unshare -u /bin/bash
hostname my-container
hostname  # my-container
exit
hostname  # 원래 호스트 이름

컨테이너가 자기 호스트 이름을 가지는 토대. 매우 가볍지만 격리 보장.

CLONE_NEWUTS 플래그.

3.5 ipc Namespace

System V IPC (shared memory, semaphore, message queue)와 POSIX message queue를 격리. 한 컨테이너의 shared memory를 다른 컨테이너가 못 본다.

CLONE_NEWIPC 플래그.

3.6 user Namespace

UID와 GID를 격리. 컨테이너 안의 root (UID 0)가 호스트의 일반 사용자 (예: UID 1000)에 매핑될 수 있다.

unshare -U -r /bin/bash
id  # uid=0(root) — 컨테이너 안에서는 root
# 호스트에서는 일반 사용자 권한밖에 없음

이는 rootless 컨테이너의 토대. 컨테이너 안에서는 root 권한을 가진 것처럼 보이지만, 실제로 호스트에 위협이 되지 않는다.

CLONE_NEWUSER 플래그.

3.7 cgroup Namespace

cgroup hierarchy의 view를 격리. 컨테이너 안에서 /proc/self/cgroup을 보면 자기 cgroup만 보인다 (호스트의 전체 hierarchy를 안 봄).

CLONE_NEWCGROUP 플래그. Linux 4.6에서 추가.

3.8 time Namespace

시스템 시각의 일부 (CLOCK_MONOTONIC, CLOCK_BOOTTIME)를 격리. 컨테이너의 monotonic clock이 호스트와 다를 수 있다.

CLONE_NEWTIME 플래그. Linux 5.6에서 추가. 가장 새로운 namespace.

체크포인트/복원 시 유용 — 복원된 컨테이너가 일관된 시간 view를 가진다.

3.9 namespace 만들기 — clone, unshare, setns

세 가지 syscall로 namespace를 다룬다:

  • clone(2): 새 프로세스를 만들면서 namespace 만들기. CLONE_NEW* 플래그 사용.
  • unshare(2): 현재 프로세스를 새 namespace로 이동.
  • setns(2): 이미 존재하는 namespace로 진입. nsenter 명령어가 이걸 사용.
// pseudo: 컨테이너 시작
int flags = CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWNET |
            CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER;
pid_t pid = clone(child_func, stack, flags | SIGCHLD, args);

3.10 namespace를 보는 법

ls -l /proc/$$/ns/
# lrwxrwxrwx 1 user user 0 ... cgroup -> 'cgroup:[4026531835]'
# lrwxrwxrwx 1 user user 0 ... ipc -> 'ipc:[4026531839]'
# lrwxrwxrwx 1 user user 0 ... mnt -> 'mnt:[4026531840]'
# lrwxrwxrwx 1 user user 0 ... net -> 'net:[4026531992]'
# lrwxrwxrwx 1 user user 0 ... pid -> 'pid:[4026531836]'
# lrwxrwxrwx 1 user user 0 ... user -> 'user:[4026531837]'
# lrwxrwxrwx 1 user user 0 ... uts -> 'uts:[4026531838]'

각 링크는 namespace의 inode 번호. 같은 inode면 같은 namespace.

3.11 namespace의 한계

namespaces는 "보는 것"만 격리한다. 다음은 격리하지 않는다:

  • 시스템 콜: 컨테이너에서 reboot()을 호출하면 호스트가 재부팅 (capabilities로 막아야 함).
  • 하드웨어 액세스: 컨테이너에서 /dev/kvm을 만지면 호스트의 KVM 영향.
  • 커널 모듈: 한 컨테이너가 커널 모듈을 로드하면 모든 컨테이너에 영향.
  • 시간: time namespace가 있어도 wall clock은 공유.

이 한계 때문에 컨테이너의 격리는 VM보다 약하다. 보안 critical한 환경에서는 추가 보호 (gVisor, Kata Containers)가 필요.


4. cgroups — 자원 제어

namespaces가 "보이는 것"을 격리한다면, cgroups는 "쓰는 양"을 제한한다.

4.1 cgroups가 푸는 문제

namespaces로 격리해도 한 컨테이너가 호스트의 모든 CPU와 메모리를 차지할 수 있다. cgroups로 자원 양을 제한해야 진짜 격리.

4.2 cgroups v1 vs v2

두 버전이 있다:

  • v1 (2008): 각 controller (cpu, memory, io 등)가 자기만의 hierarchy. 한 프로세스가 여러 hierarchy에 동시 속할 수 있다. 복잡하고 일관성 부족.
  • v2 (2016, Linux 4.5): 단일 unified hierarchy. 한 프로세스가 정확히 한 cgroup에 속한다. 단순하고 일관성 있음.

대부분의 모던 distro는 cgroups v2 사용. systemd가 강하게 권장.

4.3 cgroups v2 사용

# v2 mount 확인
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

# 새 cgroup 만들기
mkdir /sys/fs/cgroup/my-app

# 프로세스 추가
echo $$ > /sys/fs/cgroup/my-app/cgroup.procs

# 메모리 제한 1GB
echo "1G" > /sys/fs/cgroup/my-app/memory.max

# CPU 50% (한 코어 절반)
echo "50000 100000" > /sys/fs/cgroup/my-app/cpu.max

4.4 핵심 controllers

Controller파일용도
cpucpu.max, cpu.weight, cpu.statCPU 시간 제한
memorymemory.max, memory.high, memory.low, memory.current메모리 제한
ioio.max, io.weight, io.stat디스크 I/O 제한
pidspids.max, pids.current프로세스 수 제한
cpusetcpuset.cpus, cpuset.memsCPU/NUMA 핀

4.5 memory.max vs memory.high

  • memory.max: 하드 캡. 넘으면 OOM kill.
  • memory.high: 소프트 캡. 넘으면 회수 압박이 강해지지만 즉시 죽이지는 않음.

이 두 가지 조합으로 부드러운 메모리 제어가 가능. Linux 메모리 글에서 자세히 다룸.

4.6 cpu.max — Bandwidth Control

cpu.max 형식: <quota> <period>. 단위는 microsecond.

50000 100000   # 100ms 주기 동안 50ms 사용 가능 → 50% (한 코어 절반)
200000 100000  # 100ms 동안 200ms → 2 코어 분량
max 100000     # 무제한

이는 CFS Bandwidth Control로 구현. Linux 스케줄러 글에서 자세히.

4.7 PSI — Pressure Stall Information

cgroups v2의 새 인터페이스. 각 cgroup이 자원 압박을 얼마나 받는지 측정.

cat /sys/fs/cgroup/my-app/memory.pressure
# some avg10=12.34 avg60=8.90 avg300=4.50 total=...
# full avg10=2.10 avg60=1.50 avg300=0.80 total=...

avg10이 10 이상이면 압박 강함. 모니터링/오토스케일링 시그널.

4.8 cgroups와 systemd

systemd는 모든 service를 자동으로 cgroup으로 묶는다. systemctl status my-service는 그 cgroup의 정보를 보여준다.

systemctl set-property my-service.service MemoryMax=1G
systemctl set-property my-service.service CPUQuota=50%

이는 사실상 cgroups를 만진다. systemd가 사용자 공간 인터페이스 역할.


5. OCI Runtime Specification

5.1 무엇을 표준화하나

OCI Runtime Spec은 "컨테이너를 어떻게 시작하나"의 표준이다. 두 가지를 정의:

  1. config.json: 컨테이너의 모든 설정 (root 경로, 명령어, 환경 변수, namespace, cgroups, capabilities 등).
  2. 라이프사이클 명령: create, start, kill, delete.

5.2 config.json 예시

{
  "ociVersion": "1.0.2",
  "process": {
    "terminal": true,
    "user": { "uid": 0, "gid": 0 },
    "args": ["/bin/sh"],
    "env": ["PATH=/usr/bin:/bin"],
    "cwd": "/",
    "capabilities": {
      "bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
      "effective": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
      "inheritable": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
      "permitted": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"]
    },
    "rlimits": [
      { "type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024 }
    ]
  },
  "root": {
    "path": "rootfs",
    "readonly": true
  },
  "hostname": "container",
  "mounts": [
    { "destination": "/proc", "type": "proc", "source": "proc" },
    { "destination": "/dev", "type": "tmpfs", "source": "tmpfs" }
  ],
  "linux": {
    "namespaces": [
      { "type": "pid" },
      { "type": "network" },
      { "type": "ipc" },
      { "type": "uts" },
      { "type": "mount" }
    ],
    "resources": {
      "memory": { "limit": 1073741824 },
      "cpu": { "shares": 1024 }
    }
  }
}

이 한 파일이 컨테이너의 모든 설정을 표현한다. runc는 이를 입력으로 받아 컨테이너를 실행한다.

5.3 라이프사이클

OCI 라이프사이클:

  1. create: rootfs와 config가 준비된 상태에서 컨테이너 생성. namespace 만들기, mount 설정.
  2. start: process를 실제로 실행. args에 지정된 명령어 호출.
  3. stop / kill: 시그널 전송.
  4. delete: 컨테이너 정리, namespace 해제.

이 단계들이 표준이라 다른 OCI 호환 런타임이 같은 방식으로 동작한다.

5.4 OCI 호환 런타임들

  • runc: 표준 구현. Docker, containerd 등이 사용.
  • crun: C로 작성된 더 빠른 대안. Red Hat이 개발.
  • runsc (gVisor): 사용자 공간 커널로 보안 강화.
  • kata-runtime: 컨테이너를 microVM에 둠.
  • youki: Rust로 작성.

모두 같은 config.json을 받는다. 사용자가 필요에 따라 갈아끼울 수 있다.


6. runc — 표준 컨테이너 런타임

6.1 무엇을 하나

runc는 OCI Runtime Spec의 reference 구현. Go로 작성. 매우 작은 바이너리 (수 MB).

핵심 책임:

  1. config.json 읽기
  2. namespace, cgroup, mount 설정
  3. capability와 seccomp 적용
  4. user 프로세스 실행

이게 전부. 매우 좁은 책임. 이미지 관리, 네트워킹 같은 것은 안 한다 — 그건 더 위 layer (containerd, Docker)의 책임.

6.2 실행 흐름

runc spec     # 기본 config.json 생성
runc create my-container --bundle /path/to/bundle
runc start my-container
runc kill my-container TERM
runc delete my-container

bundle은 rootfs와 config.json이 있는 디렉토리.

6.3 libcontainer

runc의 핵심은 libcontainer라는 Go 라이브러리. namespace 만들기, cgroup 설정 같은 저수준 작업을 처리. runc는 이 라이브러리의 CLI wrapper.

다른 도구도 libcontainer를 사용할 수 있다. crio도 부분적으로 사용.

6.4 init 프로세스의 트릭

문제: namespace를 만들려면 syscall이 필요한데, 그 syscall은 이미 namespace 안에서 호출되어야 한다. 어떻게 부트스트랩하나?

답: 두 단계 fork. 첫 fork가 namespace를 만들고, 그 안에서 다시 fork해서 user 프로세스를 실행. 첫 단계는 init 역할 (좀비 reaping 등).

runc는 이 부트스트랩을 매우 정교하게 처리한다. C 코드 일부 (nsexec.c)가 들어 있다 — Go의 일반 fork가 namespace 부트스트랩에 적합하지 않아서.

6.5 rootfs와 mount 설정

runc는 다음 순서로 mount를 처리:

  1. 새 mount namespace 만들기
  2. rootfs를 새 root로 pivot
  3. /proc, /sys, /dev 등 마운트
  4. 사용자가 지정한 추가 mount

이 모든 것이 호스트에 영향을 주지 않는다 (mount namespace 덕분).

6.6 pivot_root vs chroot

runc는 단순한 chroot 대신 pivot_root(2)를 사용한다. chroot는 빠져나오기 쉽지만, pivot_root는 더 강력하다. 이전 root를 unmount해서 컨테이너가 호스트 fs를 영영 못 보게 한다.


7. containerd — 더 높은 추상화

7.1 runc의 한계

runc는 컨테이너 한 개를 실행한다. 그 이상은 안 한다:

  • 이미지 다운로드/관리
  • 컨테이너 라이프사이클 추적
  • 네트워킹
  • 로그 수집
  • 모니터링

이 모든 것을 사용자 공간 도구가 처리해야 한다. containerd가 그 역할.

7.2 containerd의 책임

  • 이미지 관리: 이미지 pull, 저장, layer 관리.
  • 컨테이너 라이프사이클: 시작, 중지, 재시작, 상태 추적.
  • Snapshotter: rootfs 준비 (overlayfs 등).
  • Task service: runc를 호출해서 실제 실행.
  • Plugin system: 다양한 backend 지원.

containerd는 daemon이다. gRPC API를 노출하고, 클라이언트 (Docker, ctr, K8s)가 호출한다.

7.3 shim

containerd는 컨테이너마다 별도의 "shim" 프로세스를 띄운다. shim의 역할:

  • 컨테이너의 stdio 처리
  • 컨테이너 종료 감지 (좀비 reaping)
  • containerd 재시작 시에도 컨테이너 살아있게 유지

이는 매우 영리한 디자인 — containerd가 죽어도 컨테이너는 계속 동작한다.

7.4 컨테이너 실행 흐름

  1. 사용자가 ctr run nginx 호출
  2. containerd가 nginx 이미지를 pull (이미 있으면 skip)
  3. containerd가 snapshot을 만듦 (overlayfs)
  4. containerd가 OCI bundle 생성 (config.json + rootfs)
  5. containerd가 shim을 spawn
  6. shim이 runc create 호출
  7. shim이 runc start 호출
  8. 컨테이너 프로세스 실행
  9. 사용자에게 stdio 연결

이 모든 단계가 수십 ms 안에 끝난다.

7.5 CRI — Container Runtime Interface

Kubernetes는 containerd를 직접 호출하지 않는다. 대신 CRI라는 추상화를 통해 호출한다. containerd는 CRI를 구현한다 (cri plugin).

CRI 덕분에 Kubernetes는 containerd, CRI-O, Docker (옛날) 등을 갈아끼울 수 있다.


8. OCI Image — 이미지의 내부

8.1 이미지가 무엇인가

이미지는 컨테이너의 파일시스템과 메타데이터를 묶은 것. tarball + JSON manifest.

8.2 OCI Image Spec

핵심 컴포넌트:

  • manifest: 이미지의 layer 목록과 config 참조
  • config: 환경 변수, entrypoint, working directory 등
  • layers: 각각이 tar.gz 파일

8.3 manifest 예시

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 1234,
    "digest": "sha256:abc123..."
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 56789012,
      "digest": "sha256:def456..."
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 1234567,
      "digest": "sha256:789abc..."
    }
  ]
}

각 layer는 자기 hash로 식별. 같은 layer는 다른 이미지 사이에 공유될 수 있다.

8.4 layer의 의미

각 layer는 "이전 layer 위에 어떤 변경을 가했나"를 표현. Dockerfile의 각 줄이 보통 한 layer:

FROM ubuntu:22.04        # base layer
RUN apt-get update       # layer 1: 패키지 인덱스
RUN apt-get install -y nginx  # layer 2: nginx 설치
COPY ./html /var/www/    # layer 3: html 파일

이 4개 layer가 쌓여서 최종 이미지를 만든다.

8.5 layer 공유의 가치

같은 base image를 쓰는 100개의 이미지가 있어도 base layer는 한 번만 저장된다. registry에서도 한 번만 다운로드. 디스크와 네트워크 모두 절약.

8.6 image 만들기 — Dockerfile

FROM golang:1.21 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app ./cmd/server

FROM gcr.io/distroless/static
COPY --from=builder /app /app
EXPOSE 8080
ENTRYPOINT ["/app"]

이는 multi-stage build. 빌더 이미지에서 컴파일하고, 결과 바이너리만 작은 distroless 이미지로 복사. 최종 이미지는 매우 작다 (수십 MB).

8.7 BuildKit

Docker의 새 빌드 엔진. Dockerfile을 더 효율적으로 처리:

  • 병렬 빌드
  • 더 똑똑한 캐싱
  • secret 처리
  • 더 작은 이미지 생성

DOCKER_BUILDKIT=1 docker build로 활성화.

8.8 이미지 registry 프로토콜

OCI Distribution Spec이 표준. HTTP API:

  • GET /v2/<name>/manifests/<reference>: manifest 다운로드
  • GET /v2/<name>/blobs/<digest>: layer blob 다운로드
  • PUT /v2/<name>/manifests/<reference>: manifest 업로드

이 표준 덕분에 Docker Hub, GCR, ECR, GitHub Container Registry, Harbor 등이 모두 호환된다.


9. overlayfs — Copy-on-Write 파일시스템

9.1 무엇을 푸는가

컨테이너의 rootfs는 이미지 layer들의 합성이다. 같은 base image를 쓰는 100개의 컨테이너가 base layer를 공유해야 한다. 그러나 각 컨테이너는 자기 변경을 가질 수 있어야 한다.

답: copy-on-write 파일시스템. 읽기는 공유, 쓰기는 사적 복사.

9.2 overlayfs의 모델

overlayfs는 두 디렉토리를 하나로 합친다:

  • lowerdir (read-only): 기존 데이터
  • upperdir (read-write): 변경 사항
  • merged: 둘이 합쳐진 view
mount -t overlay overlay \
    -o lowerdir=/lower,upperdir=/upper,workdir=/work \
    /merged

9.3 read 동작

/merged/foo에 접근하면:

  1. upperdir에서 먼저 찾음
  2. 없으면 lowerdir에서 찾음
  3. 둘 다 없으면 ENOENT

upper가 lower를 가린다.

9.4 write 동작

/merged/foo에 쓰면:

  1. 만약 upper에 없고 lower에만 있으면, lower에서 upper로 복사 (copy-up)
  2. upper의 복사본을 수정

이게 copy-on-write. 수정 안 된 파일은 lower에 그대로 남고, 수정된 파일만 upper에 사본이 생긴다.

9.5 whiteout

파일 삭제는 어떻게? lower의 파일을 진짜 지울 수는 없다 (lower는 read-only). 대신 upper에 "whiteout" 파일을 만든다. overlayfs가 이를 보고 "이 파일은 삭제되었다"고 해석.

upper/.wh.foo  → "lower의 foo는 지워졌다"

9.6 다층 lower

lowerdir은 여러 디렉토리일 수 있다. 콜론으로 구분.

mount -t overlay overlay \
    -o lowerdir=/layer3:/layer2:/layer1,upperdir=/upper,workdir=/work \
    /merged

각 컨테이너 이미지의 layer가 정확히 이 모델로 매핑된다. 이미지의 layer N개 → overlayfs의 lower N개 + upper 1개.

9.7 컨테이너 시작 시

containerd의 snapshotter가:

  1. 이미지 layer들을 lowerdir으로 준비
  2. 새 빈 디렉토리를 upperdir로 만듦
  3. overlayfs mount
  4. 결과 merged 디렉토리를 컨테이너의 rootfs로 사용

컨테이너가 무엇을 쓰든 upperdir에만 쌓이고, lower (= 이미지 layer)는 변하지 않는다. 한 이미지로 1000개 컨테이너를 띄워도 이미지 데이터는 한 번만 디스크에 있다.

9.8 overlayfs vs aufs vs devicemapper

옛날에는 다른 backend도 있었다:

  • aufs: overlayfs 이전의 union 파일시스템. 메인라인에 안 들어감.
  • devicemapper: 블록 device 단위 CoW. 단편화 문제.
  • btrfs: 파일시스템 자체에 snapshot 기능.
  • zfs: btrfs와 비슷.

오늘날 거의 모든 컨테이너 환경이 overlayfs. 메인라인 통합, 단순성, 성능 모두 우수.


10. 컨테이너 네트워킹

10.1 기본 — net namespace는 비어 있다

새 net namespace는 loopback만 가진다. 외부 통신이 안 된다. 컨테이너가 진짜로 동작하려면 네트워크 인터페이스를 추가해야 한다.

10.2 veth pair

가상 이더넷 쌍. 두 끝이 있는 가상 케이블. 한 끝은 호스트 namespace, 다른 끝은 컨테이너 namespace.

ip link add veth0 type veth peer name veth1
ip link set veth1 netns my-container

호스트의 veth0으로 뭔가 보내면 컨테이너의 veth1으로 도착.

10.3 bridge

여러 컨테이너의 veth들을 bridge에 연결. bridge는 가상 스위치 역할. Docker의 docker0 인터페이스가 이것.

ip link add docker0 type bridge
ip link set veth0 master docker0
ip link set docker0 up

이제 같은 bridge에 연결된 컨테이너들이 서로 통신 가능.

10.4 NAT으로 외부 통신

외부 인터넷과 통신하려면 NAT이 필요. iptables 규칙으로 처리.

iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -j MASQUERADE

이 규칙으로 컨테이너에서 나가는 트래픽이 호스트의 IP로 변환된다.

10.5 CNI — Container Network Interface

Kubernetes 같은 오케스트레이션에서는 네트워킹이 더 복잡하다. CNI는 표준 인터페이스:

  • 컨테이너 시작 시 CNI plugin 호출
  • plugin이 네트워크 설정 (veth, IP 할당, 라우팅)
  • 컨테이너 종료 시 plugin이 정리

CNI plugin들:

  • bridge: 단순 bridge
  • calico: BGP 기반, large-scale
  • flannel: VXLAN 기반
  • cilium: eBPF 기반 (다음 절)
  • weave: 메시 모델

10.6 Cilium과 eBPF

eBPF 글에서 봤듯, Cilium은 컨테이너 네트워킹을 eBPF로 다시 썼다. iptables 대신 BPF map을 쓴다. 더 빠르고 일관된 성능.

대형 Kubernetes 클러스터의 사실상 표준이 되어가고 있다.


11. 컨테이너 보안

11.1 위협 모델

컨테이너의 위협:

  • 컨테이너 안의 코드가 호스트로 escape
  • 한 컨테이너가 다른 컨테이너를 손상
  • 컨테이너가 호스트의 자원 (CPU, 메모리, 디스크)을 고갈
  • 컨테이너가 호스트의 파일/네트워크에 접근

각각에 대한 방어선이 있다.

11.2 Capabilities

Linux의 root 권한을 ~40개의 작은 권한으로 분리.

CAP_NET_BIND_SERVICE  - 1024 미만 포트 binding
CAP_SYS_ADMIN         - 거의 모든 것 (위험)
CAP_NET_ADMIN         - 네트워크 설정
CAP_SYS_PTRACE        - ptrace
CAP_DAC_OVERRIDE      - 파일 권한 무시
CAP_SYS_MODULE        - 커널 모듈 로드 (위험)
CAP_SYS_TIME          - 시간 변경
...

컨테이너는 보통 이 중 일부만 가진다. Docker 기본은 약 14개의 안전한 capability만 부여:

CHOWN, DAC_OVERRIDE, FSETID, FOWNER, MKNOD, NET_RAW, SETGID, SETUID,
SETFCAP, SETPCAP, NET_BIND_SERVICE, SYS_CHROOT, KILL, AUDIT_WRITE

--cap-add / --cap-drop으로 조정 가능.

11.3 Seccomp

System call filter. 각 syscall을 허용/거부.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": ["read", "write", "exit", "exit_group"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

이 프로필은 read/write/exit만 허용. 다른 모든 syscall은 EPERM.

Docker의 기본 seccomp 프로필은 약 60개 syscall을 막는다 (전체 ~400개 중). mount, reboot, kexec_load, init_module 등 위험한 것들.

11.4 BPF 기반 seccomp

seccomp 자체가 BPF (cBPF)로 구현된다. 사용자가 BPF 프로그램을 작성해서 정밀한 필터를 만들 수 있다. 모던 컨테이너 런타임은 자동으로 BPF 프로필을 생성.

11.5 AppArmor

Ubuntu의 LSM (Linux Security Module). 경로 기반 권한.

profile docker-nginx flags=(attach_disconnected) {
  /var/www/** r,
  /var/log/nginx/** w,
  network tcp,
  ...
}

Docker 기본 AppArmor 프로필이 있다. --security-opt apparmor=...로 커스텀 가능.

11.6 SELinux

Red Hat 계열의 LSM. label 기반 권한. 더 엄격하지만 복잡.

docker run --security-opt label=type:my_container_t ...

기본 컨테이너에는 일반적으로 SELinux가 적용되어 있고 (RHEL/Fedora), 이는 추가 격리 layer를 제공한다.

11.7 user namespace와 rootless

위에서 본 user namespace를 활용하면 컨테이너 안의 root가 호스트의 일반 사용자에 매핑된다. 컨테이너가 escape해도 일반 사용자 권한밖에 없다.

podman run --rootless ...

Podman은 기본이 rootless. Docker도 rootless 모드 지원.

11.8 gVisor

Google의 사용자 공간 커널. 컨테이너의 syscall을 가로채서 자기 안에서 처리. 호스트 커널 노출이 거의 없다.

docker run --runtime=runsc ...  # runsc = gVisor

성능이 약 30% 떨어지지만 보안이 매우 강하다. Google App Engine, GKE Sandbox에서 사용.

11.9 Kata Containers

컨테이너를 microVM에 둔다. 호스트 커널을 공유하지 않음 — 진짜 VM 격리. 그러나 컨테이너처럼 가볍게 시작.

docker run --runtime=kata-runtime ...

성능 손실이 약간 있지만 (10-20%), VM 수준 격리. 멀티테넌트 환경에 적합.


12. Docker vs Podman vs containerd

12.1 Docker

가장 유명. daemon (dockerd) + CLI (docker). 컨테이너의 대중화.

특징:

  • 풍부한 기능
  • 거대한 이미지 생태계 (Docker Hub)
  • 익숙한 사용자 경험

단점:

  • daemon이 root로 실행 (보안 우려)
  • 단일 obstacle point of failure (daemon 죽으면 다 죽음)
  • Kubernetes에서 deprecated

12.2 Podman

Red Hat의 대안. daemonless. CLI가 직접 컨테이너 실행.

특징:

  • daemon 없음
  • 기본 rootless
  • Docker CLI 호환 (alias docker=podman 가능)
  • systemd와 잘 통합

단점:

  • 일부 Docker 기능 미지원
  • 생태계가 Docker만큼 크지 않음

12.3 containerd

저수준 daemon. CLI는 ctr (디버깅용) 또는 nerdctl (Docker-like).

특징:

  • 가장 가벼움
  • Kubernetes의 사실상 표준 런타임
  • 단순한 API

단점:

  • 사용자 친화적 도구가 적음
  • 직접 사용보다 다른 도구의 backend로 사용

12.4 어떤 것을 쓸 것인가

  • 개발자 데스크탑: Docker Desktop 또는 Podman
  • CI/CD: BuildKit 또는 Buildah
  • Kubernetes 노드: containerd
  • rootless가 중요한 경우: Podman
  • 익숙함이 중요한 경우: Docker

13. Kubernetes의 컨테이너

13.1 Pod이라는 추상화

Kubernetes는 컨테이너를 직접 관리하지 않는다. 대신 Pod (한 개 이상의 컨테이너가 같은 namespace를 공유하는 단위)을 관리.

같은 Pod 안의 컨테이너는:

  • 같은 net namespace (같은 IP, 같은 포트 공간)
  • 같은 ipc namespace
  • 같은 storage volume

다른 namespace는 분리.

13.2 Sidecar 패턴

Pod 안에 메인 컨테이너 + 보조 컨테이너 (사이드카)를 둔다. 예: 메인 앱 + 로그 수집기, 메인 앱 + Envoy 프록시.

이 패턴이 service mesh (Istio, Linkerd)의 토대였다. 최근에는 eBPF 기반으로 사이드카 없는 모델이 등장 (Cilium 글 참고).

13.3 Kubelet과 CRI

각 노드에서 kubelet 데몬이 컨테이너 관리. kubelet은 CRI를 통해 containerd (또는 CRI-O)와 통신.

[Kube API Server]
   [Kubelet]
       ↓ CRI
   [containerd]
    [runc]
   [Container]

각 layer가 좁은 책임. 모듈성과 교체 가능성이 높다.

13.4 init 컨테이너

Pod 시작 전에 한 번 실행되는 컨테이너. 메인 컨테이너 시작 전 초기화 작업 (DB schema 마이그레이션, 설정 다운로드 등).

13.5 Ephemeral 컨테이너

이미 돌아가는 Pod에 디버깅용 컨테이너를 추가. kubectl debug. 운영 환경에서 ssh 대신 사용.


14. 디버깅 도구

14.1 nsenter

다른 프로세스의 namespace에 진입.

nsenter -t 12345 -n /bin/bash    # PID 12345의 net namespace로
nsenter -t 12345 -m -p /bin/bash # mnt + pid namespace로

컨테이너 내부 디버깅에 매우 유용.

14.2 ctr와 crictl

ctr은 containerd의 CLI. 디버깅용.

ctr namespace ls
ctr --namespace k8s.io containers ls
ctr --namespace k8s.io tasks ls

crictl은 CRI의 CLI. Kubernetes 노드 디버깅.

crictl ps
crictl pods
crictl logs <container-id>
crictl exec -it <container-id> /bin/bash

14.3 lsns

namespace를 보는 도구.

lsns
        NS TYPE   NPROCS   PID USER             COMMAND
4026531835 cgroup    100     1 root             /sbin/init
4026531836 pid       100     1 root             /sbin/init
4026531837 user      100     1 root             /sbin/init
...

14.4 bpftrace

eBPF 글에서 본 도구. 컨테이너 안의 syscall, 파일 액세스 등을 추적.

bpftrace -e 'tracepoint:syscalls:sys_enter_openat /comm == "nginx"/ { @[str(args->filename)] = count(); }'

14.5 tcpdump in container namespace

nsenter -t <pid> -n tcpdump -i eth0

컨테이너의 네트워크 트래픽을 호스트에서 캡처.


15. 흔한 함정과 안티패턴

15.1 컨테이너 안의 init

컨테이너 안의 PID 1은 좀비 reaping을 해야 한다. 일반 앱이 PID 1이면 좀비를 못 잡고 누적된다.

해결:

  • tini 같은 작은 init 사용
  • Docker의 --init 플래그
  • 앱 자체가 sub-reaper

15.2 logs를 stdout으로

컨테이너의 로그는 stdout/stderr로 보내야 한다. 파일로 쓰면:

  • 로그 rotation 안 됨
  • 디스크가 찰 수 있음
  • 컨테이너 외부에서 보기 어려움

12 factor app의 원칙. 모든 로그는 stdout으로.

15.3 latest 태그 의존

image: nginx:latest

이는 위험하다. latest가 시간에 따라 바뀐다. 빌드/배포가 비결정적.

해결: 명시적 버전 또는 hash 사용.

image: nginx:1.25.3
# 또는
image: nginx@sha256:abc123...

15.4 큰 이미지

수 GB짜리 이미지는:

  • 다운로드가 느림
  • registry storage 비쌈
  • 공격 표면 큼

해결:

  • multi-stage build
  • distroless base image
  • alpine base image
  • 불필요한 파일 제거

15.5 root로 실행

USER root  # 위험

기본은 root다. 이는 위험. 가능하면 비-root 사용자로:

USER 1000:1000

15.6 secret을 환경 변수로

env:
  - name: DB_PASSWORD
    value: "secret123"

환경 변수는 /proc/<pid>/environ을 통해 노출될 수 있다. Kubernetes Secret 또는 외부 vault 사용.


16. 사례 연구

16.1 Google Borg

Google이 2003년부터 사용한 내부 컨테이너 시스템. cgroups의 발상지. Kubernetes의 직접 조상.

핵심 특징:

  • 매우 큰 규모 (수십만 머신)
  • 자원 효율성 극대화 (90%+ utilization)
  • 미세한 priority 시스템

Borg의 교훈이 Kubernetes에 그대로 들어갔다.

16.2 AWS Lambda

서버리스 컴퓨팅. 매 함수 호출이 컨테이너 (또는 microVM, Firecracker)에서 실행.

도전: cold start. Firecracker로 ~125ms cold start 달성. 컨테이너와 VM의 경계.

16.3 GitHub Actions

각 워크플로우 작업이 별도 컨테이너에서 실행. CI/CD의 표준 모델.

16.4 Cloudflare Workers

서버리스. 그러나 컨테이너가 아니라 V8 isolate. WebAssembly와 결합. 더 가벼운 모델 (마이크로초 cold start).

이는 컨테이너의 한 limit를 보여준다 — V8 isolate가 컨테이너보다 1000배 빠를 수 있다 (특정 워크로드에서).


17. 미래 — 컨테이너의 다음 단계

17.1 WebAssembly 컨테이너

WASM을 컨테이너의 보완 또는 대안으로. 장점:

  • 더 가벼움 (수 MB vs 수십 MB)
  • 더 빠른 시작 (마이크로초)
  • 더 나은 격리 (namespace보다 강함)
  • 언어 독립성

도구: wasmtime, wasmer, krustlet, wasmCloud.

단점:

  • 생태계가 미성숙
  • 모든 워크로드에 적합하지 않음
  • 시스템 콜 인터페이스 미정 (WASI 진행 중)

17.2 microVM

Firecracker, Cloud Hypervisor 같은 가벼운 VM. 컨테이너의 격리 약점을 보완.

[VM heavyweight]   [microVM]   [container lightweight]
   Linux VM      Firecracker      runc
   ~수 GB         ~수 MB          ~수 MB
   분 단위 boot   125ms boot      ms boot
   강한 격리      강한 격리        보통 격리

microVM이 컨테이너의 가벼움과 VM의 격리를 동시에 가지는 새 카테고리. AWS Lambda, Fargate가 이를 사용.

17.3 Confidential Computing

CPU의 enclaves (Intel SGX, AMD SEV)를 활용해 컨테이너 안의 데이터를 호스트로부터도 보호. 클라우드 제공자 자체를 못 믿는 시나리오에 적합.

17.4 sched_ext와 컨테이너

Linux 스케줄러 글에서 본 sched_ext가 컨테이너 워크로드에 맞춰진 스케줄러를 가능하게 한다. 컨테이너별로 다른 정책.

17.5 eBPF 기반 모든 것

eBPF 글에서 봤듯, eBPF가 컨테이너 네트워킹, 보안, 관찰성 모두를 다시 쓰고 있다. iptables, sidecar proxy, 일부 LSM이 점차 eBPF로 대체.

★ Insight ─────────────────────────────────────

  • 컨테이너의 진짜 이점은 unit of deployment: 격리는 부산물이다. 진짜 가치는 "내 코드 + 모든 의존성을 한 단위로 배포"라는 모델. 이는 deployment 자동화, rollback, A/B 테스트, blue-green 같은 모든 모던 운영 패턴의 토대가 된다.
  • Kubernetes의 "Pod"이 미묘한 추상화: Pod이 컨테이너 그 자체가 아니라 "공유 namespace를 가진 컨테이너 그룹"이라는 점이 sidecar 패턴을 가능하게 만든다. 이는 컨테이너 하나만으로는 불가능한 표현력. Kubernetes의 작은 디자인 결정 하나가 service mesh 같은 큰 패턴의 토대.
  • 컨테이너는 끝이 아니다: WebAssembly, microVM, V8 isolate 등이 다음 세대의 격리 단위로 등장하고 있다. 컨테이너가 향후 10년 동안 사라지지는 않겠지만, 컨테이너가 "유일한 단위"였던 시대는 끝나고 있다. 워크로드에 맞는 단위를 고르는 안목이 점점 중요해진다. ─────────────────────────────────────────────────

18. 결론 — 컨테이너는 Linux의 응용이다

이 글을 다 읽었다면 다음 질문에 답할 수 있을 것이다:

  • 컨테이너와 가상 머신의 진짜 차이는?
  • Linux namespaces 8가지가 무엇이고 무엇을 격리하나?
  • cgroups v1과 v2의 차이는?
  • OCI Runtime Spec이 무엇을 표준화하나?
  • runc, containerd, Docker의 관계는?
  • overlayfs가 어떻게 layer를 합치나?
  • Capabilities, seccomp, AppArmor가 각각 무엇을 보호하나?
  • Rootless 컨테이너의 토대는?
  • Kubernetes의 Pod이 왜 컨테이너 하나가 아닌가?

컨테이너는 마법이 아니다. Linux 커널의 namespaces + cgroups + LSM + filesystem feature를 정교하게 조합한 결과이다. 30년의 진화 — chroot에서 jail, zone, cgroups, LXC, Docker, OCI, Kubernetes — 가 만든 풍경이다.

이 글은 Linux 인프라 시리즈의 종합편이다. 부팅 → 스케줄러 → I/O → 메모리 → eBPF → 컨테이너. 각 글이 독립적으로 의미가 있지만, 모두 모이면 "Linux 위에 만들어진 모던 인프라"의 풍경 전체가 그려진다.

다음 글에서는 [Kubernetes 내부 구조 (etcd, controller, scheduler)] 또는 [Service Mesh와 Cilium]을 다룰 예정이다. 컨테이너 위에 쌓인 layer들을 더 깊이 들여다볼 차례이다.


부록 A — 참고 자료

부록 B — 자주 묻는 질문

Q: 컨테이너와 VM, 무엇을 써야 하나? A: 워크로드에 따라. 빠른 배포, 작은 오버헤드 = 컨테이너. 강한 격리, 다른 OS = VM. 둘을 결합 (Kata, Firecracker)도 가능.

Q: Docker가 deprecated인가? A: Kubernetes 1.20에서 Docker shim이 deprecated되었다. 그러나 Docker 자체는 여전히 살아있다. 단지 Kubernetes 노드의 런타임으로 직접 쓰지 않을 뿐 (containerd로 대체).

Q: 모든 앱을 컨테이너에 넣어야 하나? A: 아니다. 단순한 서버, 임베디드, 일부 desktop 앱은 컨테이너 없이 더 낫다. 컨테이너가 가치를 더하는 곳에만.

Q: rootless 컨테이너가 정말 안전한가? A: 더 안전하다 (호스트 root 우회 불가). 그러나 여전히 컨테이너 escape 가능성은 있다. 보안 critical에는 추가 layer (gVisor, Kata).

Q: 컨테이너 오케스트레이션은 항상 Kubernetes여야 하나? A: 아니다. 작은 환경에는 docker-compose, Nomad, swarm 등이 더 간단. Kubernetes는 큰 클러스터의 가치가 있을 때.

Q: 컨테이너 안에서 컨테이너를 띄울 수 있나? A: 가능 (DinD - Docker in Docker). 그러나 보안과 성능 우려가 있다. 보통 host의 docker socket을 공유하는 게 더 안전.

Q: 컨테이너의 메모리 한도를 어떻게 정해야 하나? A: 워크로드의 working set + 20-30% 여유. JVM/Go 같은 런타임은 자기 한도를 컨테이너 한도에 맞춰야 한다 (GC 글 참고).

Q: WebAssembly가 컨테이너를 대체할까? A: 일부 워크로드 (서버리스, edge)에서는 그럴 수 있다. 그러나 일반적인 백엔드 워크로드는 당분간 컨테이너가 표준일 것.


부록 C — 미니 용어집

  • Container: namespaces + cgroups로 격리된 프로세스 그룹.
  • Namespace: 보이는 자원의 격리 (pid, mnt, net, ...).
  • cgroups: 자원 사용 제어 (cpu, memory, io, ...).
  • OCI: Open Container Initiative. 컨테이너 표준화 단체.
  • Runtime Spec: 컨테이너 실행 인터페이스 표준.
  • Image Spec: 이미지 layout 표준.
  • Distribution Spec: registry 프로토콜 표준.
  • runc: 표준 OCI runtime 구현.
  • containerd: high-level 컨테이너 daemon.
  • Docker: containerd + 사용자 도구 + builder 등.
  • Podman: daemonless 대안.
  • CRI: Container Runtime Interface (Kubernetes).
  • Pod (k8s): 같은 namespace 공유 컨테이너 그룹.
  • Layer: 이미지의 한 단위. tar.gz.
  • manifest: 이미지의 layer 목록과 메타데이터.
  • overlayfs: union mount filesystem.
  • upperdir / lowerdir: overlayfs의 두 계층.
  • whiteout: 삭제된 파일을 표시.
  • veth: 가상 이더넷 쌍.
  • bridge: 가상 스위치.
  • CNI: Container Network Interface.
  • Capability: Linux의 root 권한 분할.
  • seccomp: syscall 필터.
  • AppArmor / SELinux: LSM (Linux Security Module).
  • rootless: root 없이 실행되는 컨테이너.
  • user namespace: UID/GID 격리.
  • shim: containerd와 컨테이너 사이의 분리 프로세스.
  • gVisor: 사용자 공간 커널.
  • Kata Containers: 컨테이너를 microVM에.
  • Firecracker: AWS Lambda의 microVM.
  • WebAssembly: 차세대 가벼운 격리 단위 후보.

이 글로 Linux 인프라 시리즈의 핵심 부분이 마무리된다. 부팅, 스케줄러, io_uring, 메모리, eBPF, 그리고 이 글까지 — 여섯 글이 모이면 모던 Linux 인프라의 풍경 전체가 그려진다. 다음에는 더 위 layer (Kubernetes, service mesh, observability)를 다룰 차례이다.