Skip to content

필사 모드: 컨테이너와 Docker 내부 완전 정복 — Namespace, cgroups, OverlayFS, seccomp, Capabilities 그리고 Kubernetes까지 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

0. "컨테이너는 VM 이 아니다" 가 의미하는 것

많은 개발자가 컨테이너를 "가벼운 VM" 으로 이해한다. 실제로는 완전히 다르다:

| 항목 | VM | Container |

| --- | --- | --- |

| 격리 단위 | 하드웨어 | OS 네임스페이스 |

| 커널 | VM 마다 독립 | 호스트와 공유 |

| 부팅 시간 | 수 초 ~ 수 분 | 수십 ms |

| 메모리 오버헤드 | 수백 MB | 수 MB |

| 디스크 오버헤드 | GB | MB |

| 격리 강도 | 강력 | 약함 |

컨테이너 = **프로세스 + 커널 기능들 (namespace, cgroup, rootfs) 의 조합**. 별도 OS 가 있는 게 아니다.

이 글은 `docker run nginx` 라는 한 줄이 실제로 리눅스 커널에서 어떻게 작동하는지, 그리고 그 뒤에 숨은 10년 넘는 엔지니어링 역사를 파헤친다.

1. 컨테이너의 짧지 않은 역사

1.1 1979 — chroot

Unix v7 의 `chroot()` 시스템 콜. "이 프로세스의 파일 시스템 루트를 바꾼다" → 제한된 디렉터리만 보임.

하지만 chroot 만으로는 부족:

- 프로세스 목록, 네트워크, 사용자는 그대로 보임.

- 탈출 기법 잘 알려짐 (`chdir`, 마운트 등).

1.2 2000 — FreeBSD Jails

chroot 확장판. 네트워크, 프로세스, 사용자까지 격리. 진짜 "경량 가상화" 의 시작.

1.3 2004 — Solaris Zones

완전한 격리 기술. 컨테이너 개념의 상업적 첫 성공.

1.4 2006 — cgroups, 2007 — LXC

Google 엔지니어들이 내부 자원 격리를 위해 "process containers" 개발 → cgroups 로 Linux 에 도입. 네임스페이스도 단계적 추가.

LXC (Linux Containers) 가 이들을 조합해 첫 실용 컨테이너 런타임 제공.

1.5 2013 — Docker

Solomon Hykes 의 dotCloud 에서 LXC 를 쉬운 API 로 포장. `docker run` 한 줄의 마법.

킬러 기능: **이미지 레이어** (OverlayFS) + **레지스트리** (Docker Hub). 개발자가 "내 환경을 그대로 공유" 가능.

1.6 2015 — OCI, 2016 — containerd, 2017 — Kubernetes 급부상

Open Container Initiative (Docker + CoreOS + Linux Foundation) 가 이미지/런타임 표준 규격 (OCI) 확립.

Docker 가 런타임을 containerd 로 분리. 작고 재사용 가능한 컴포넌트로 진화.

Kubernetes 가 오케스트레이션 표준이 되며 "docker 엔진" 은 점점 추상화됨.

1.7 2024 — 현재

Docker Desktop, Podman (daemonless), Kubernetes + containerd + runc, ECS, Cloud Run, Fly.io. 컨테이너는 이제 모든 클라우드 배포의 기본 단위.

2. Linux Namespace — 격리의 7가지 차원

네임스페이스는 "이 프로세스가 보는 OS 자원" 을 제한한다. 7가지 종류 (2024 기준):

2.1 PID Namespace

프로세스 ID 격리. 컨테이너 안에서 `ps` 하면:

PID TTY TIME CMD

1 ? 00:00:00 nginx

10 ? 00:00:00 worker

11 ? 00:00:00 worker

컨테이너의 `nginx` 가 PID 1. 호스트에서 보면 그건 PID 12345. 서로 다른 숫자 공간이다.

PID 1 의 특별함:

- 커널이 좀비 리핑 (init 역할) 을 기대.

- 종료 시 모든 자식도 종료.

