32P by neo 11시간전 | ★ favorite | 댓글 4개
  • docker run ubuntu를 실행해도 호스트 Linux 커널을 공유하며 Ubuntu는 사용자 공간 도구만 제공
  • uname -r 결과가 호스트 커널 버전으로 표시되고, /etc/os-release만 Ubuntu 정보로 나타남
  • VM은 각각 고유한 커널을 가지고 부팅에 수 분이 걸리지만, 컨테이너는 밀리초 단위로 시작되고 하드웨어 가상화 없이 OS 수준 격리로 호스트 커널을 공유하여 오버헤드가 낮음
  • 리눅스 시스템 콜 ABI의 안정성으로 다양한 배포판 컨테이너가 동일 커널에서 동작 가능함
  • 16GB RAM 환경에서 경량 컨테이너는 50-100개, 중간 규모는 10-30개, 대형 컨테이너는 5-10개 정도가 실용적인 한계
  • 이 아키텍처 이해가 중요한 이유는 커널 취약점이 모든 컨테이너에 영향을 미치고, 베이스 이미지 선택이 호환성과 보안에 직접적 영향을 주기 때문

“Ubuntu를 실행한다”는 의미

  • docker run ubuntu:22.04 실행 시 Ubuntu처럼 보이는 bash 프롬프트를 얻고 apt update와 패키지 설치가 가능
  • 그러나 컨테이너 내부에서 uname -r 실행 시 호스트 커널 버전(예: 6.5.0-44-generic)이 표시됨
  • /etc/os-release 파일은 Ubuntu 22.04로 표시되지만, 커널은 호스트 머신의 것이며 "Ubuntu" 부분은 사용자 공간을 구성하는 파일시스템에 불과

컨테이너 vs 가상 머신: 아키텍처 비교

  • VM은 하드웨어를 가상화하고, 컨테이너는 운영체제를 가상화
  • 주요 차이점:
    • 커널: VM은 각각 고유 커널 보유, 컨테이너는 호스트 커널 공유
    • 부팅 시간: VM은 수 분, 컨테이너는 밀리초
    • 메모리 오버헤드: VM은 512MB-4GB, 컨테이너는 1-10MB
    • 디스크 사용량: VM은 10-100GB, 컨테이너 이미지는 10-500MB
    • 격리 수준: VM은 하드웨어 레벨, 컨테이너는 프로세스 레벨
    • 성능: VM은 약 5-10% 오버헤드, 컨테이너는 네이티브에 가까운 성능

베이스 이미지의 실제 구성 요소

  • ubuntu:22.04 풀 시 다운로드되는 tarball 내용:
  • 1. 필수 바이너리 (/bin, /usr/bin)

    • /bin/bash (셸), /bin/ls (파일 목록), /bin/cat (파일 표시)
    • /usr/bin/apt (패키지 관리자), /usr/bin/dpkg (Debian 패키지 도구)
  • 2. 공유 라이브러리 (/lib, /usr/lib)

    • glibc와 기타 프로그램이 링크하는 공유 라이브러리
    • /lib/x86_64-linux-gnu/libc.so.6 (C 라이브러리 - 모든 C 프로그램의 기반)
    • libpthread.so.0, libm.so.6 등 필수 라이브러리
  • 3. 설정 파일 (/etc)

    • /etc/apt/sources.list (패키지 저장소)
    • /etc/passwd (사용자 데이터베이스)
    • /etc/resolv.conf (DNS 설정, 보통 호스트에서 마운트)
  • 4. 패키지 데이터베이스

    • /var/lib/dpkg/status (설치된 패키지)
    • /var/lib/apt/lists/ (사용 가능한 패키지 캐시)
  • 커널·부트로더·드라이버는 포함되지 않음

커널은 그대로, 모든 것이 변함

  • Linux 커널이 제공하는 기능: 프로세스 스케줄링, 메모리 관리, 파일시스템 연산, 네트워크 스택, 디바이스 드라이버, 시스템 콜
  • 컨테이너 프로세스가 open(), read(), fork()를 호출하면 호스트 커널로 직접 전달
  • 커널은 해당 프로세스가 "Ubuntu 컨테이너"인지 "Alpine 컨테이너"인지 알지도 신경 쓰지도 않음
  • 시스템 콜 인터페이스의 안정성

    • Linux syscall ABI는 매우 안정적
    • glibc 2.31(Ubuntu 20.04)에서 컴파일된 바이너리가 Ubuntu 24.04 커널에서도 동작하는 이유:
      • 커널이 하위 호환성 유지
      • 시스템 콜 번호 변경 없음
      • 새 기능은 추가되지만 기존 기능은 거의 제거되지 않음
    • 커널 6.5를 실행하는 호스트에서 Ubuntu 18.04 컨테이너 실행이 가능한 이유

