리눅스 컨테이너 기술 완전정복
Updated:
리눅스 컨테이너 기술 완전정복
도커와 쿠버네티스 공부에 앞서 리눅스 컨테이너 기술에 대해 공부 및 실습을 하였다.
목차
- 컨테이너란 무엇인가
- 리눅스가 제공하는 두 축: Namespaces와 cgroups
- 루트 파일시스템과 레이어드 스토리지 (OverlayFS)
- OCI(오픈 컨테이너 이니셔티브) 표준과 런타임 스택
- 컨테이너 네트워킹 한눈에 보기 (veth, bridge, iptables/nftables, CNI)
- 컨테이너 보안: capabilities, seccomp, AppArmor/SELinux, rootless
- 실습 A:
unshare+chroot로 “핸드메이드 컨테이너” 만들기 - 실습 B:
runc로 OCI 번들 실행하기 - 실습 C: veth 페어와 브리지로 간단한 컨테이너 네트워크 구성
- 프로세스/신호/로그: “PID 1 문제”와 init
- 성능 이야기: copy-on-write, 페이지 캐시, cgroup v2
- 디버깅/관찰 도구 모음(nsenter, strace, perf, eBPF)
- VM과 컨테이너 차이 요약
1) 컨테이너란 무엇인가
- 정의: 컨테이너는 리눅스 커널 기능(네임스페이스, cgroups, FS 격리 등)을 조합해 만든 프로세스 격리 실행환경입니다. 하이퍼바이저 기반 VM처럼 OS 전체를 가상화하지 않고, 호스트 커널을 공유합니다.
-
핵심 특징
- 시작/종료가 빠름(프로세스 스폰 수준)
- 부팅이 아닌 프로세스 시작 수준 → 수 ms~수 초.
- 이미지 레이어 재사용(COW) → 다운로드/배포 효율.
- 이미지 레이어로 배포 용이
- 밀집 배치 가능(오버헤드↓)
- 커널을 공유하므로 메모리/CPU 오버헤드가 작음 → 고밀도 배치에 유리.
- 대신 커널을 공유하므로 커널 취약점은 공통 리스크
- 시작/종료가 빠름(프로세스 스폰 수준)
2) 리눅스의 두 축: Namespaces & cgroups
Namespaces (격리)
- PID: 프로세스 트리 격리 (컨테이너 내부 PID 1은 호스트에선 일반 PID)
- UTS: hostname, domainname 격리
- IPC: System V IPC, POSIX message queue 격리
- NET: 네트워크 스택 격리 (veth, 라우팅 테이블, iptables 등 별도)
- MNT: 마운트 네임스페이스(루트FS, 마운트포인트 격리)
- USER: UID/GID 매핑을 통한 권한 격리(루트리스의 핵심)
- CGROUP: cgroup 네임스페이스(경로 격리)
cgroups (자원 제어)
- CPU: shares, quota/period로 CPU 할당 제한
- 메모리: 상한/스왑/oom 정책
- IO/Block: IOPS/스루풋 제한
- PIDs: 프로세스 개수 제한
- cgroup v2: 단일 계층 계보, 일관된 컨트롤러 모델
컨테이너 = 여러 네임스페이스 + cgroup 제한 + 독립 루트FS 조합
3) 루트 파일시스템과 레이어드 스토리지
- 컨테이너 루트FS는 보통 이미지 레이어의 합성 결과(읽기 전용) + 쓰기 레이어(읽기-쓰기)로 구성됩니다.
- OverlayFS: 상단(upperdir)과 하단(lowerdir)을 합쳐 보이는 union FS. 쓰기 시 copy-up이 일어남(COW, Copy-On-Write).
- 장점: 이미지 레이어 재사용으로 배포/캐시 효율 ↑
- 주의: 작은 파일 다량 write/rename 패턴은 copy-up 비용 고려
4) OCI 표준과 런타임 스택
- OCI Image Spec: 이미지 포맷 표준 (레이어, config)
- OCI Runtime Spec: 컨테이너 실행 번들(루트FS + config.json) 표준
-
런타임 계층
- High-level: Docker Engine, containerd, CRI-O, Podman
- Low-level: runc, crun (OCI Runtime 구현)
-
도식
CLI → (Docker/Podman) → containerd/CRI-O → runc/crun → 리눅스 커널(Namespaces/cgroups)
5) 컨테이너 네트워킹 핵심
- veth pair: 양 끝이 연결된 가상 이더넷 NIC. 한쪽은 호스트 네임스페이스, 다른쪽은 컨테이너 네임스페이스에 붙임.
- 브리지(bridge): 리눅스 브리지(
br0,docker0)가 L2 스위치 역할. 컨테이너 veth를 브리지에 붙여 L2 통신. - IPAM: IP 할당. 단일 호스트는 브리지+NAT가 보편.
- NAT/포워딩: iptables/nftables로 SNAT/DNAT 수행(포트포워딩).
- Kubernetes: CNI 플러그인(Flannel, Calico, Cilium 등)이 네트워크를 자동 구성.
6) 컨테이너 보안
- Linux capabilities: 루트 권한을 세분화. 컨테이너는 기본적으로 일부 capability만 가짐.
- seccomp: 허용된 시스템 콜 화이트리스트로 커널 면적 축소.
- AppArmor/SELinux: LSM을 통한 MAC 정책 적용.
- rootless: USER 네임스페이스로 호스트 root 없이 컨테이너 실행.
- 이미지 서명/스캐닝: 공급망 안전(SBOM, cosign, trivy 등).
7) 실습 A: unshare + chroot로 핸드메이드 컨테이너
목적: 도커 없이 리눅스 원시 도구만으로 “컨테이너 같은 것”을 체험.
busybox 설치
sudo apt-get update && sudo apt-get install -y busybox-static uidmap
루트FS 만들기
mkdir -p ~/mini-root/{bin,proc,sys,dev}
cp /bin/busybox ~/mini-root/bin/
# busybox 링크 생성
sudo chroot ~/mini-root /bin/busybox --install -s /bin
격리된 네임스페이스 생성 후 진입
sudo unshare --fork --pid --mount --uts --ipc --net --mount-proc bash -lc '
hostname mini
mount -t proc proc ~/mini-root/proc
chroot ~/mini-root /bin/sh -lc "echo \"Hello from $(hostname)\"; ps; sleep 1000"
'