- 대부분 앱은 init 으로 쓰도록 설계 안 됨 → `tini`, `dumb-init` 같은 작은 init.

2.2 Mount Namespace

파일 시스템 마운트 포인트 격리. 각 컨테이너는 자기만의 `/` 트리를 가진다.

`chroot` 의 진화형. 호스트의 특정 마운트만 공유 가능.

2.3 Network Namespace

각 컨테이너가 자기 네트워크 인터페이스, 라우팅 테이블, iptables 규칙.

호스트에서 컨테이너 네트워크 진입

sudo nsenter -t <container_pid> -n ip addr

Docker 의 `bridge` 모드: 가상 브리지 `docker0` + veth 쌍으로 연결.

2.4 UTS Namespace

Unix Timesharing System. 호스트네임 + 도메인 격리. 컨테이너가 자기 호스트네임 설정 가능.

2.5 IPC Namespace

System V IPC, POSIX 메시지 큐 격리. 컨테이너 간 공유 메모리 분리.

2.6 User Namespace

UID/GID 매핑. 컨테이너의 root (UID 0) 이 호스트에서는 비특권 사용자 UID 100000.

보안에 결정적이지만 설정 복잡 → Docker 는 기본적으로 활성화 안 함. Podman rootless 가 활용.

2.7 Cgroup Namespace

cgroup 계층을 격리. 컨테이너가 자기 cgroup 경로를 `/` 로 봄. 2016년 도입.

2.8 Time Namespace

Linux 5.6 (2020) 신규. 각 컨테이너가 다른 부팅 시간 (CLOCK_BOOTTIME) 가질 수 있음. 체크포인트/복원 (CRIU) 에 활용.

2.9 네임스페이스 조작

unshare --pid --mount --net --fork bash # 새 네임스페이스로 bash

nsenter -t PID -n -p # 기존 네임스페이스 진입

Docker 의 `docker exec` 는 사실 대상 컨테이너의 네임스페이스들을 조합해 새 프로세스 생성.

3. cgroups — 자원 제한

3.1 cgroups v1 (2007-)

각 자원 (CPU, 메모리, 네트워크 etc) 마다 별도 계층:

/sys/fs/cgroup/

├── cpu/

│ ├── docker/

│ │ └── <container_id>/

│ │ ├── cpu.cfs_quota_us

│ │ └── cpu.cfs_period_us

├── memory/

├── blkio/

└── ...

각 계층 독립 → 복잡.

3.2 cgroups v2 (2016-, systemd 232+)

단일 통합 계층:

/sys/fs/cgroup/

├── user.slice/

├── system.slice/

│ └── docker-<id>.scope/

│ ├── cpu.max # "100000 100000" = 1 CPU

│ ├── memory.max # "536870912" = 512MB

│ ├── io.max

│ └── pids.max

- 하나의 트리에 모든 자원.

- 계층적 리소스 배분.

- 더 단순한 모델.

3.3 CPU 제한의 실제

`cpu.max = "50000 100000"` 의미: "100ms 주기마다 최대 50ms CPU 사용" = 0.5 CPU.

하지만 **버스팅 허용** 은 설정에 따라. JVM, Node 가 짧게 CPU 폭주하면 throttle 걸림 → 응답 지연.

**Docker의 `--cpus=1`** = `cpu.max = "100000 100000"`.

3.4 메모리 제한

- `memory.max`: 상한. 초과 시 OOM.

- `memory.high`: soft limit. 넘으면 reclaim 압박.

- `memory.min`, `memory.low`: 보호 메모리.

컨테이너가 `memory.max` 에 도달하면 **컨테이너 내부 OOM killer** 발동. 호스트는 멀쩡.

3.5 JVM, Node, Go 의 cgroup 인식

JVM 10+ (`-XX:+UseContainerSupport` 기본): cgroup 한도를 읽어 힙 설정.

Go 1.19+ `GOMEMLIMIT`: 수동으로 cgroup limit 반영.

