컨테이너와 Docker
가상 머신은 훌륭한 격리를 제공하지만, 각 VM이 완전한 OS를 포함하므로 무겁습니다. 부팅에 수십 초가 걸리고, 수 GB의 디스크를 차지합니다. 컨테이너는 같은 커널을 공유하면서 프로세스를 격리하는 경량 가상화 기술입니다. 2013년 Docker의 등장 이후 컨테이너는 소프트웨어 배포의 표준이 되었습니다.
컨테이너 vs 가상 머신
┌─────────────────────────────┐
│ VM 1: App A + Guest OS │
├─────────────────────────────┤
│ VM 2: App B + Guest OS │
├─────────────────────────────┤
│ Hypervisor │
├─────────────────────────────┤
│ Host OS │
├─────────────────────────────┤
│ Hardware │
└─────────────────────────────┘┌─────────────────────────────┐
│ Container A: App A + Libs │
├─────────────────────────────┤
│ Container B: App B + Libs │
├─────────────────────────────┤
│ Container C: App C + Libs │
├─────────────────────────────┤
│ Container Runtime │
├─────────────────────────────┤
│ Host OS (커널 공유) │
├─────────────────────────────┤
│ Hardware │
└─────────────────────────────┘| 비교 | 가상 머신 | 컨테이너 |
|---|---|---|
| 격리 수준 | OS 전체 격리 (커널 독립) | 프로세스 수준 격리 (커널 공유) |
| 시작 시간 | 수십 초 ~ 수 분 | 수 밀리초 ~ 수 초 |
| 이미지 크기 | 수 GB | 수십 MB ~ 수백 MB |
| 메모리 오버헤드 | OS당 수백 MB | 프로세스 크기 |
| 밀도 | 서버당 수십 개 | 서버당 수백~수천 개 |
| 보안 격리 | 강함 (하이퍼바이저 경계) | 상대적으로 약함 |
컨테이너는 같은 커널을 공유하므로, 커널 취약점이 발견되면 모든 컨테이너가 위험해질 수 있습니다. 멀티테넌트 환경에서는 VM을 사용하거나, VM 위에서 컨테이너를 실행하는 방식을 택합니다.
리눅스 컨테이너의 핵심 기술
컨테이너는 새로운 가상화 기술이 아닙니다. 리눅스 커널에 이미 있는 두 가지 기능의 조합입니다.
네임스페이스 (Namespaces)
프로세스에게 시스템 자원의 독립된 뷰를 제공합니다. 컨테이너 안의 프로세스는 외부를 볼 수 없습니다.
| 네임스페이스 | 격리 대상 | 효과 |
|---|---|---|
| PID | 프로세스 ID | 컨테이너 안에서 PID 1번부터 시작 |
| NET | 네트워크 스택 | 독립된 IP, 포트, 라우팅 테이블 |
| MNT | 파일 시스템 마운트 | 독립된 루트 파일 시스템 |
| UTS | 호스트 이름 | 컨테이너마다 다른 hostname |
| IPC | 프로세스 간 통신 | 독립된 메시지 큐, 세마포어 |
| USER | 사용자/그룹 ID | 컨테이너 안 root ≠ 호스트 root |
| CGROUP | cgroup 뷰 | 자원 제한의 독립적 뷰 |
#define _GNU_SOURCE
#include <stdio.h>
#include <sched.h>
#include <unistd.h>
#include <sys/wait.h>
/* 새 네임스페이스에서 실행될 함수 */
int child_func(void *arg) {
printf("자식 PID (네임스페이스 내부): %d\n", getpid()); /* 1 */
char hostname[] = "container";
sethostname(hostname, sizeof(hostname) - 1);
/* 새 네임스페이스에서 셸 실행 */
execlp("/bin/sh", "sh", NULL);
return 0;
}
int main() {
char stack[65536];
/* 새 PID + UTS + NET 네임스페이스로 프로세스 생성 */
pid_t pid = clone(
child_func,
stack + sizeof(stack),
CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNET | SIGCHLD,
NULL
);
printf("호스트에서 본 자식 PID: %d\n", pid); /* 실제 PID */
waitpid(pid, NULL, 0);
return 0;
}cgroups (Control Groups)
프로세스 그룹의 자원 사용량을 제한합니다.
# cgroup v2에서 메모리 제한 설정 (개념)
# /sys/fs/cgroup/my-container/
# 메모리 제한: 256MB
echo $((256 * 1024 * 1024)) > /sys/fs/cgroup/my-container/memory.max
# CPU 제한: 50% (100ms 중 50ms만 사용)
echo "50000 100000" > /sys/fs/cgroup/my-container/cpu.max
# PID 개수 제한: 최대 100개 프로세스
echo 100 > /sys/fs/cgroup/my-container/pids.max
# 프로세스를 cgroup에 추가
echo $$ > /sys/fs/cgroup/my-container/cgroup.procs| 컨트롤러 | 제한 대상 | 설정 파일 | 예시 |
|---|---|---|---|
| memory | 메모리 사용량 | memory.max | 512MB |
| cpu | CPU 시간 | cpu.max | 1.5 코어 |
| io | 디스크 I/O | io.max | 100MB/s |
| pids | 프로세스 수 | pids.max | 100개 |
Docker의 동작 원리
Docker는 컨테이너를 쉽게 빌드, 배포, 실행하기 위한 도구입니다.
이미지와 레이어
Docker 이미지(Image)는 컨테이너의 파일 시스템 스냅샷입니다. 이미지는 레이어(Layer)로 구성됩니다.
[쓰기 레이어] ← 컨테이너 실행 시 추가 (임시)
[소스 코드 복사]
[pip install]
[apt-get install python3]
[Ubuntu 22.04 베이스]레이어는 읽기 전용이며, 컨테이너가 실행되면 그 위에 쓰기 가능한 레이어가 추가됩니다(Union FS: OverlayFS). 같은 베이스 이미지를 공유하는 컨테이너들은 중복 저장 없이 공통 레이어를 공유하므로 디스크를 절약합니다.
Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# non-root 사용자로 실행 (보안)
RUN useradd -r appuser
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]각 명령어(FROM, RUN, COPY)가 하나의 레이어를 생성합니다. RUN 명령을 합치면 레이어 수를 줄여 이미지 크기를 최적화할 수 있습니다.
Docker 실행
# 이미지 빌드
docker build -t myapp:1.0 .
# 컨테이너 실행 (리소스 제한 + 보안 옵션)
docker run -d \
--name web \
-p 8080:80 \
--memory=512m \
--cpus=1.0 \
--read-only \
--security-opt=no-new-privileges \
nginx:alpine
# 실행 중인 컨테이너 목록
docker ps
# 컨테이너 내부 접속
docker exec -it web /bin/sh
# 리소스 사용량 모니터링
docker stats web
# 컨테이너가 사용하는 네임스페이스 확인
docker inspect --format '{{.State.Pid}}' web
# ls -la /proc/<PID>/ns/--memory=512m은 cgroups로 메모리를 512MB로 제한합니다. 이 한도를 초과하면 OOM Killer가 컨테이너 프로세스를 종료합니다.
컨테이너 격리의 수준과 한계
컨테이너가 같은 커널을 공유한다는 것은 커널 수준의 취약점에 모든 컨테이너가 노출된다는 뜻입니다.
격리 강화 기술
| 기술 | 방식 | 보안 수준 |
|---|---|---|
| rootless 컨테이너 | 엔진을 non-root로 실행 | 중 |
| seccomp | 허용 시스템 콜 제한 | 중상 |
| AppArmor/SELinux | MAC 정책 적용 | 상 |
| user namespace | 컨테이너 root ≠ 호스트 root | 중상 |
| gVisor | 사용자 공간 커널 (시스콜 프록시) | 상 (성능 감소) |
| Kata Containers | 경량 VM 안에서 컨테이너 실행 | 최상 |
| Firecracker | 마이크로 VM (AWS Lambda 사용) | 최상 |
# Docker 기본 seccomp 프로파일은 약 300개 시스콜 중 ~44개를 차단
# 예: mount, reboot, syslog 등 위험한 시스콜
# 커스텀 seccomp 프로파일 적용
docker run --security-opt seccomp=custom-profile.json myapp실무에서는 보안 요구사항에 따라 적절한 수준을 선택합니다. 일반 웹 서비스는 rootless + seccomp으로 충분하고, 멀티테넌트 환경은 Kata/Firecracker를 고려합니다.
다음 절에서는 컨테이너를 대규모로 관리하는 오케스트레이션과 실무 인프라 선택 기준을 다루겠습니다.