8) 실습 B: runc로 OCI 번들 실행하기
runc 설치
OCI 번들 구성
mkdir -p ~/oci-bundle/rootfs
cp /bin/busybox ~/oci-bundle/rootfs/bin/
sudo chroot ~/oci-bundle/rootfs /bin/busybox --install -s /bin
cat > ~/oci-bundle/config.json <<'JSON'
{
"ociVersion": "1.1.0",
"process": {
"terminal": true,
"args": ["/bin/sh"],
"cwd": "/",
"user": { "uid": 0, "gid": 0 },
"capabilities": { "bounding": ["CAP_CHOWN","CAP_DAC_OVERRIDE","CAP_FSETID","CAP_FOWNER","CAP_MKNOD","CAP_NET_RAW","CAP_SETGID","CAP_SETUID","CAP_SETFCAP","CAP_SETPCAP","CAP_NET_BIND_SERVICE","CAP_SYS_CHROOT","CAP_KILL","CAP_AUDIT_WRITE"] }
},
"root": { "path": "rootfs", "readonly": false },
"mounts": [
{ "destination": "/proc", "type": "proc", "source": "proc" }
],
"linux": {
"namespaces": [
{"type": "pid"}, {"type": "uts"}, {"type": "ipc"}, {"type": "mount"}, {"type": "network"}
]
}
}
JSON
실행
cd ~/oci-bundle
runc run mini
9) 실습 C: veth + bridge로 네트워크 체험
# 브리지 생성
sudo ip link add br0 type bridge
sudo ip link set br0 up
sudo ip addr add 10.10.0.1/24 dev br0
# veth 페어 생성
sudo ip link add veth-host type veth peer name veth-ns
sudo ip link set veth-host master br0
sudo ip link set veth-host up
# 네트워크 네임스페이스 생성 및 NIC 이동
sudo ip netns add blue
sudo ip link set veth-ns netns blue
sudo ip netns exec blue ip link set lo up
sudo ip netns exec blue ip addr add 10.10.0.2/24 dev veth-ns
sudo ip netns exec blue ip link set veth-ns up
# 통신 확인
ping -c 2 10.10.0.2
sudo ip netns exec blue ping -c 2 10.10.0.1

10) 프로세스/신호/로그: PID 1 문제
- 컨테이너의 PID 1은 좀비 프로세스 수거와 신호 전달에 책임. bash가 PID 1이면 신호 처리가 기대와 다를 수 있음.
- 해결책:
tini,dumb-init, 또는 애플리케이션이 직접 SIGTERM/SIGINT 처리 및 reaping 구현. - 로그는 stdout/stderr로 흘리고, 수집기는 런타임/노드에서 담당(도커 로그 드라이버, K8s 로깅).
11) 성능 이야기
- Copy-on-Write: 처음 쓰기 비용 증가 가능 → 쓰기 많은 경로는 별도 볼륨 마운트 고려.
- 페이지 캐시 공유: 동일 레이어 재사용으로 메모리 이점.
- CPU/메모리/IO 제한: cgroup v2로 일관된 제어, OOMKiller 동작 이해 필요.
12) 디버깅/관찰 도구
- nsenter: 다른 네임스페이스에 진입
- strace: 시스템콜 추적
- lsof / ss / ip / ethtool: FD/소켓/링크 확인
- perf / eBPF(bpftrace, bcc): 커널/성능 분석
- criu: 컨테이너 체크포인트/복원
13) VM vs 컨테이너 요약
| 항목 | VM | 컨테이너 |
|---|---|---|
| 커널 | 게스트 커널 | 호스트 커널 공유 |
| 격리 | 강함(하이퍼바이저) | 프로세스 격리(커널 공유) |
| 부팅속도 | 수 초~분 | 수 밀리초~초 |
| 밀도 | 낮음 | 높음 |
| 보안 경계 | 강함 | 커널 취약점 공유 리스크 |
마무리
컨테이너는 리눅스 커널 기능의 조합입니다. 이 본질을 이해하면 도커/쿠버네티스의 많은 동작이 투명해집니다. 위의 실습을 먼저 손으로 익히고, 그 다음 도커/쿠버네티스 개념으로 확장해보세요. 블로그 게시 전, 본문 상단에 배포 환경(테스트 리눅스 배포판/커널 버전)을 명시하면 독자 재현성이 더 좋아집니다.
댓글남기기