Node `--max-old-space-size`: 직접 설정.

이걸 안 하면 "호스트 메모리 64GB" 로 오해하고 힙을 크게 → OOM.

4. OverlayFS — 이미지 레이어의 비밀

4.1 왜 레이어인가

Docker 이미지는 **읽기 전용 레이어의 스택**:

Layer 4 (10KB): 앱 코드

Layer 3 (50MB): npm install 결과

Layer 2 (80MB): apt packages

Layer 1 (70MB): ubuntu base

- 각 레이어는 **독립 + 불변**.

- 여러 이미지가 base 레이어 공유.

- 100 GB 이미지 있어도 변경된 top layer 만 push/pull.

4.2 OverlayFS 의 구조

┌─────────────────────┐

│ upperdir (RW) │ ← 컨테이너 쓰기

├─────────────────────┤

│ merged view │ ← 앱이 보는 파일시스템

├─────────────────────┤

│ lowerdir (RO) ×N │ ← 이미지 레이어들

└─────────────────────┘

- 읽기: upperdir 에 없으면 lowerdir 에서 찾음.

- 쓰기: 항상 upperdir 에.

- 수정: **Copy-up** — 원본을 upperdir 에 복사 후 수정.

4.3 Copy-up 의 비용

큰 파일 수정 시:

- 원본을 upperdir 에 **전체 복사**.

- 수정은 그 후.

100MB 파일에 1바이트 수정 = 100MB 복사. 대용량 DB 파일을 컨테이너 내부에 두면 안 되는 이유.

해결: **Volume mount**. DB 데이터는 호스트 볼륨으로.

4.4 Whiteout — 파일 삭제

`rm` 한다고 lowerdir 에서 지워지지 않음. 대신:

- upperdir 에 특수 "whiteout" 파일 생성.

- merged view 에서 해당 파일 안 보임.

- 실제 파일은 여전히 아래에.

결과: "Dockerfile 에서 RUN rm 으로 용량 줄이려 해도" 레이어 크기 안 줄어든다. 한 RUN 안에서 생성+삭제 해야 레이어에 안 남음.

4.5 StorageDriver 진화

Docker 의 storage driver 변천사:

- **aufs** (2013-): 초기. 커널 upstream 없음.

- **devicemapper** (2014-): thin provisioning. 느림.

- **btrfs, zfs**: 파일시스템 의존.

- **overlay** (2014): upstream 커널, 빠름.

- **overlay2** (2016-): 현재 기본. 여러 lowerdir 동시 지원.

Podman 은 fuse-overlayfs (rootless 지원) 도 옵션.

5. 컨테이너 런타임 — runc 와 친구들

5.1 Docker 의 내부 계층

docker CLI

dockerd (daemon)

containerd (컨테이너 lifecycle)

containerd-shim (각 컨테이너당 1개)

runc (OCI runtime, 실제 컨테이너 생성)

Linux kernel (namespace, cgroup, ...)

5.2 runc 의 역할

OCI Runtime Specification 구현. 입력: rootfs + config.json. 출력: 실행 중인 컨테이너.

C / Go 로 구현, 순수 시스템 콜 호출:

- `clone()` + namespace flags.

- `setns()` 로 네임스페이스 진입.

- cgroup 설정.

- rootfs 마운트.

- `execve()` 로 사용자 프로세스 시작.

5.3 대안 런타임들

- **crun**: C 로 작성, runc 보다 빠름 (Go 런타임 오버헤드 없음).

- **youki**: Rust 구현.

- **Kata Containers**: 각 컨테이너를 초경량 VM 안에서 실행 → VM 급 격리.

- **gVisor**: Google. 유저 공간 커널로 시스템 콜 가로챔 → 커널 격리.

- **Firecracker**: AWS Lambda 기반. microVM.

보안이 중요한 멀티테넌트 환경 (Lambda, Fargate) 은 VM 격리 런타임을 씀.

5.4 shim 의 역할

`containerd-shim` 이 컨테이너마다 존재:

- containerd 데몬이 죽어도 컨테이너는 살아남음.

- stdout/stderr 수집.

- exit code 기록.

Docker 가 kubelet 에 직접 연결되지 않는 이유. kubelet → CRI → containerd → shim → runc 구조.

6. 보안 — 컨테이너가 "진짜 격리" 가 아닌 이유

6.1 공유 커널의 위험

컨테이너들이 같은 커널을 공유 → **커널 취약점 = 모든 컨테이너 탈출**. 실제 사례:

- **Dirty COW** (CVE-2016-5195): 권한 상승. 컨테이너 탈출 가능.

- **runc CVE-2019-5736**: runc 바이너리 덮어쓰기로 호스트 root 탈취.

- **Leaky Vessels** (2024): runc, containerd 다수 취약점.

대응: 커널 자주 업데이트, AppArmor/SELinux, seccomp 프로필.

6.2 Linux Capabilities

전통적 Unix: root (UID 0) = 전체 권한. 컨테이너에는 너무 위험.

Capabilities 가 root 권한을 **세분화**:

- `CAP_NET_ADMIN`: 네트워크 설정.

- `CAP_SYS_ADMIN`: 대부분의 위험한 것들.

- `CAP_NET_BIND_SERVICE`: 1024 이하 포트 바인딩.

- ... 수십 가지.

Docker 기본: 제한된 capability set (최소 권한). `--cap-add`, `--cap-drop` 로 조정. `--privileged` 는 모든 cap = 호스트 root 와 거의 동등.

6.3 seccomp — 시스템 콜 필터

"이 컨테이너는 특정 시스템 콜만 허용":

{

"defaultAction": "SCMP_ACT_ERRNO",

"syscalls": [

{ "names": ["read", "write", "open", ...], "action": "SCMP_ACT_ALLOW" }

]

}

Docker 의 기본 seccomp profile 이 약 300 개 syscall 만 허용. `keyctl`, `ptrace`, `mount` 등은 차단.

6.4 AppArmor / SELinux

파일 시스템 레벨 MAC (Mandatory Access Control). 컨테이너가 시도할 수 있는 파일 접근을 정책으로 제한.

Ubuntu 는 AppArmor, RHEL 은 SELinux.

6.5 Rootless Container

컨테이너 데몬도 비특권 사용자로 실행. User namespace 로 UID 매핑:

- Podman 의 기본.

- Docker 도 rootless 모드 지원.

- 보안 대폭 강화, 일부 기능 제한 (1024 이하 포트 등).

6.6 보안 체크리스트

- **최신 커널**.

- **seccomp 기본 프로필** 유지 (`--security-opt seccomp=unconfined` 금지).

- **capability 최소화** (`--cap-drop ALL` + 필요한 것만 add).

- **Non-root 사용자**로 앱 실행 (`USER` 지시어).

- **Read-only rootfs** (`--read-only` + tmpfs 볼륨).

- **Image scanning**: Trivy, Grype, Docker Scout.

- **Runtime monitoring**: Falco.

7. 이미지 만들기의 기술

7.1 레이어 최적화

나쁨: 각 RUN 이 새 레이어

FROM node:20

COPY package.json .

RUN npm install # 레이어 1

COPY . .

RUN npm run build # 레이어 2

RUN rm -rf /tmp/cache # 레이어 3 (1번의 cache 는 그대로 남음)

좋음: 임시 파일을 같은 RUN 에서

FROM node:20

COPY package.json .

