도커는 하나의 이미지로 여러 개의 동일한 컨테이너를 구동할 수 있다.
컨테이너는 다음의 특징들을 갖는다.
OS 컨테이너
를 사용하게 되면 컨테이너 하나에 Nginx, Nodejs, MySQL 등 여러 개의 어플리케이션을 기동할 수 있다. 이런 구조는 패키지화에는 좋지만 확장성이 굉장히 낮다. 만약 트래픽에 따라 효율적인 서비스를 제공하는 클라우드의 장점을 극대화하고 싶다면 클라우드 네이티브 즉, 클라우드 환경에 최적화된 어플리케이션을 만들어야 한다. 하나의 컨테이너에 하나의 어플리케이션을 구동하는 어플리케이션 컨테이너
를 사용해 언제든 Scale IN/OUT이 가능한 위의 조건을 만족할 수 있다.
하지만 이 좋은 컨테이너도 무지성으로 사용하면 안 된다. 기존의 가상머신은 무거운 OS위에 어플리케이션이 구동되지만 우리는 컨테이너가 동작하는데 필요한 환경만 갖고 동작해야 한다.
새로 생성한 NGINX 컨테이너를 살펴보면 OS가 보인다. docker는 guest OS를 갖지 않아서 가볍다고 배웠다.
nginx는 어플리케이션임에도 불구하고 왜 OS가 보이는 걸까?
당연하게도 어플리케이션을 구동하기 위해서는 OS 환경이 필요하다. 앞에서 보인 NGINX나 우리가 만든 어플리케이션이 작동하기 위해서는 최소한의 OS 환경이 필요하다.
때문에 nignx:latest 이미지 보다는 경량화된 리눅스 환경(alpine)에서 nginx가 탑재된 stable-alpine을 사용하는 것이 옳다. 결국 게스트 OS가 없다고 봐도 무방하다.
주제를 바꿔보자. 하나의 이미지로 여러 개의 동일한 컨테이너를 구동할 수 있음을 기억하고 2번
을 바라보면 컨테이너에 변경사항이 발생해도 하나의 이미지에는 영향이 가지 않음을 알 수 있다.
앞에서 컨테이너는 Image에 의해 만들어진다는 것을 배웠다. 그리고 Image는 Dockerfile에 기반해 만들어지는 것을 배웠다.
이 Image
는 1개 이상의 레이어로 이루어져 있다. 우리가 어플리케이션을 기동하기 위해 필요한 각종 라이브러리, 바이너리 파일들 또한 Image에 포함이 되어 있는데 이미지 안에 소스코드 뿐만아니라 동작 환경도 존재하기 때문에 그렇다. 이들 각각이 layer이다.
A Docker image is built up from a series of layers.
도커 이미지는 다수의 레이어로 이루어져 있다.
ubuntu15.04 이미지를 실행하기 위해 docker hub에서 이미지를 pull받는다. 위의 사진의 밑부분을 살펴보면 Image가 layer 4개로 나뉘어 pull되는 것으로 보인다.
각각의 layer들은 Dockerfile에 적힌 명령어를 나타낸다. 도커 docs에 가면 밑의 사진을 확인할 수 있다.
여기서 파일 시스템을 수정하는 명령어가 layer를 생성한다.
파일 시스템을 수정하지 않는 명령어는 layer를 생성하지 않는다.
각각의 layer들은 하위 layer를 참조하기 때문에 현재 layer에 변경사항이 생겨도 하위 layer에는 영향을 주지 않는다. 하지만 이러한 layer들은 모두 Read-Only
다. 왜 그럴까?
그 이유는 컨테이너가 동작할 때 데이터, 로그 파일들이 생성되는데 이것들로 인해 이미지가 변경되면 안되기 때문이다. 이미지에 문제가 생기면 서비스 중인 서버와 동일한 서버를 배포하기 어려워질 것이다. 이런 문제를 해결하기 위해 컨테이너가 기동이 될 때만 Container layer
를 추가한다.
새로운 데이터의 추가, 수정, 삭제 등 모든 변경사항은 Container layer
에 작성된다. 다이어 그램으로 표현하면 다음과 같다.
하지만 기존 데이터의 수정, 삭제가 필요하다면 어떻게 해야 할까?
기존 Data를 수정, 삭제 즉 image layer
를 수정, 삭제하는 것은 불가능하다.
때문에 기존 파일의 Data를 쓰기 가능한 Container layer
로 복사한 후 이를 변경해야 한다. 이것을 Copy-on-Write
전략이라 한다. 변경 사항은 기존의 image layer에는 저장되지 않으며 변경 후에는 Container layer에 저장된 내용만을 볼 수 있다.
이 전략은 컨테이너 시작 시간을 단축할 수 있다. 컨테이너를 생성할 때 Docker는 기존 Image layer의 전체 복사본을 만들지 않고 Write
가 가능한 한 개의 컨테이너 계층만을 생성하면 되기 때문이다.
하지만 하위 layer에 있는 원본 Data를 유지하고, 상위(Container) layer의 변경된 Data도 유지해야 하므로 저장공간을 많이 사용한다. 때문에 base image의 변경은 지양하고 변경 사항은 이전 포스트에서 설명한 Volume를 통해서 관리하는 것이 옳다.
Dockerfile은 우리의 어플리케이션이 구동하는데 필요한 런타임 환경을 명시하는 곳이다. 아래처럼 작성해보자
위에서 부터 아래로 한줄 씩, STEP 별로 image layer가 쌓인다. Image는 layer 구조
인데 같은 도커 파일을 여러 번 빌드하면 캐시
를 많이 사용할수록 빌드가 빠를 것이다. 앞에서 상위 layer는 하위 layer를 참조
한다는 설명을 했는데 하위 layer가 바뀌면 상위 layer도 바뀌기 때문에 한 지점의 layer가 변경되면 그 위의 layer 전체가 다시 빌드된다.
결국 도커 파일을 구성할 때 중요한 점은 이후의 변경 사항이 이전의 것들에게 최소한의 영향을 미치도록 구성하는 것이다.
만약 package.json파일 복사 위치가 자주 변경된다면 COPY를 WORKDIR 보다 먼저 작성하는 것이 옳다.
이런 특징을 갖기 때문에 Docker가 빠른 것이다. 기존의 배포 시스템은 하나의 엄청 큰 파일을 배포했기 때문에 사소한 변경 사항이 생기면 엄청 큰 파일을 다시 배포해야했지만 도커는 변경된 파일만 배포하면 되기 때문이다.