Linux Container Runtime 심화

컨테이너는 VM이 아니다

Docker 컨테이너는 가상 머신처럼 보이지만, 실제로는 Linux 커널의 격리 기능을 조합한 프로세스입니다. 컨테이너를 제대로 이해하려면 그 내부를 구성하는 세 가지 핵심 기술을 알아야 합니다: Namespace, Cgroup, 그리고 OCI Runtime.

Linux Namespace: 프로세스 격리의 핵심

Namespace는 커널 리소스를 프로세스 단위로 격리합니다. 컨테이너 내부의 프로세스는 자신만의 독립된 환경을 가진 것처럼 보입니다.

Namespace 격리 대상 효과
PID 프로세스 ID 컨테이너 내 PID 1부터 시작
NET 네트워크 스택 독립된 IP, 포트, 라우팅 테이블
MNT 파일시스템 마운트 독립된 마운트 포인트
UTS 호스트명 컨테이너별 hostname 설정
IPC 프로세스 간 통신 독립된 세마포어, 메시지 큐
USER 사용자/그룹 ID 컨테이너 내 root ≠ 호스트 root
CGROUP cgroup 루트 디렉토리 자신의 cgroup 트리만 조회

직접 namespace를 만들어 컨테이너의 격리를 체험할 수 있습니다.

# 새로운 PID + NET + MNT namespace에서 bash 실행
sudo unshare --pid --net --mount --fork bash

# 컨테이너 안에서 확인
hostname           # 호스트명은 같음 (UTS namespace 미지정)
ip addr            # 네트워크 인터페이스 없음 (loopback만)
ps aux             # PID 격리 확인 (proc 마운트 필요)
mount -t proc proc /proc
ps aux             # PID 1이 현재 bash

# 호스트에서 확인
sudo lsns -t pid   # namespace 목록 조회

Cgroup v2: 리소스 제한의 기반

Namespace가 격리를 담당한다면, Cgroup(Control Group)은 리소스 사용량을 제한합니다. 최신 배포판은 Cgroup v2를 기본 사용합니다.

# Cgroup v2 확인
mount | grep cgroup2
# cgroup2 on /sys/fs/cgroup type cgroup2 ...

# 수동으로 cgroup 생성 및 메모리 제한
sudo mkdir /sys/fs/cgroup/my-container
echo "104857600" | sudo tee /sys/fs/cgroup/my-container/memory.max  # 100MB
echo "50000 100000" | sudo tee /sys/fs/cgroup/my-container/cpu.max  # CPU 50%

# 프로세스를 cgroup에 할당
echo $$ | sudo tee /sys/fs/cgroup/my-container/cgroup.procs

# 메모리 사용량 확인
cat /sys/fs/cgroup/my-container/memory.current
컨트롤러 파일 역할
memory memory.max 메모리 상한 (초과 시 OOM Kill)
cpu cpu.max CPU 시간 제한 (quota/period)
io io.max 블록 I/O 대역폭 제한
pids pids.max 프로세스 수 제한 (fork bomb 방지)

K8s kubectl debug 트러블슈팅에서 다루는 Pod 디버깅도 결국 이 cgroup 경계 안에서 동작합니다.

OCI Runtime Spec: 표준화된 컨테이너 실행

OCI(Open Container Initiative)는 컨테이너 이미지와 런타임의 표준 스펙입니다. 이 표준 덕분에 Docker, Podman, containerd 등이 동일한 이미지를 실행할 수 있습니다.

# OCI 번들 구조
my-container/
├── config.json    # 런타임 설정 (namespace, cgroup, mounts, entrypoint)
└── rootfs/        # 루트 파일시스템
    ├── bin/
    ├── etc/
    ├── lib/
    └── ...
// config.json 핵심 구조
{
  "ociVersion": "1.1.0",
  "process": {
    "terminal": false,
    "user": { "uid": 0, "gid": 0 },
    "args": ["/app/server"],
    "env": ["PATH=/usr/bin:/bin", "NODE_ENV=production"],
    "cwd": "/app"
  },
  "root": {
    "path": "rootfs",
    "readonly": true
  },
  "linux": {
    "namespaces": [
      { "type": "pid" },
      { "type": "network" },
      { "type": "mount" },
      { "type": "ipc" },
      { "type": "uts" }
    ],
    "resources": {
      "memory": { "limit": 104857600 },
      "cpu": { "quota": 50000, "period": 100000 }
    }
  }
}

runc: 저수준 컨테이너 런타임

runc는 OCI 스펙을 구현하는 참조 런타임입니다. Docker와 Kubernetes 모두 최종적으로 runc를 호출하여 컨테이너를 생성합니다.

