> [!abstract] Introduction > [가상머신](Virtual%20Machine.md)은 하드웨어 전체를 소프트웨어로 흉내 내어 격리를 달성합니다. 강력하지만 무겁습니다 — 게스트 OS를 통째로 올려야 하니까요. 컨테이너*container*는 같은 목표를 전혀 다른 방식으로 풀어냅니다. **커널 하나를 공유하면서, 커널이 제공하는 격리 프리미티브만으로** 독립된 실행 환경을 만드는 것입니다. 이 글에서는 컨테이너의 핵심 원리인 네임스페이스와 cgroup부터, 컨테이너 이미지의 레이어 구조, 그리고 Docker → containerd → runc로 이어지는 런타임 아키텍처까지를 살펴봅니다. ## 컨테이너란 무엇인가 컨테이너*container*란 **호스트 OS의 커널을 공유하면서, 파일 시스템·네트워크·프로세스 ID 등을 격리하여 독립된 실행 환경을 제공하는 기술**이다. [가상머신](Virtual%20Machine.md)이 하이퍼바이저 위에 게스트 OS를 통째로 올리는 방식이라면, 컨테이너는 호스트 커널 위에서 격리된 [프로세스](Process.md) 그룹을 실행하는 방식이다. 핵심적 차이는 **추상화의 대상**에 있다. 가상머신은 하드웨어를 추상화하여 "가상 컴퓨터"를 만들고, 컨테이너는 운영체제를 추상화하여 "가상 운영 환경"을 만든다. 커널을 공유하므로 게스트 OS를 부팅할 필요가 없어 시작 시간이 밀리초 단위이고, 커널 코드를 중복 적재하지 않아 메모리 사용량도 적다. ```mermaid flowchart TB subgraph vm["가상머신"] direction TB A1["App"] & A2["App"] OS1["게스트 OS"] & OS2["게스트 OS"] HV["하이퍼바이저"] HW["물리 하드웨어"] A1 --> OS1 A2 --> OS2 OS1 & OS2 --> HV --> HW end subgraph container["컨테이너"] direction TB A3["App"] & A4["App"] NS1["namespace + cgroup"] & NS2["namespace + cgroup"] HOS["호스트 OS (공유 커널)"] HW2["물리 하드웨어"] A3 --> NS1 A4 --> NS2 NS1 & NS2 --> HOS --> HW2 end ``` > [!tip] VM vs 컨테이너 상세 비교 > 가상머신과 컨테이너의 격리 강도, 시작 시간, 이미지 크기, 성능 오버헤드 비교는 [가상머신](Virtual%20Machine.md)의 "가상머신 vs 컨테이너" 섹션에서 다루고 있다. 그렇다면 컨테이너의 격리는 구체적으로 **무엇으로** 만들어지는가? 답은 리눅스 커널이 제공하는 두 가지 프리미티브 — **네임스페이스**와 **cgroup** — 에 있다. ## 네임스페이스: "무엇을 볼 수 있는가" 네임스페이스*namespace*[^namespace]는 [프로세스](Process.md)가 볼 수 있는 시스템 자원의 **범위를 제한**하는 커널 기능이다. 네임스페이스 안의 프로세스는 같은 네임스페이스에 속한 자원만 볼 수 있고, 다른 네임스페이스에 속한 자원은 존재하지 않는 것처럼 보인다. 리눅스는 현재 8종의 네임스페이스를 제공하며, 각각이 서로 다른 종류의 자원을 격리한다. | 네임스페이스 | 플래그 | 격리 대상 | 컨테이너에서의 효과 | | ----------- | ----------------- | ---------------------------------------------------------------- | ------------------------------- | | **Mount** | `CLONE_NEWNS` | 파일 시스템 마운트 포인트 | 컨테이너마다 독립된 파일 시스템 트리 | | **PID** | `CLONE_NEWPID` | 프로세스 ID | 컨테이너 내부에서 PID 1부터 시작 | | **Network** | `CLONE_NEWNET` | 네트워크 인터페이스, 라우팅 | 컨테이너마다 독립된 IP 주소와 포트 공간 | | **UTS** | `CLONE_NEWUTS` | 호스트명, 도메인명 | 컨테이너마다 고유한 호스트명 | | **IPC** | `CLONE_NEWIPC` | [IPC](Inter-Process%20Communication.md) 자원 (세마포어, 메시지 큐, 공유 메모리) | 컨테이너 간 IPC 자원 격리 | | **User** | `CLONE_NEWUSER` | UID/GID 매핑 | 컨테이너 내부에서 root여도 호스트에서는 비특권 사용자 | | **Cgroup** | `CLONE_NEWCGROUP` | cgroup 루트 디렉토리 뷰 | 컨테이너가 자신의 cgroup만 볼 수 있음 | | **Time** | `CLONE_NEWTIME` | 부팅 시각, 모노토닉 시각 | 컨테이너마다 독립된 시간 오프셋 (Linux 5.6+) | ### 네임스페이스를 만드는 시스템 호출 네임스페이스는 세 가지 [시스템 호출](System%20Call.md)로 생성·진입한다. `clone()`[^clone]은 새 프로세스를 생성하면서 동시에 새 네임스페이스에 배치한다. 플래그로 어떤 네임스페이스를 새로 만들지 지정한다. 컨테이너 런타임이 컨테이너의 첫 프로세스를 생성할 때 사용하는 핵심 시스템 호출이다. ```c /* PID + Network + Mount 네임스페이스를 새로 생성하며 자식 프로세스 생성 */ clone(child_fn, stack, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL); ``` `unshare()`[^unshare]는 이미 실행 중인 프로세스를 새 네임스페이스로 분리한다. `clone()`이 "태어날 때부터 격리"라면, `unshare()`는 "실행 도중에 격리"다. `setns()`[^setns]는 이미 존재하는 네임스페이스에 합류한다. `docker exec`가 실행 중인 컨테이너에 새 프로세스를 추가할 때 이 시스템 호출을 사용한다. ### PID 네임스페이스: 프로세스 ID의 격리 PID 네임스페이스는 컨테이너가 동작하는 방식을 이해하는 데 특히 중요하다. 새 PID 네임스페이스를 생성하면, 해당 네임스페이스 안에서는 프로세스 번호가 1부터 다시 시작한다. PID 1은 유닉스에서 특별한 의미를 갖는 `init` [프로세스](Process.md)로, 고아 프로세스[^orphan-process]의 부모가 되고 좀비 프로세스를 정리하는 역할을 한다. ```mermaid flowchart TB subgraph host["호스트 PID 네임스페이스"] INIT["PID 1: systemd"] DOCKER["PID 500: containerd"] subgraph c1ns["컨테이너 A PID 네임스페이스"] C1P1["PID 1 (호스트 PID 1001):<br/>nginx master"] C1P2["PID 2 (호스트 PID 1002):<br/>nginx worker"] end subgraph c2ns["컨테이너 B PID 네임스페이스"] C2P1["PID 1 (호스트 PID 2001):<br/>python app"] end end INIT --> DOCKER DOCKER --> C1P1 C1P1 --> C1P2 DOCKER --> C2P1 ``` 컨테이너 A 내부에서 `ps`를 실행하면 PID 1(nginx master)과 PID 2(nginx worker)만 보인다. 호스트의 systemd(PID 1)나 컨테이너 B의 python 앱은 존재하지 않는 것처럼 보인다. 그러나 호스트에서 보면 이들은 모두 일반 프로세스(PID 1001, 1002, 2001)로 보이고 호스트의 도구로 관리할 수 있다. 이것이 "커널을 공유한다"는 말의 구체적 의미다. ### Network 네임스페이스: 독립된 네트워크 스택 Network 네임스페이스는 컨테이너에 독립된 네트워크 인터페이스, IP 주소, 라우팅 테이블, iptables 규칙을 부여한다. 컨테이너 내부에서는 자신만의 `eth0`이 보이지만, 이것은 실제로는 호스트와 연결된 veth(Virtual Ethernet) 페어[^veth]의 한쪽 끝이다. ```mermaid flowchart LR subgraph c1["컨테이너 A"] ETH0_A["eth0<br/>172.17.0.2"] end subgraph c2["컨테이너 B"] ETH0_B["eth0<br/>172.17.0.3"] end subgraph host["호스트"] VETH_A["vethXXX"] VETH_B["vethYYY"] BR["docker0 브리지<br/>172.17.0.1"] ETH_HOST["eth0<br/>(물리 NIC)"] end ETH0_A --- VETH_A ETH0_B --- VETH_B VETH_A --- BR VETH_B --- BR BR --- ETH_HOST ``` Docker는 기본적으로 `docker0`라는 리눅스 브리지*bridge*를 만들고, 각 컨테이너의 veth 페어를 이 브리지에 연결한다. 컨테이너 간 통신은 브리지를 경유하고, 외부 통신은 호스트의 NAT(iptables MASQUERADE)를 거친다. ## cgroup: "얼마나 사용할 수 있는가" 네임스페이스가 "무엇을 볼 수 있는가"를 제한한다면, cgroup(control group)[^cgroup]은 **"얼마나 사용할 수 있는가"**를 제한한다. 프로세스 그룹의 CPU, 메모리, I/O, 네트워크 대역폭 사용량에 상한을 설정하는 커널 기능이다. ### cgroup v2의 계층 구조 현대 리눅스(5.x 이상)에서는 cgroup v2[^cgroup-v2]가 기본이다. cgroup v2는 단일 통합 계층*unified hierarchy*으로, 모든 컨트롤러가 하나의 트리 아래에 존재한다. ![[cgroupHierarchy.svg]] ### 주요 컨트롤러 | 컨트롤러 | 파일 | 역할 | 예시 | |----------|------|------|------| | **cpu** | `cpu.max` | CPU 시간 제한 (대역폭 조절) | `100000 100000` → CPU 1코어 100% | | **memory** | `memory.max` | 메모리 상한 | `536870912` → 512MB 제한 | | **io** | `io.max` | 블록 I/O 대역폭 제한 | `rbps=1048576` → 읽기 1MB/s | | **pids** | `pids.max` | 최대 프로세스 수 | `100` → 포크 폭탄 방지 | `docker run --memory=512m --cpus=1.5`를 실행하면, Docker는 해당 컨테이너의 cgroup 디렉토리에 `memory.max=536870912`, `cpu.max=150000 100000`을 기록한다. 컨테이너 내부의 프로세스가 이 한도를 초과하면 커널이 직접 개입한다 — 메모리 초과 시 OOM Killer[^oom-killer]가 프로세스를 종료하고, CPU 초과 시 스로틀링*throttling*이 적용된다. ### 네임스페이스 + cgroup = 컨테이너 네임스페이스와 cgroup은 각각 독립적인 커널 기능이지만, 이 둘을 조합하면 컨테이너가 된다. $ \text{컨테이너} = \text{네임스페이스 (격리)} + \text{cgroup (자원 제한)} + \text{루트 파일 시스템} $ 여기에 **루트 파일 시스템**이 추가된다. 컨테이너는 Mount 네임스페이스를 통해 호스트와 다른 파일 시스템 트리를 보게 되는데, 이 파일 시스템 트리가 바로 컨테이너 **이미지**에서 온다. ## 컨테이너 이미지: 레이어의 마법 컨테이너 이미지*container image*란 컨테이너의 루트 파일 시스템과 실행 설정을 패키징한 것이다. 하지만 단순한 `tar` 아카이브가 아니다 — 이미지는 **읽기 전용 레이어*layer*의 스택**으로 구성된다. ### 레이어 구조 `Dockerfile`의 각 명령어(`FROM`, `RUN`, `COPY` 등)는 하나의 레이어를 생성한다. 각 레이어는 직전 레이어 대비 변경된 파일만 담고 있으며, 레이어들은 아래에서 위로 쌓인다. ```mermaid flowchart TB subgraph image["이미지 (읽기 전용)"] direction TB L1["레이어 1: Ubuntu 기본 파일 시스템<br/>(FROM ubuntu:22.04)"] L2["레이어 2: apt install python3<br/>(RUN apt install python3)"] L3["레이어 3: pip install flask<br/>(RUN pip install flask)"] L4["레이어 4: 애플리케이션 코드<br/>(COPY app.py /app/)"] end subgraph container_layer["컨테이너 레이어 (읽기/쓰기)"] RW["쓰기 가능 레이어<br/>(런타임에 생성되는 파일)"] end RW --> L4 --> L3 --> L2 --> L1 ``` 이 설계의 핵심 이점은 **레이어 공유**다. 동일한 `ubuntu:22.04` 베이스 이미지를 사용하는 컨테이너 100개가 있어도, 레이어 1은 디스크에 한 번만 저장된다. 각 컨테이너는 자신만의 쓰기 가능 레이어만 추가하면 된다. ### OverlayFS: 레이어를 하나로 합치기 레이어들을 실제로 하나의 파일 시스템처럼 보이게 만드는 것이 OverlayFS[^overlayfs]다. OverlayFS는 리눅스 커널에 내장된 유니언 파일 시스템*union filesystem*으로, 여러 디렉토리를 하나의 디렉토리처럼 합쳐서 보여준다. ``` overlay on /var/lib/docker/overlay2/merged ├── lowerdir = 레이어 1 + 레이어 2 + 레이어 3 + 레이어 4 (읽기 전용) ├── upperdir = 컨테이너 쓰기 레이어 (읽기/쓰기) ├── workdir = OverlayFS 내부 작업 디렉토리 └── merged = 통합된 뷰 (컨테이너가 보는 루트 파일 시스템) ``` 컨테이너 내부에서 파일을 읽으면, OverlayFS는 upperdir부터 아래쪽 lowerdir 순으로 파일을 찾는다. 파일을 수정하면 **Copy-on-Write*CoW***[^cow] 전략이 적용된다 — 원본 파일을 lowerdir에서 upperdir로 복사한 뒤 수정본만 upperdir에 기록한다. 이렇게 하면 읽기 전용 레이어는 절대 변경되지 않으므로, 같은 이미지를 사용하는 다른 컨테이너에 영향이 없다. ### OCI 이미지 스펙 컨테이너 이미지의 형식은 OCI(Open Container Initiative)[^oci] 이미지 스펙*Image Specification*으로 표준화되어 있다. OCI 이미지는 크게 세 부분으로 구성된다. | 구성 요소 | 내용 | |----------|------| | **매니페스트*manifest*** | 이미지의 레이어 목록과 설정 파일에 대한 참조 (SHA256 다이제스트) | | **설정*config*** | 환경 변수, 엔트리포인트, 노출 포트 등 컨테이너 실행 설정 | | **레이어*layers*** | 각 레이어의 파일 시스템 변경분 (`tar+gzip` 아카이브) | 이 표준 덕분에 Docker로 빌드한 이미지를 containerd, Podman, CRI-O 등 어떤 런타임에서든 실행할 수 있다. ## 컨테이너 런타임 아키텍처 "Docker로 컨테이너를 실행한다"고 말하지만, 실제로 컨테이너를 만드는 과정에는 여러 컴포넌트가 관여한다. 이 컴포넌트들의 관계를 이해하면 컨테이너 생태계 전체의 구조가 보인다. ### 런타임의 두 계층 컨테이너 런타임은 **고수준 런타임*high-level runtime***과 **저수준 런타임*low-level runtime***으로 나뉜다. ```mermaid flowchart TB subgraph user["사용자 인터페이스"] CLI["docker CLI"] end subgraph engine["컨테이너 엔진"] DOCKERD["dockerd<br/>(Docker 데몬)"] end subgraph highlevel["고수준 런타임"] CONTAINERD["containerd<br/>(이미지 관리, 컨테이너 수명 관리)"] end subgraph lowlevel["저수준 런타임"] SHIM["containerd-shim"] RUNC["runc<br/>(OCI 런타임)"] end subgraph kernel["리눅스 커널"] NS["namespace"] CG["cgroup"] OVL["OverlayFS"] end CLI -->|"REST API"| DOCKERD DOCKERD -->|"gRPC"| CONTAINERD CONTAINERD -->|"생성"| SHIM SHIM -->|"exec"| RUNC RUNC -->|"clone() + unshare()"| NS RUNC -->|"cgroup 설정"| CG CONTAINERD -->|"마운트"| OVL ``` **`dockerd`**(Docker 데몬)는 사용자의 REST API 요청을 받아 처리하는 프론트엔드다. 이미지 빌드, 네트워크 관리, 볼륨 관리 등 Docker 고유의 기능을 담당하지만, 컨테이너의 실제 생성과 실행은 containerd에 위임한다. **containerd**[^containerd]는 CNCF 산하의 고수준 컨테이너 런타임이다. 이미지 풀*pull*, 스토리지 관리(OverlayFS 마운트), 컨테이너 수명 관리를 담당한다. Kubernetes도 CRI(Container Runtime Interface)[^cri]를 통해 containerd와 직접 통신한다 — Docker 데몬을 거치지 않는다. **runc**[^runc]는 OCI 런타임 스펙의 참조 구현체로, 실제로 커널 시스템 호출을 발행하여 네임스페이스와 cgroup을 설정하는 저수준 런타임이다. 컨테이너를 생성한 후에는 자신의 역할이 끝나 즉시 종료된다. **containerd-shim**[^shim]은 runc와 containerd 사이의 중간 프로세스로, 컨테이너 프로세스의 부모 역할을 맡는다. runc가 종료된 후에도 컨테이너의 stdout/stderr을 수집하고, 컨테이너가 종료되면 exit 코드를 containerd에 보고한다. shim 덕분에 containerd가 재시작되어도 실행 중인 컨테이너에는 영향이 없다. ### OCI 런타임 스펙 runc가 따르는 OCI 런타임 스펙*Runtime Specification*[^oci-runtime]은 컨테이너의 생성·실행·종료를 표준화한다. 핵심은 `config.json` 파일로, 컨테이너의 루트 파일 시스템 경로, 실행할 프로세스, 네임스페이스 목록, cgroup 설정 등을 JSON으로 기술한다. ```json { "ociVersion": "1.0.2", "process": { "args": ["nginx", "-g", "daemon off;"], "cwd": "/", "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"] }, "root": { "path": "rootfs", "readonly": true }, "linux": { "namespaces": [ { "type": "pid" }, { "type": "network" }, { "type": "mount" }, { "type": "ipc" }, { "type": "uts" } ], "resources": { "memory": { "limit": 536870912 }, "cpu": { "quota": 150000, "period": 100000 } } } } ``` 이 스펙 덕분에 runc를 다른 OCI 호환 런타임(crun[^crun], gVisor, Kata Containers)으로 교체할 수 있다. 고수준 런타임(containerd)은 어떤 저수준 런타임이 사용되는지 알 필요가 없다. ## `docker run`의 이면 지금까지 살펴본 모든 개념이 하나로 합쳐지는 순간을 보자. `docker run -d --name web --memory=512m nginx`를 실행하면, 내부에서는 다음과 같은 일이 벌어진다. ```mermaid sequenceDiagram participant User as docker CLI participant D as dockerd participant C as containerd participant S as containerd-shim participant R as runc participant K as 커널 User->>D: POST /containers/create D->>C: gRPC: CreateContainer C->>C: nginx 이미지 레이어 확인 (없으면 pull) C->>K: OverlayFS 마운트 (lowerdir=레이어들, upperdir=쓰기 레이어) C->>C: OCI bundle 생성 (config.json + rootfs) C->>S: containerd-shim 프로세스 생성 S->>R: runc create R->>K: clone(CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | ...) R->>K: cgroup 디렉토리 생성, memory.max=536870912 R->>K: pivot_root()로 루트 파일 시스템 전환 R->>S: 컨테이너 프로세스 준비 완료 R-->>R: runc 종료 User->>D: POST /containers/start D->>C: gRPC: StartContainer C->>S: 시작 신호 S->>K: 컨테이너 프로세스 실행 (nginx) Note over S,K: shim이 nginx의 부모 프로세스로 남음 ``` **① 이미지 준비**: containerd가 nginx 이미지의 레이어들이 로컬에 있는지 확인하고, 없으면 레지스트리에서 풀한다. 레이어들을 OverlayFS로 마운트하여 통합된 루트 파일 시스템을 만든다. **② OCI 번들 생성**: containerd가 `config.json`(네임스페이스, cgroup 설정, 실행 명령어 포함)과 rootfs 경로를 포함하는 OCI 번들*bundle*을 생성한다. **③ 컨테이너 생성**: containerd-shim을 생성하고, shim이 runc를 실행한다. runc는 `clone()` [시스템 호출](System%20Call.md)로 새 네임스페이스들을 생성하고, cgroup 디렉토리에 자원 제한을 기록하고, `pivot_root()`[^pivot-root]로 루트 파일 시스템을 전환한다. 모든 준비가 끝나면 runc는 종료되고, 컨테이너 프로세스의 부모는 containerd-shim이 된다. **④ 컨테이너 실행**: 시작 신호를 받으면 컨테이너의 1번 프로세스(nginx)가 실행된다. 이 시점부터 nginx는 격리된 네임스페이스 안에서, cgroup의 자원 제한 아래에서, OverlayFS 위에서 동작하는 — 즉 컨테이너 안에서 실행되는 상태가 된다. ## 컨테이너의 보안 경계 컨테이너는 커널을 공유한다는 근본적 특성 때문에, [가상머신](Virtual%20Machine.md)보다 격리 수준이 약하다. 네임스페이스와 cgroup만으로는 커널 취약점을 통한 탈출*container escape*을 막을 수 없다. 이를 보완하기 위해 여러 보안 메커니즘이 추가로 사용된다. | 메커니즘 | 역할 | |----------|------| | **seccomp**[^seccomp] | 컨테이너가 호출할 수 있는 시스템 호출을 화이트리스트/블랙리스트로 제한 | | **AppArmor / SELinux** | MAC(Mandatory Access Control)으로 파일·네트워크 접근을 제어 | | **capabilities** | 리눅스의 root 권한을 세분화하여 컨테이너에 필요한 최소 권한만 부여 | | **User 네임스페이스** | 컨테이너 내부의 root를 호스트의 비특권 사용자로 매핑 (rootless 컨테이너) | | **read-only rootfs** | 이미지 레이어를 읽기 전용으로 마운트하여 파일 시스템 변조 방지 | Docker는 기본적으로 약 300개의 시스템 호출 중 약 50개를 seccomp으로 차단하고, `CAP_SYS_ADMIN` 등 위험한 capability를 제거한 상태로 컨테이너를 실행한다. 그럼에도 커널을 공유한다는 사실 자체가 보안 경계이므로, 멀티테넌트 환경에서는 VM 안에서 컨테이너를 실행하거나, gVisor·Kata Containers 같은 샌드박스 런타임을 사용하여 격리를 강화하는 구성이 일반적이다. --- [^namespace]: 리눅스 네임스페이스*namespace*는 프로세스가 볼 수 있는 시스템 자원의 범위를 제한하는 커널 기능이다. 2002년 Linux 2.4.19에서 Mount 네임스페이스가 최초 도입된 후, 점진적으로 확장되어 현재 8종이 존재한다. — `man 7 namespaces`; Kerrisk, M. (2013) 'Namespaces in operation', *LWN.net*. [^clone]: `clone()`은 `fork()`의 확장판으로, 플래그를 통해 부모와 자식이 공유할 자원(메모리, 파일 디스크립터, 네임스페이스 등)을 세밀하게 제어할 수 있다. [스레드](Thread.md)도 `clone(CLONE_VM | CLONE_FS | CLONE_FILES | ...)`로 생성된다. — `man 2 clone`. [^unshare]: `unshare()`는 호출한 프로세스를 지정된 네임스페이스에서 분리하여 새 네임스페이스로 이동시키는 시스템 호출이다. `unshare` 명령어로 셸에서 직접 사용할 수도 있다 — 예: `unshare --pid --mount-proc --fork bash`. — `man 2 unshare`. [^setns]: `setns()`는 `/proc/[pid]/ns/` 아래의 네임스페이스 파일 디스크립터를 열어, 호출한 프로세스를 해당 네임스페이스에 합류시키는 시스템 호출이다. — `man 2 setns`. [^orphan-process]: 고아 프로세스*orphan process*란 부모 프로세스가 먼저 종료되어, PID 1(init)이 새 부모가 된 프로세스를 말한다. 컨테이너의 PID 네임스페이스에서는 해당 네임스페이스의 PID 1이 고아 프로세스를 입양한다. — Silberschatz, Galvin and Gagne (2018), §3.3. [^veth]: veth(Virtual Ethernet)는 항상 쌍으로 생성되는 가상 네트워크 인터페이스다. 한쪽 끝에 들어간 패킷은 다른 쪽 끝에서 나온다. 한쪽을 컨테이너의 Network 네임스페이스에, 다른 쪽을 호스트의 브리지에 연결하면 컨테이너의 네트워크 통신 경로가 완성된다. — `man 4 veth`. [^cgroup]: cgroup(control group)은 프로세스 그룹의 자원 사용량을 계층적으로 제한·모니터링하는 리눅스 커널 기능이다. 2008년 Google 엔지니어 Paul Menage와 Rohit Seth이 개발하여 Linux 2.6.24에 도입되었다. — `Documentation/admin-guide/cgroup-v2.rst`. [^cgroup-v2]: cgroup v2(unified hierarchy)는 cgroup v1의 설계적 문제(컨트롤러별 개별 계층, 비일관적 인터페이스)를 해결하기 위해 재설계된 버전이다. Linux 4.5에서 도입되었으며, systemd 248+와 Docker 20.10+에서 기본 지원한다. 핵심 변화는 프로세스가 단일 계층의 하나의 리프 노드에만 속할 수 있다는 점이다. — Heo, T. (2015) 'Control Group v2', `Documentation/admin-guide/cgroup-v2.rst`. [^oom-killer]: OOM Killer(Out-Of-Memory Killer)는 시스템 메모리가 부족할 때 커널이 프로세스를 강제 종료하여 메모리를 확보하는 메커니즘이다. cgroup의 `memory.max`를 초과하면 해당 cgroup 내 프로세스에 대해 OOM Kill이 발생한다. — `Documentation/admin-guide/mm/concepts.rst`. [^overlayfs]: OverlayFS는 Linux 3.18(2014년)에 메인라인 커널에 포함된 유니언 파일 시스템이다. Docker의 기본 스토리지 드라이버인 `overlay2`가 이를 사용한다. 이전에는 AUFS, devicemapper 등이 사용되었으나, OverlayFS가 커널 메인라인에 포함되면서 사실상 표준이 되었다. — `Documentation/filesystems/overlayfs.rst`. [^cow]: Copy-on-Write(CoW)는 자원을 복사하는 시점을 실제 수정이 발생하는 시점까지 지연하는 전략이다. `fork()` 시 부모-자식 프로세스의 메모리 공유, OverlayFS의 파일 수정, B-tree 파일 시스템(Btrfs, ZFS) 등에서 널리 사용된다. [^oci]: OCI(Open Container Initiative)는 2015년 Docker, CoreOS, Google 등이 설립한 리눅스 재단 산하 프로젝트로, 컨테이너 이미지 형식과 런타임 인터페이스를 표준화한다. — https://opencontainers.org/. [^containerd]: containerd는 Docker에서 분리되어 2017년 CNCF에 기부된 고수준 컨테이너 런타임이다. Docker Engine, Kubernetes(CRI 플러그인 경유), AWS Fargate 등에서 사용된다. — https://containerd.io/. [^cri]: CRI(Container Runtime Interface)는 Kubernetes가 컨테이너 런타임과 통신하기 위한 gRPC 인터페이스다. Kubernetes 1.24부터 dockershim이 제거되어, containerd나 CRI-O를 직접 사용해야 한다. — Kubernetes Documentation, 'Container Runtime Interface'. [^runc]: runc는 Docker가 개발하여 OCI에 기부한 저수준 컨테이너 런타임이다. Go로 작성되었으며, `libcontainer` 라이브러리를 기반으로 리눅스 커널의 네임스페이스, cgroup, seccomp, capabilities 등을 직접 조작한다. — https://github.com/opencontainers/runc. [^shim]: containerd-shim은 컨테이너 프로세스의 직접적 부모 역할을 하는 경량 프로세스다. (1) runc 종료 후 컨테이너의 stdin/stdout/stderr를 유지하고, (2) containerd 재시작 시에도 컨테이너가 영향받지 않도록 분리(decoupling)하며, (3) 컨테이너 종료 시 exit 상태를 보고한다. — containerd documentation, 'Architecture'. [^oci-runtime]: OCI 런타임 스펙은 `config.json` 파일의 형식과, 컨테이너의 생명주기(creating → created → running → stopped) 상태 머신을 정의한다. — Open Container Initiative, *Runtime Specification*, v1.0.2. [^pivot-root]: `pivot_root()`는 현재 프로세스의 루트 파일 시스템을 교체하는 시스템 호출이다. `chroot()`보다 보안성이 높은데, `chroot()`은 프로세스의 루트 디렉토리만 변경하여 탈출이 가능한 반면, `pivot_root()`은 마운트 네임스페이스의 루트 자체를 교체한다. — `man 2 pivot_root`. [^crun]: crun은 Red Hat이 개발한 C 언어 기반의 OCI 런타임으로, runc(Go 기반)보다 시작 시간이 빠르고 메모리 사용량이 적다. Podman의 기본 런타임이다. — https://github.com/containers/crun. [^seccomp]: seccomp(Secure Computing Mode)은 프로세스가 호출할 수 있는 시스템 호출을 BPF 필터로 제한하는 리눅스 커널 기능이다. Docker는 기본 seccomp 프로파일로 `reboot()`, `kexec_load()`, `mount()` 등 위험한 시스템 호출을 차단한다. — `man 2 seccomp`. ## 출처 - Silberschatz, A., Galvin, P. B., and Gagne, G. (2018) *Operating System Concepts*. 10th ed. Hoboken, NJ: Wiley. - Tanenbaum, A. S. and Bos, H. (2023) *Modern Operating Systems*. 5th ed. London: Pearson. - Kerrisk, M. (2013) 'Namespaces in operation, part 1: namespaces overview', *LWN.net*. - Heo, T. (2015) 'Control Group v2', Linux kernel documentation, `Documentation/admin-guide/cgroup-v2.rst`. - Merkel, D. (2014) 'Docker: Lightweight Linux Containers for Consistent Development and Deployment', *Linux Journal*, 2014(239). - Open Container Initiative. *Image Format Specification*, v1.0.2. - Open Container Initiative. *Runtime Specification*, v1.0.2. - Biederman, E. W. (2006) 'Multiple Instances of the Global Linux Namespaces', *Proceedings of the Linux Symposium*. - Linux kernel source v6.19 — `kernel/nsproxy.c`, `kernel/cgroup/`, `fs/overlayfs/`. - Docker Documentation — https://docs.docker.com/engine/reference/. - containerd Documentation — https://containerd.io/docs/.