실습 해보기: 동일 커널, 다른 사용자 공간

  • 여러 베이스 이미지에서 동일한 커널 쿼리 실행 시 모든 이미지가 호스트 커널을 공유한다는 것을 볼수 있음
  • ubuntu:22.04, debian:12, alpine:3.19, fedora:39, archlinux:latest 모두 동일한 커널 버전(6.5.0-44-generic)임
  • 컨테이너별 차이는 uname 바이너리와 libc 등 유저랜드 구성

컨테이너가 매우 효율적인 이유

  • 1. 커널 중복 없음

    • VM은 각각 전체 커널을 메모리에 로드(약 100-500MB)
    • 10개 VM은 10개 커널이 메모리 소비, 10개 컨테이너는 커널 하나만 사용
  • 2. 즉시 시작

    • VM 부팅 순서: BIOS → 부트로더 → 커널 → init 시스템 → 서비스
    • 컨테이너는 fork()exec() 호출만으로 밀리초 내 프로세스 존재
    • 일반적인 VM 부팅: 30-60초 / 컨테이너 시작: 약 0.347초
  • 3. 공유 이미지 레이어

    • ubuntu:22.04에서 100개 컨테이너 실행 시 베이스 이미지 레이어는 디스크에 한 번만 존재
    • 각 컨테이너는 변경 사항을 위한 얇은 copy-on-write 레이어만 획득
  • 4. 커널을 통한 메모리 공유

    • 커널의 페이지 캐시가 공유
    • 50개 컨테이너가 동일 파일 읽기 시 커널은 한 번만 캐시
    • 동일한 공유 라이브러리 사용 시 copy-on-write로 메모리 페이지 공유 가능

컨테이너 실행 가능 한계 계산

  • 메모리 분석 (16GB RAM VM 기준)

    • 총 RAM: 16,384 MB
    • 호스트 OS 오버헤드: -1,024 MB
    • Docker 데몬: -256 MB
    • 컨테이너 런타임 오버헤드: -512 MB
    • 컨테이너 가용량: 14,592 MB
  • 컨테이너 유형별 메모리 사용량

    • 최소(sleep): 약 1MB
    • Alpine + 소형 앱: 약 25MB
    • Ubuntu + Python 앱: 약 120MB
    • Ubuntu + Java 앱: 약 500MB
    • Node.js 서비스: 약 200MB
  • 이론적 최대치

    • 최소 컨테이너(1MB): 14,592개
    • Alpine + 소형 앱(25MB): 583개
    • Ubuntu + Python(120MB): 121개
    • Java 마이크로서비스(500MB): 29개
  • 실제 한계

    • 메모리 외 고려사항:
      • CPU 스케줄링: 너무 많은 컨테이너 경쟁 시 지연 스파이크 발생
      • 파일 디스크립터: 기본 ulimit 1024
      • 네트워크 포트: 포트 매핑에 65,535개만 사용 가능
      • PIDs: /proc/sys/kernel/pid_max 제한(기본: 32,768)
      • 디스크 I/O: OverlayFS 오버헤드, 많은 레이어 탐색 필요
    • 16GB VM에서 실제 워크로드 실행 시 실용적 한계:
      • 경량 컨테이너(API, 워커): 50-100개
      • 중간 컨테이너(DB, 캐시): 10-30개
      • 대형 컨테이너(ML 모델, JVM 앱): 5-10개

Linux 배포판 호환성

  • 커널 ABI 약속

    • Linux는 안정적인 syscall 인터페이스 유지
    • 오래된 커널용으로 컴파일된 바이너리가 새 커널에서 동작
    • Ubuntu 18.04 바이너리가 커널 6.5에서 정상 실행
  • 호환성이 깨지는 경우

    • 커널 기능 요구사항: 컨테이너가 커널에 없는 기능 필요 시 (예: io_uring은 커널 5.1+ 필요)
    • 커널 모듈 의존성: Wireguard는 wireguard 커널 모듈 필요, NVIDIA 컨테이너는 nvidia 커널 드라이버 필요
    • Seccomp/capability 제한: 호스트가 컨테이너에 필요한 syscall 차단 시 (예: ptrace 사용 시 --cap-add SYS_PTRACE 필요)

베이스 이미지 선택 가이드

베이스 이미지 크기 패키지 관리자 용도
scratch 0 MB 없음 정적 컴파일된 Go/Rust 바이너리
alpine 7 MB apk 최소 컨테이너, musl libc
distroless 20 MB 없음 보안 중심, 셸·패키지 관리자 없음
debian-slim 80 MB apt 크기와 호환성 균형
ubuntu 78 MB apt 개발 친숙성
fedora 180 MB dnf 최신 패키지, SELinux
  • 각 이미지 사용 시점

    • scratch: 정적 컴파일 바이너리용, OS 전혀 없이 바이너리만 포함
    • alpine: 셸 접근이 필요한 최소 이미지, glibc 대신 musl libc 사용으로 일부 호환성 문제 가능
    • distroless: 보안 중심 프로덕션 이미지, 셸과 패키지 관리자 없어 디버깅 어렵지만 더 안전