RUN npm install && rm -rf /tmp/* /var/cache/*

COPY . .

RUN npm run build

7.2 Multi-stage Build

Stage 1: 빌드

FROM node:20 AS builder

WORKDIR /app

COPY . .

RUN npm install && npm run build

Stage 2: 런타임 (소형)

FROM node:20-alpine

COPY --from=builder /app/dist /app

COPY --from=builder /app/node_modules /app/node_modules

CMD ["node", "/app/index.js"]

빌드 도구 (gcc, webpack 등) 는 최종 이미지에 없음 → 크기 5배+ 감소.

7.3 Distroless 이미지

Google 이 만든 최소 이미지 (shell 조차 없음):

FROM gcr.io/distroless/nodejs20

COPY --from=builder /app /app

CMD ["/app/index.js"]

- 크기: 수십 MB.

- 공격 표면: shell 없어서 RCE 어려움.

- 단점: 디버깅 어려움 (exec 들어갈 수 없음).

7.4 Alpine vs Debian

- **Alpine** (`node:20-alpine`): 5MB base. musl libc.

- **Debian slim** (`node:20-slim`): 80MB. glibc.

Alpine 주의: **musl 호환성 문제** (DNS resolver, pthread 동작 미세 차이). Python 의 C extensions 빌드 시 추가 도구 필요.

7.5 BuildKit 과 캐시

DOCKER_BUILDKIT=1 docker build .

- **병렬 스테이지 빌드**.

- **캐시 내보내기/가져오기**: `--cache-to=type=registry,ref=...`.

- **보안**: 빌드 타임 secrets (`--secret`).

- **BuildKit 은 containerd 네이티브**.

8. 네트워킹 — docker0 부터 CNI 까지

8.1 Docker 기본 브리지

docker0 (bridge, 172.17.0.1)

├── veth0 → container1 eth0 (172.17.0.2)

├── veth1 → container2 eth0 (172.17.0.3)

└── veth2 → container3 eth0 (172.17.0.4)

- **veth 쌍**: 한쪽 호스트, 다른쪽 컨테이너.

- **NAT**: `iptables MASQUERADE` 로 외부 연결.

- **포트 포워딩**: `-p 80:8080` 은 iptables DNAT.

8.2 네트워크 모드

- **bridge** (기본): 위 설명.

- **host**: 호스트 네트워크 그대로 (격리 없음, 가장 빠름).

- **none**: 네트워크 없음.

- **container**: 다른 컨테이너와 공유 (pod 와 유사).

8.3 Kubernetes CNI

Kubernetes 는 "모든 pod 가 평면적 IP 를 가진다" 는 철학. Docker bridge 로는 부족.

- **Flannel**: VXLAN 터널링, 단순.

- **Calico**: BGP 기반, eBPF 지원, 네트워크 정책.

- **Cilium**: eBPF 네이티브, 관찰성/보안 풍부.

- **AWS VPC CNI**: ENI 를 pod 에 직접 할당.

CNI = Container Network Interface. 오케스트레이터가 네트워크 플러그인을 교체 가능하게 하는 표준.

9. Kubernetes 와의 통합

9.1 왜 Kubernetes 가 Docker 를 버렸는가 (2020)

Kubernetes 1.20 에서 "dockershim deprecation" 공지 → 1.24 (2022) 에서 제거.

이유:

- Docker 엔진은 kubelet 이 쓰지 않는 기능 다수 포함 (이미지 빌드, swarm 등).

- CRI (Container Runtime Interface) 표준에 맞추기 위한 별도 shim 계층 필요.

- containerd 가 이미 Docker 의 core 였음.

결과: kubelet → CRI → containerd 직접. 더 단순, 더 빠름. 사용자 관점에서 OCI 이미지는 그대로 작동.

9.2 Pod = 네임스페이스를 공유하는 컨테이너들

Pod 는 여러 컨테이너가 **network, IPC, UTS 네임스페이스를 공유**. Mount, PID 는 (기본) 독립.

pause container (네임스페이스 소유자)

├── app container

└── sidecar container

`pause` 는 아무 것도 안 하는 미니 프로세스. 네임스페이스만 유지.

9.3 Init container 와 sidecar

- **Init container**: Pod 시작 전 순차 실행.

- **Sidecar** (native 2023+): 앱과 나란히 실행되며 독립 재시작 가능.

Envoy (Istio), Fluent Bit 같은 보조 프로세스가 sidecar 로.

9.4 오케스트레이션의 의미

- Scheduling: 어느 노드에 실행?

- Health check, auto-restart.

- Rolling update, canary.

- Service discovery, load balancing.

- Storage, Config, Secret.

- Horizontal autoscaling.

Kubernetes 는 이 모든 걸 declarative YAML 로.

10. 실전 팁

10.1 디버깅 툴킷

docker ps -a # 모든 컨테이너

docker logs -f <container> # 로그 stream

docker exec -it <container> sh # 진입

docker inspect <container> # 상세 정보 (JSON)

docker stats # 실시간 자원 사용

docker events # 이벤트 stream

Kubernetes:

kubectl logs -f <pod>

kubectl exec -it <pod> -- sh

kubectl describe pod <pod>

kubectl get events --sort-by='.lastTimestamp'

10.2 Distroless 에서 디버깅

shell 이 없어서 `exec` 진입 불가. 대안:

kubectl debug <pod> --image=busybox --target=app

또는

docker run --rm -it --pid=container:<id> --net=container:<id> busybox

`pid/net` 네임스페이스를 공유해서 디버깅 컨테이너로 접근.

10.3 이미지 크기 분석

- `docker history <image>`: 레이어별 크기.

- `dive` tool: 레이어 내용 탐색.

- `docker image ls --format="..."` + 포맷팅.

10.4 성능 관찰

컨테이너 자원 사용

docker stats --no-stream

cgroup 직접 보기

cat /sys/fs/cgroup/memory.current

cat /sys/fs/cgroup/cpu.stat

perf 로 프로파일

docker run --cap-add SYS_ADMIN --pid host ...

11. 마치며 — docker run 한 줄의 깊이

`docker run nginx` 한 줄이 실제로 하는 일:

1. Docker Hub 에서 nginx 이미지 레이어 다운로드.

2. OverlayFS 로 레이어 스택 조립.

3. containerd 가 containerd-shim 생성.

4. shim 이 runc 실행.

5. runc 가 7개 네임스페이스 + cgroup + rootfs 설정.

6. seccomp/AppArmor 프로필 적용.

7. `execve("nginx")` 로 프로세스 시작.

8. 컨테이너 네트워크 (veth + bridge) 구성.

9. iptables 포트 포워딩 추가.

50ms 안에 끝난다. 2000년대 VM 을 부팅하던 수 분과 비교하면 기적이다. 그 뒤에는 2006 cgroups, 2008 namespaces, 2013 Docker, 2015 OCI, 2016 overlay2, 2020 CRI 등 10년 넘는 엔지니어링이 있다.

동시에 이 편의성의 대가는 **커널 공유 = 보안 약점**. Lambda, Fargate 같은 상용 플랫폼이 microVM 을 쓰는 이유. 멀티테넌트라면 "컨테이너 = 격리" 를 전적으로 신뢰하지 말 것.

다음 글에서는 **Kubernetes 내부** — etcd 의 Raft 합의, 스케줄러의 필터/스코어링, 컨트롤러 패턴, 커스텀 리소스, CRI/CNI/CSI 의 플러그인 시스템 — 을 파볼 예정이다. YAML 한 장이 수백 대 노드에서 실행되는 과정.

참고 자료

- Jérôme Petazzoni — "Anatomy of a Container: Namespaces, cgroups & some filesystem magic" (LinuxCon 2015).

- Michael Kerrisk — "Understanding Linux Namespaces" (LWN 시리즈).

- OCI Image/Runtime Specifications (GitHub).

- Julia Evans — "Container Networking" 시리즈.

- Liz Rice — "Container Security" (O'Reilly, 2020).

- Kubernetes 공식 문서 — Pods, CNI, CRI.

- Red Hat Crun 블로그.

- Firecracker 논문 (NSDI 2020).

- Google gVisor 논문.

- "The Kubernetes Book" — Nigel Poulton.

현재 단락 (1/299)

많은 개발자가 컨테이너를 "가벼운 VM" 으로 이해한다. 실제로는 완전히 다르다:

작성 글자: 0원문 글자: 10,562작성 단락: 0/299