icon

안동민 개발노트

13장 : 가상화와 컨테이너

컨테이너와 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
CGROUPcgroup 뷰자원 제한의 독립적 뷰
namespace_demo.c
#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_demo.sh
# 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.max512MB
cpuCPU 시간cpu.max1.5 코어
io디스크 I/Oio.max100MB/s
pids프로세스 수pids.max100개

Docker의 동작 원리

Docker는 컨테이너를 쉽게 빌드, 배포, 실행하기 위한 도구입니다.

이미지와 레이어

Docker 이미지(Image)는 컨테이너의 파일 시스템 스냅샷입니다. 이미지는 레이어(Layer)로 구성됩니다.

[쓰기 레이어] ← 컨테이너 실행 시 추가 (임시)
[소스 코드 복사]
[pip install]
[apt-get install python3]
[Ubuntu 22.04 베이스]

레이어는 읽기 전용이며, 컨테이너가 실행되면 그 위에 쓰기 가능한 레이어가 추가됩니다(Union FS: OverlayFS). 같은 베이스 이미지를 공유하는 컨테이너들은 중복 저장 없이 공통 레이어를 공유하므로 디스크를 절약합니다.

Dockerfile

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_commands.sh
# 이미지 빌드
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/SELinuxMAC 정책 적용
user namespace컨테이너 root ≠ 호스트 root중상
gVisor사용자 공간 커널 (시스콜 프록시)상 (성능 감소)
Kata Containers경량 VM 안에서 컨테이너 실행최상
Firecracker마이크로 VM (AWS Lambda 사용)최상
seccomp_profile.sh
# Docker 기본 seccomp 프로파일은 약 300개 시스콜 중 ~44개를 차단
# 예: mount, reboot, syslog 등 위험한 시스콜

# 커스텀 seccomp 프로파일 적용
docker run --security-opt seccomp=custom-profile.json myapp

실무에서는 보안 요구사항에 따라 적절한 수준을 선택합니다. 일반 웹 서비스는 rootless + seccomp으로 충분하고, 멀티테넌트 환경은 Kata/Firecracker를 고려합니다.

다음 절에서는 컨테이너를 대규모로 관리하는 오케스트레이션과 실무 인프라 선택 기준을 다루겠습니다.

목차