The VM-centric way to solve this problem
VM은 어떤 방식으로 동작할까? 가상 머신은 하이퍼바이저(Hypervisor)라는 소프트웨어 계층 위에서 동작한다. 하이퍼바이저는 호스트 시스템의 하드웨어 리소스를 가상화하고, 각 가상 머신은 독립된 운영 체제와 커널을 가지며, 이 운영 체제는 하이퍼바이저의 관리하에 실행된다.
커널 관점에서 가상 머신은 물리적인 하드웨어 리소스에 직접 접근하지 않는다. 가상 머신의 각 운영 체제는 가상화된 하드웨어에서 동작하며, 각 운영 체제는 자체 커널을 가지고 있다. 이러한 커널은 하이퍼바이저와의 상호 작용을 거쳐 하드웨어 자원에 액세스한다. 이런 원리로 운영되기 때문에 하이퍼바이저와 가상 머신 간에는 추가적인 오버헤드가 발생하게 된다.
그런데 VM을 설치하여 수백~수천 개의 애플리케이션을 돌린다고 가정해보자. 이때 VM과 같은 방식은 비효율적이다. VM은 서로 다른 커널을 가지고 있으므로 단순한 커널 업데이트가 일어나기만 해도 심각한 문제가 생길 수 있기 때문이다.
이때 컨테이너 방식을 사용하면 전체 운영체제나 VM을 부팅하는 시간이 들지 않으며, 컴퓨터에서는 돌아갔던 코드가 프로덕션에서는 돌아가지 않는다는 등의 문제가 생기지 않는다. 컨테이너는 호스트 시스템의 운영 체제 커널(즉 리눅스 커널)을 공유하며, 각 컨테이너는 격리된 실행 환경을 제공하기 때문에 하드웨어와 운영체제에 대한 안전한 가정을 하게 만들어준다. 따라서 VM에 비해서 종속성 격리가 간편하며, 마이크로서비스와 묘듈식 디자인 패턴을 활용하기 쉽게 만들어준다.
요약하자면, 가상 머신은 각각 독립된 운영 체제와 커널을 가지며 하이퍼바이저 위에서 실행되고, 컨테이너는 호스트 시스템의 커널을 공유하여 가볍게 격리된 실행 환경을 제공한다.
Containers use a varied set of Linux technologies
그렇다면 컨테이너는 어떻게 호스트의 리눅스 커널을 같이 사용할 수 있을까? 그 이유는 바로 컨테이너가 리눅스 기술을 사용하기 때문이다. 컨테이너가 활용하는 핵심 리눅스 기술에는 다음과 같은 4가지가 있다.
- Processes
- Virtual memory, address space
- Namespaces
- 프로세스 ID 넘버, directory trees, IP 주소
- cgroups
- CPU, Memory, I/O bandwidth 등의 사용을 관리
- Union file systems
- 애플리케이션 캡슐화, 종속성 관리의 레이어화
먼저, Dockerfile이란 무엇일까? Dockerfile은 컨테이너 이미지를 생성하기 위한 청사진 역할을 하는 스크립트이다. Dockerfile은 필요한 소프트웨어 패키지를 정의하기도 하고, 이미지에 포함될 파일, 디렉토리, 라이브러리, 환경 변수 등을 정의하여 컨테이너를 구성하기도 한다. 이때 컨테이너는 여러 레이어로 이루어져 있는데, Dockerfile의 각 명령어들은 컨테이너의 각 레이어를 뜻한다.
예를 들어 다음과 같은 Dockerfile이 있다고 하자.
FROM ubuntu:18.04
COPY ./app
RUN make /app
CMD python /app/app.py
Dockerfile의 각 레이어의 의미는 다음과 같다.
- FROM
- base 레이어를 만드는 명령어로, public repository에서 이미지를 pull하는 역할을 한다. 위에서는 ubuntu:18.04 이미지를 통해서 base 레이어를 만들고 있다.
- COPY
- 특정한 파일들을 복사해서 현재 디렉토리에 넣는 역할을 한다.
- RUN
- make 커멘드를 사용해서 애플리케이션을 빌드하는 역할을 한다. 위에서는 /app 디렉토리 내부에 있는 파일들을 빌드하는 역할을 한다.
- CMD
- 컨테이너가 출시된 다음에 실행할 명령문들을 정의한다. 위에서는 컨테이너가 실행될 때 기본적으로 /app/app.py 파일을 파이썬으로 실행하도록 지시하고 있다.
각 레이어들은 이 직전 레이어와 다른 점을 명시해 놓은 집합에 불과하다. 따라서 Dockerfile을 작성할 때는 가장 적게 변경되는 레이어에서 가장 많이 변경될 레이어 순으로 작성해야 한다. 왜냐하면 Docker는 컨테이너를 빌드할 때 기존에 빌드했던 내역을 캐시로 가지고 있으며, 여기에서 변경 사항이 일어난 레이어부터 새롭게 빌드하기 때문이다. 따라서 가장 많이 변경되는 레이어를 먼저 적는다면(즉, Dockerfile의 가장 위에 놓는다면) 그 이후의 레이어도 전부 다시 실행되어야 한다. 이는 비효율적인 방식이므로, 가장 많이 변경되는 레이어는 Dockerfile의 가장 위에 있어야 한다.
이러한 모든 새로운 변경은 실행되고 있는 컨테이너 안에서 만들어지며, 변경된 사항은 container layer에 작성된다. 이 레이어들은 컨테이너가 삭제되면 같이 사라지게 되고, 이미지만 변경되지 않은 채 남게 된다.