사용자 공간-커널 경계

  • 베이스 이미지에서 오는 것 (사용자 공간)

    • 셸 (/bin/bash, /bin/sh)
    • C 라이브러리 (glibc, musl)
    • 패키지 관리자 (apt, apk, yum)
    • 핵심 유틸리티 (ls, cat, grep)
    • init 시스템 설정 (보통 systemd 자체는 아님)
    • 기본 사용자와 그룹 (/etc/passwd)
    • 라이브러리 경로와 설정
  • 호스트에서 오는 것 (커널)

    • 프로세스 스케줄링과 메모리 관리
    • 네트워크 스택 (TCP/IP, 라우팅)
    • 파일시스템 연산 (읽기, 쓰기, 마운트)
    • 보안 기능 (네임스페이스, cgroups, seccomp)
    • 디바이스 드라이버 (GPU, 네트워크, 스토리지)
    • 시간과 클럭 관리
    • 암호화와 난수 생성
  • 네임스페이스가 만들어 내는 환각

    • 커널이 네임스페이스를 제공하여 컨테이너가 격리된 것처럼 느끼게 함
    • 컨테이너 내부에서 PID 1로 보이는 프로세스가 호스트에서는 더 높은 PID(예: 45678)로 존재
    • 커널이 매핑 유지: 컨테이너 PID 1 → 호스트 PID 45678
    • 가상화 없이 격리가 작동하는 방식

프로덕션 환경에서의 의미

  • 1. 커널 취약점이 모든 컨테이너에 영향

    • 호스트 커널에 취약점이 있으면 모든 컨테이너가 노출됨
    • 호스트 패치 유지 필수
  • 2. 호스트 커널이 컨테이너 기능 제한

    • io_uring 사용하려면 호스트 커널 5.1+ 필요
    • eBPF 기능은 특정 옵션이 활성화된 커널 4.15+ 필요
  • 3. glibc vs musl 중요성

    • Alpine은 musl libc 사용
    • glibc용으로 컴파일된 일부 바이너리가 동작하지 않을 수 있음
    • 예: Alpine에서 glibc 바이너리 실행 시 /lib/x86_64-linux-gnu/libc.so.6 파일 없음 오류 발생 가능
  • 4. 컨테이너 "OS"는 순전히 조직적 개념

    • 커널 관점에서 "Ubuntu 컨테이너"와 "Debian 컨테이너"는 차이 없음
    • 모두 syscall을 만드는 프로세스일 뿐

흔한 오해

  • ❌ "컨테이너는 경량 VM": 컨테이너는 고급 격리를 가진 프로세스이며, VM은 하드웨어를 가상화하고 별도 커널 실행
  • ❌ "각 컨테이너가 고유 커널 보유": 모든 컨테이너가 호스트 커널 공유, 컨테이너의 "OS"는 사용자 공간 파일일 뿐
  • ❌ "Ubuntu 컨테이너 실행 = Ubuntu 실행": 호스트 커널과 Ubuntu 도구 실행, 호스트가 Debian이면 실제로는 Debian 커널 실행
  • ❌ "베이스 이미지에 완전한 운영체제 포함": 베이스 이미지는 최소 사용자 공간 도구만 포함, 커널·부트로더·드라이버 없음
  • ❌ "더 많은 컨테이너 = 더 많은 메모리": 공유 레이어와 커널 페이지 캐싱으로 컨테이너가 종종 효율적으로 메모리 공유

핵심 요약

  • Docker 베이스 이미지는 Linux 배포판의 사용자 공간 구성 요소에 대한 파일시스템 스냅샷
    • Ubuntu를 Ubuntu처럼 느끼게 하는 바이너리, 라이브러리, 설정
  • 실제 운영체제인 커널은 호스트와 공유
  • 이 아키텍처가 가능하게 하는 것:
    • 밀리초 단위 시작 시간 (커널 부팅 없음)
    • 최소한의 메모리 오버헤드 (하나의 커널, 공유 페이지)
    • 대규모 밀도 (호스트당 수백 개 컨테이너)
    • 네이티브에 가까운 성능 (커널로의 직접 syscall)
  • 트레이드오프는 VM보다 약한 격리 - 컨테이너가 커널을 공유하므로 커널 익스플로잇이 모든 컨테이너에 영향
  • 대부분의 워크로드에서 이 트레이드오프는 가치 있음

그래서 리눅스에서 직접 도커 만들어보기 해가지고 디렉토리 격리해서 유저랑 그룹이랑 이래저래 하는 튜토리얼도 있고 그러더라고요.

https://news.hada.io/topic?id=9531

그래서 좀 과장되게 말해서 chroot + cgroup = docker 이렇게 봐도 되겠더라고요

정말 신기하네요

본문은 LINUX 기준으로 설명한 거 같지만,
윈도우에서 실행하는 경우에는 WSL2로 만들어진 가상 커널을 글처럼 공유하게 되는 거겠죠?

만약에 도커에 취약점이 생겨서 커널을 건드릴 수 있게 된다면 리눅스보다 한 번 가상화를 하는 윈도우가 더 보안에 강하다고 봐야 할 지도 궁금하네요.