# runc로 직접 컨테이너 실행
# 1. 루트 파일시스템 준비
mkdir -p my-container/rootfs
docker export $(docker create alpine:latest) | tar -C my-container/rootfs -xf -

# 2. OCI config 생성
cd my-container
runc spec  # 기본 config.json 생성

# 3. 컨테이너 생성 및 실행
sudo runc create my-test-container
sudo runc start my-test-container

# 4. 상태 확인
sudo runc list
sudo runc state my-test-container

# 5. 정리
sudo runc delete my-test-container

runc의 라이프사이클은 명확합니다: create → start → (running) → kill → delete. 이것이 모든 컨테이너의 기본 생명주기입니다.

containerd: 고수준 컨테이너 런타임

containerd는 runc 위에서 동작하는 데몬으로, 이미지 관리, 스냅샷, 네트워킹 등 컨테이너 운영에 필요한 고수준 기능을 제공합니다. Kubernetes는 CRI(Container Runtime Interface)를 통해 containerd와 통신합니다.

# 호출 체인 (Kubernetes 기준)
kubelet → CRI → containerd → containerd-shim-runc-v2 → runc → 컨테이너 프로세스

# containerd 직접 사용 (ctr CLI)
sudo ctr images pull docker.io/library/nginx:alpine
sudo ctr images list

# 컨테이너 생성 및 실행
sudo ctr run --rm docker.io/library/nginx:alpine my-nginx

# 네임스페이스 확인 (containerd의 논리적 격리)
sudo ctr namespaces list
# default
# moby        ← Docker가 사용하는 네임스페이스
# k8s.io      ← Kubernetes가 사용하는 네임스페이스
구분 runc containerd Docker
역할 컨테이너 생성/실행 이미지+컨테이너 관리 UX + 빌드 + 네트워킹
레벨 Low-level runtime High-level runtime Container engine
데몬 없음 (바이너리) containerd 데몬 dockerd 데몬
K8s 연동 직접 불가 CRI 네이티브 cri-dockerd 필요

containerd-shim: 왜 필요한가

containerd-shim은 containerd와 runc 사이의 중간 프로세스입니다. 핵심 역할은 두 가지입니다.

  • containerd 재시작 허용: shim이 컨테이너의 부모 프로세스가 되므로, containerd를 업데이트해도 실행 중인 컨테이너에 영향 없음
  • STDIO 관리: 컨테이너의 stdout/stderr를 로그 파일로 전달
# 실행 중인 shim 프로세스 확인
ps aux | grep containerd-shim
# root  1234  containerd-shim-runc-v2 -namespace k8s.io -id abc123...

# 프로세스 트리
pstree -p $(pgrep containerd-shim | head -1)
# containerd-shim(1234)─┬─nginx(1235)
#                        └─nginx(1236)

보안 강화: rootless와 gVisor

기본 runc는 호스트 커널을 공유하므로, 추가 격리 옵션을 고려해야 합니다.

# Rootless 컨테이너 (Docker)
# 컨테이너 프로세스가 호스트의 비특권 사용자로 실행
dockerd-rootless-setuptool.sh install
docker context use rootless

# gVisor (runsc) - 사용자 공간 커널
# runc 대신 runsc를 런타임으로 등록
cat /etc/containerd/config.toml
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
#   runtime_type = "io.containerd.runsc.v1"

# K8s RuntimeClass로 Pod에 적용
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc

K8s Pod Security Standards와 함께 적용하면 컨테이너 보안을 다층으로 강화할 수 있습니다.

운영 트러블슈팅

# 컨테이너의 namespace 정보 확인
sudo ls -la /proc/<PID>/ns/
# lrwxrwxrwx 1 root root 0 pid -> 'pid:[4026532456]'
# lrwxrwxrwx 1 root root 0 net -> 'net:[4026532518]'

# 컨테이너 네트워크 namespace에 진입
sudo nsenter --target <PID> --net ip addr
sudo nsenter --target <PID> --pid --mount ps aux

# cgroup 리소스 사용량 실시간 모니터링
watch -n 1 cat /sys/fs/cgroup/system.slice/docker-<ID>.scope/memory.current

# OOM Kill 로그 확인
dmesg | grep -i "oom|killed process"
journalctl -k | grep -i oom

컨테이너는 마법이 아닙니다. Namespace로 격리하고, Cgroup으로 제한하고, OCI 스펙으로 표준화한 Linux 프로세스입니다. 이 기반을 이해하면 Docker 문제 디버깅, Kubernetes 노드 트러블슈팅, 보안 강화 설계까지 한 단계 깊이 있는 인프라 운영이 가능합니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux