Docker 해부 (2) - What is Docker: Container, Image, and Registry
Docker가 푸는 문제에서 출발해 container가 VM과 다른 점, Docker와 container의 관계, image의 layer 저장 방식, 그리고 registry까지 Docker의 세 구성요소를 정리합니다.
Docker 해부 시리즈의 2편입니다. 전체 목차는 0편에 있습니다.
What is Docker
Docker(도커)는 Linux의 응용프로그램들을 software container 안에 배치시키는 일을 자동화하는 오픈소스 프로젝트입니다. Docker가 푸는 문제는 한 줄로 “같은 코드가 컴퓨터마다 다르게 도는 것”입니다. 코드는 같아도 그 아래의 Python 버전, 시스템 라이브러리, OS가 다르면 결과가 달라집니다. 가상환경(venv)은 Python 패키지까지만 격리하므로 그 아래 층의 차이는 막지 못합니다. Docker는 애플리케이션과 그 실행 환경 전체를 image라는 단위로 묶어, 내 노트북에서든 서버에서든 같은 모습으로 실행되게 만듭니다.
Docker는 크게 세 구성요소로 이루어집니다. 실행 단위인 container, 그 원본인 image, image를 주고받는 registry입니다. 이 글에서 순서대로 다룹니다.
Container란 무엇인가?
Container는 application code를 library, dependency와 함께 패키징하는 software 단위입니다. 이 정의만으로는 기존 방식과 무엇이 다른지 보이지 않으므로, 먼저 Virtual Machine과 비교해 봅니다.
기존의 Virtual Machine은 Host OS(예: 제 Macbook의 macOS)에서 hypervisor에 의해 구동됩니다. hypervisor가 가짜 하드웨어(가상 CPU, 가상 디스크, 가상 네트워크)를 흉내내고, 그 위에 Guest OS(예: Linux)를 통째로 설치해 부팅하는 구조입니다.
1
2
3
4
5
6
7
8
9
10
11
┌──────────┐ ┌──────────┐
│ App A │ │ App B │
├──────────┤ ├──────────┤
│ Guest OS │ │ Guest OS │ ← 커널 포함 OS 한 벌씩 (GB 단위)
├──────────┴──┴──────────┤
│ Hypervisor │ ← 모든 하드웨어 접근이 항상 여길 거친다
├────────────────────────┤
│ Host OS │
├────────────────────────┤
│ Hardware │
└────────────────────────┘
반면 container는 별도의 Guest OS를 부팅하지 않습니다. 가상 하드웨어 없이 host의 Linux 커널을 공유하며 실행되고, 이런 방식을 ‘OS 수준의 virtualization’이라고 부릅니다. 이것이 가능한 까닭은 Linux 커널이 namespace(프로세스가 보는 세계의 격리)와 cgroup(자원 사용량 제한)으로 process를 격리할 수 있기 때문입니다. 커널만 공유하고 사용자 공간은 image가 통째로 제공하므로, Ubuntu 호스트에서 Debian 기반 image가 그대로 돕니다. 아래 그림 오른쪽의 Docker Engine이 무엇인지는 다음 섹션에서 다룹니다.
1
2
3
4
5
6
7
8
9
10
11
12
(커널에 시스템 콜 직접) (만들고 지울 때만 개입)
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────────────┐
│ App A │ │ App B │ │ App C │<---│ Docker Engine │
│container│ │container│ │container│ │(dockerd, runc)│
└────┬────┘ └────┬────┘ └────┬────┘ └───────┬───────┘
│ │ │ │
├────┴───────────┴───────────┴─────────────────┴───────┤
│ Host OS (Linux kernel) │
│ namespace: isolation, cgroup: limits │
├──────────────────────────────────────────────────────┤
│ Hardware │
└──────────────────────────────────────────────────────┘
Docker와 Container의 관계
앞에서 본 대로 container를 실제로 만드는 것은 Linux 커널입니다. namespace와 cgroup은 Docker가 나오기 전부터 커널에 있던 기능이고, Docker는 이 기능을 쓰기 쉽게 포장한 도구입니다.
docker run 한 줄이 실행되는 경로를 따라가면 관계가 정확히 보입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker run python:3.12-slim
│
▼
┌─────────────────┐
│ docker CLI │ 명령을 받아 Engine의 API를 호출
└────────┬────────┘
▼
┌─────────────────┐
│ Docker Engine │ image 준비, network와 volume 연결,
│ (dockerd, runc) │ 커널에 "격리된 프로세스 생성"을 요청
└────────┬────────┘
▼
┌─────────────────┐
│ Linux kernel │ namespace로 격리, cgroup으로 자원 제한
└────────┬────────┘
▼
container 시작 (= 격리되어 실행되는 프로세스)
한 가지 주의할 점이 있습니다. 위 그림의 전제는 Linux 커널이라서 macOS나 Windows에는 그대로 성립하지 않습니다. 그래서 Docker Desktop이 경량 Linux VM을 하나 띄우고, 위 그림 전체가 그 VM 안에서 돌아갑니다. VM은 container마다 하나가 아니라 전체에 하나이고, Linux 서버에서는 VM 없이 커널이 직접 container를 만듭니다.
Docker의 image
Docker의 image는 container 실행에 필요한 모든 파일과 설정값을 포함하며, 상태값을 가지지 않고 변하지 않습니다. image를 내려받아(pull) 실행(run)하는 것만으로 container가 하나 만들어집니다. 즉 container는 image를 실행한 상태입니다.
그렇다면 어떤 image를 실행할지는 무엇으로 지정할까요? 앞의 그림에서 docker run python:3.12-slim이라고 쓸 때 python:3.12-slim이 바로 image의 이름입니다. 이름은 저장소:tag 구조로, python 저장소의 3.12-slim 버전이라는 뜻입니다. tag를 생략하면 latest가 붙는데, latest는 특정 버전을 고정하는 이름이 아니므로 재현성이 필요한 곳에서는 tag를 명시합니다.
layer 저장 방식
image는 실행에 필요한 파일 전부를 담기 때문에 용량이 큽니다. 그런데 파일 하나만 바뀌어도 image 전체를 다시 받아야 한다면 비효율적일 것입니다. Docker는 이 문제를 image를 하나의 파일이 아니라 여러 layer를 쌓아 만드는 방식으로 해결합니다.
각 layer는 읽기 전용이라 한번 만들어지면 바뀌지 않고, 바뀌지 않기 때문에 image들 사이에서 공유할 수 있습니다. 여러 layer를 겹쳐 하나의 파일 시스템처럼 보여주는 일은 Union File System이 담당합니다.
1
2
3
4
5
6
7
8
9
10
11
12
python image 라이브러리 image 내 앱 image
┌───────────┐
│ app code │ ← 새로 만든 layer만 추가
┌───────────┐ ├───────────┤
│ libs │ │ libs │
┌───────────┐ ├───────────┤ ├───────────┤
│ layer C │ │ layer C │ │ layer C │
├───────────┤ ├───────────┤ ├───────────┤
│ layer B │ │ layer B │ │ layer B │ ← A, B, C는 세 image가
├───────────┤ ├───────────┤ ├───────────┤ 공유 (다시 받지 않음)
│ layer A │ │ layer A │ │ layer A │
└───────────┘ └───────────┘ └───────────┘
그림의 세 image는 layer를 공유합니다. python image(A+B+C)를 이미 가진 상태에서 라이브러리 image를 pull 받으면 A, B, C는 건너뛰고 libs layer만 내려받습니다. 그 위에 코드를 얹은 내 앱 image를 다른 사람이 받을 때도 겹치는 layer는 두고 app code layer만 내려받습니다. 다운로드도 저장도 바뀐 만큼만 일어납니다.
그런데 layer가 전부 읽기 전용이기 때문에, container가 실행 중에 파일을 만들거나 고치면, 해당 내용을 기록할 수 없습니다. 그래서 R/W layer (Read and Write Layer) 를 통해 쓸 수 있는 Layer를 추가로 만들어 놓습니다.
1
2
3
4
5
6
7
8
9
10
11
container 1 container 2
┌─────────────┐ ┌─────────────┐
│ R/W layer 1 │ │ R/W layer 2 │ ← container마다 하나씩, 쓰기 가능
└──────┬──────┘ └──────┬──────┘ (container를 지우면 사라지는 부분)
└────────┬─────────┘
▼
┌─────────────┐
│ layer C │
│ layer B │ ← image layer: 읽기 전용,
│ layer A │ 모든 container가 같은 것을 공유
└─────────────┘
위 그림처럼 container를 만들면 image layer들 위에 쓰기 가능한 R/W layer가 하나 얹힙니다. 실행 중에 생기는 파일과 변경 사항은 전부 여기 기록되고, 아래의 image layer는 영향을 받지 않습니다. container를 지울 때 사라지는 것도 바로 이 R/W layer입니다.
Docker의 registry
image를 모아 두고 주고받는 곳이 registry입니다. 기본값은 Docker Hub이고, push로 올리고 pull로 받습니다. 공개할 수 없는 image는 AWS ECR 같은 사설 registry에 올립니다.
Docker Hub에는 누구나 image를 올릴 수 있습니다. 그래서 python, postgres처럼 Docker가 직접 검증하고 관리하는 official image와 일반 사용자가 올린 image가 섞여 있습니다. image를 받아 실행하는 것은 남이 만든 코드를 실행하는 것과 같으므로, 기본은 official image를 쓰고 출처가 불분명한 image는 내용물을 확인하고 받는 것이 안전합니다.
다음 편에서 Docker를 설치하고 container를 직접 띄워봅니다.