Canvas 1 Layer 1

도커 튜토리얼 : 깐 김에 배포까지

도커Docker는 2013년에 등장한 새로운 컨테이너 기반 가상화 도구입니다. 파이콘 US 2013Pycon US 2013에서 솔로몬 하이크Solomon Hykes에 의해서 처음 소개되었던 도커는 그 이후로 빠른 발전을 거쳐 2016년 10월 현재 1.12.2 버전이 릴리즈 되었습니다. 도커는 운영체제 상에서 컨테이너로 프로세스를 격리해서 실행할 수 있도록 도와주며, 계층화된 파일 시스템에 기반해 컨테이너의 변경사항을 모두 추적하고 관리할 수 있습니다. 이를 통해 컨테이너의 특정한 상태를 다시 이미지로 보존하고 공유할 수 있으며, 도커만 설치되어 있다면 필요할 때 언제 어디서나 이를 실행할 수 있 있도록 해줍니다. 이 문서는 도커 입문자를 튜토리얼로 도커의 핵심적인 개념들과 기본적인 사용법에 대해서 소개합니다.

도커(Docker) 소개 - 우분투에서 센트OS 환경 실행하기

가상 머신Virtual Machine은 이제 대중적인 도구가 되었습니다. 본체와 격리된 환경을 제공함으로서 개발과 테스트를 비롯해 다양한 목적으로 활용되고 있습니다. 하지만 하드웨어를 소프트웨어적으로 가상화하는 가상 머신은 일반적으로 성능 손실을 비롯해 많은 단점을 가지고 있습니다. 격리된 환경을 제공한다는 데서는 매력적이지만 동시에 같은 환경을 그대로 배포하기에는 성능적으로 불리합니다.

이러한 하드웨어를 소프트웨어로 가상화하는 가상 머신과 달리 프로세스 격리에 기반한 가상화 방법 중 하나가 컨테이너형 가상화 기술입니다. 도커는 이러한 컨테이너 형 가상화를 지원하는 도구 중 하나입니다. 도커는 단순히 가상 머신의 역할을 넘어서 어느 플랫폼에서나 이전의 상태를 그대로 재현가능한 애플리케이션 컨테이너를 운용하는 것을 목표로 합니다. LXC(리눅스 컨테이너)에서 출발한 도커의 가상화는 기존의 방식과는 근본적으로 다른 접근이라는 점을 짚어둘 필요가 있습니다. 이는 가상 머신과 같이 하드웨어를 가상화하는 것이 아니라, 운영체제 상에서 지원하는 방법을 통해서 하나의 프로세스(컨테이너)를 실행하기 위한 별도의 환경을 구축하는 일을 지원하고, 이러한 환경에서 프로세스를 격리시켜 실행해주는 도구라고 할 수 있습니다.

가상 머신과의 차이를 보기 위해서, 간단한 예를 살펴보겠습니다. 예를 들어 우분투Ubuntu에서 센트OSCentOS 환경을 구축하는데 시간이 얼마나 걸릴까요? 먼저 우분투 상에서 작업을 하고 있다고 가정해보겠습니다.

이미 도커가 설치되어있는 환경이라면 명령어 하나로 센트OS 머신을 실행할 수 있습니다.

$ docker run -it --rm centos:latest bash
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
8d30e94188e7: Pull complete
Digest: sha256:2ae0d2c881c7123870114fb9cc7afabd1e31f9888dac8286884f6cf59373ed9b
Status: Downloaded newer image for centos:latest

[root@881189373f8b /]# 

docker run -it centos:latest bash 명령어는 도커를 통해서 centos:latest 이미지로부터 bash를 실행하라는 의미입니다. 아직 이미지가 없다면, 도커의 공식 저장소에서 이 이미지를 다운 받아옵니다. 그리고 이 이미지를 기반으로 bash 프로세스를 실행합니다. 안정적인 네트워크 환경이 보장된다면 이미지를 받는 데는 30초가 걸리지 않습니다. 명령어를 실행하는 데는 우분투 환경에서 실행하는 것과 거의 같다고 봐도 무방합니다. 마지막에는 셸이 달라진 것을 볼 수 있습니다. 그럼 정말 센트OS인지 확인해보도록 하겠습니다.

[root@881189373f8b /]# cat /etc/redhat-release
CentOS Linux release 7.2.1511 (Core)

무슨 일이 벌어진 걸까요? 이 튜토리얼에서는 도커의 원리보다는 도구로서 도커를 사용하는 법에 집중합니다. 구체적인 내용은 컨테이너 기반 가상화와 도커의 원리에 대해서 다루는 문서들을 참고해보시기 바랍니다. 한국어로 된 좋은 자료로는 네이버 데뷰Naver Deview 2013에서 김영찬 님이 발표하신 이렇게 배포해야 할까? - Lightweight Linux Container Docker 를 활용하여 어플리케이션 배포하기 세션과 xym 님이 쓴 docker the cloud를 추천합니다.

원리적인 부분도 중요하지만 이 도구를 실제로 활용하려면 컨테이너와 이미지의 차이를 이해해야 하고, Dockerfile을 사용해 자신만의 이미지를 만드는 법을 익히는 것도 중요합니다. 이 글에서는 바로 이러한 관점에서 개발 환경이자 배포 도구로써 도커를 이해하기 위한 개념들을 소개하고 애플리케이션을 도커로 이미지화하는 부분까지 다루겠습니다.

도커(Docker) 설치하고 기본적인 설정하기

도커는 다양한 환경에서 사용할 수 있습니다. 개발 환경에서는 주로 도커와 관련 도구들로 구성된 패키지인 도커 포 맥Docker for Mac도커 포 윈도우Docker for Windows를 사용합니다. 단, 도커는 기본적으로 리눅스 환경에서 개발된 도구이기 때문에 맥OSmacOS나 윈도우즈에서는 운영체제에서 직접 컨테이너 가상화를 지원하지 않기 때문에 얕은 가상 머신 레이어를 사용합니다.

리눅스 환경에서는 각 배포판에서 제공하는 패키지 관리자를 사용해서 도커를 설치할 수 있습니다. 단, 일반적으로 패키지 관리자는 보수적으로 관리되기 때문에 최신 버전을 설치하고자 한다면 도커에서 제공하는 스크립트를 사용해서 설치하는 것이 편리합니다. 여기서는 이 스크립트를 통해서 정식 릴리즈된 도커 최신 버전을 설치해보겠습니다.*

* curl이 없다면 sudo apt-get curl로 설치하시면 됩니다.

$ curl -s https://get.docker.com | sudo sh

스크립트를 실행하면 현재 환경을 파악하고 적절한 과정을 거쳐 도커를 설치할 것입니다. 몇 분 정도의 시간이 소요됩니다. 스크립트는 조금 복잡해보이지만, 우분투 환경에서라면 패키지 관리자에 도커의 저장소를 추가하고 docker-engine 패키지를 설치하도록 되어있습니다. 스크립트가 끝나면 도커가 정상적으로 설치되었는지 확인합니다.

$ docker -v
Docker version 1.12.2, build bb80604

스크립트가 어떻게 도커를 설치했는지 간단히만 살펴보겠습니다.

$ cat /etc/apt/sources.list.d/docker.list
deb [arch=amd64] https://apt.dockerproject.org/repo ubuntu-xenial main

$ dpkg --get-selections | grep docker
docker-engine               install

스크립트로 도커 설치를 하고 나면 위와 같이 /etc/apt/sources.list.d/docker.list가 추가된 것을 볼 수 있습니다. 그리고 dpkg 명령어로 설치된 패키지 중에서 docker 이름이 들어간 패키지를 찾아봅니다. docker-engine이 설치된 것을 볼 수 있습니다.

도커가 설치되면 실제로 컨테이너를 운용하는 시스템 서비스도 같이 실행됩니다. 클라이언트는 이 서비스 프로세스에 명령얼 보내는 방식으로 작동합니다. 그렇다면 이제 도커 명령어를 하나 실행해보겠습니다.

$ docker ps 
Cannot connect to the Docker daemon. Is the docker daemon running on this host?

docker ps는 앞으로 자주 사용할 명령어 중 하나로, 현재 실행중인 모든 컨테이너 목록을 출력하라는 명령어입니다. 하지만 도커 데몬에 연결할 수 없다는 에러 메시지가 나옵니다. 앞서 얘기했듯이 도커를 설치하는 순간 도커 서비스도 같이 실행됩니다. 일반적으로 이러한 에러가 발생하는 이유는 사용자에게 도커 서비스(소켓)에 접근할 권한이 없기 때문입니다.

사용자 계정에서도 도커를 직접 사용할 수 있도록 docker 그룹에 사용자를 추가해줍니다. 이 때 관리자 권한이 필요합니다.

sudo usermod -aG docker your-user

셸을 재실행하고 나면 도커를 직접 사용할 수 있습니다.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAME

아직 아무것도 실행하지 않아서 목록에 출력되는 것은 없지만 정상적으로 작동하는 걸 볼 수 있습니다. 이제 본격적으로 도커를 사용해보도록하겠습니다.

도커 이미지(Image) 기초

이미지와 컨테이너는 도커를 이해하는 데 있어 가장 중요한 개념입니다. 여기서는 먼저 이미지 개념에 대해서 같이 살펴보겠습니다. 이미지는 가상 머신에서 사용하는 이미지와 같이 어떤 환경을 저장해둔 파일들입니다. 일반적으로 어떤 애플리케이션을 바로 실행하기 위해 준비해두고, 이 이미지를 통해서 곧 바로 애플리케이션을 배포할 수 있습니다.

처음에 간단히 살펴 본 예제에서는 centos 컨테이너에서 bash 셸을 실행했습니다. 이 과정을 좀 더 풀어써보면 다음과 같습니다.

  1. 도커 레지스트리에서 centos 이미지를 풀 받아서 로컬로 다운로드 받는다.
  2. 이 이미지를 통해서 컨테이너를 실행한다.

그렇다면 실제로 이 과정을 나눠서 진행해보겠습니다. 이번에는 1의 이미지를 받는 부분만을 진행합니다. 먼저 이제 막 도커를 설치했으니 이미지가 없다는 것을 확인해봅니다.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

아직 목록이 비어있는 것을 확인할 수 있습니다. 그렇다면 centos 이미지를 도커 레지스트리 상에서 받아와보겠습니다. 도커에서는 docker pull <IMAGE_NAME> 명령어로 이미지를 다운받을 수 있습니다.

$ docker pull centos
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
8d30e94188e7: Pull complete
Digest: sha256:2ae0d2c881c7123870114fb9cc7afabd1e31f9888dac8286884f6cf59373ed9b
Status: Downloaded newer image for centos:latest

이미지 이름은 :을 통해 이름과 태그로 구분됩니다. 이름을 지정하지 않으면 기본적으로 latest 태그를 사용합니다. 따라서 centos라는 이름은 centos:latest로 해석됩니다. 먼저 이 이미지를 로컬에서 찾아보고 없으면 도커 공식 저장소에서 찾아봅니다. 저장소에 해당하는 이미지가 있으면 풀(pull)을 받아옵니다. 마지막 줄의 Downloaded newer image for centos:latest 메시지로 부터 centos:latest 이미지가 다운로드된 것을 알 수 있습니다.

여기서도 알 수 있지만 도커의 가장 큰 매력 중 하나는 별다른 설정 없이 곧바로 공식 저장소를 통해서 이미지를 받아올 수 있다는 점입니다. 이미 리눅스의 apt-get, yum이나 프로그래밍 언어의 gem, cpan, pip 같은 패키지 매니저에 익숙하시다면 어렵지 않게 이해하실 수 있을 것입니다. 이런 도구들에 익숙하지 않다면 어딘가에 도커 이미지를 관리해주는 별도 서버가 있어서 필요할 때 다운로드 받을 수 있다고 이해하셔도 무방합니다.

한 가지 독특한 점은 pull이라는 명령어 이름입니다. 도커에서는 이미지를 다운 받을 때 install이나 download와 같은 이름 대신 pull을 사용합니다. 앞으로 살펴보겠지만 이는 단순히 이미지를 다운로드 받는 데서만 그런 것은 아닙니다. 이미지를 업로드 할 때는 push라는 명령어를 쓰고, 새로운 이미지를 저장할 때는 commit, 이미지의 차이를 확인할 때는 diff라는 명령어를 사용하기도 합니다. 이러한 이름들에서 대해서 깃Git이나 서브버전Subversion과 같은 VCS에 익숙하시다면 추가적인 설명은 필요없을 것입니다. 일단 기능적으로는 이미지를 다운로드 받아온다고 이해해주시기 바랍니다.

앞서 사용해본 images 명령어로 다운 받은 이미지를 확인해보겠습니다.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              980e0e4c79ec        5 weeks ago         196.8 MB

centos:latest 이미지가 추가된 것을 볼 수 있습니다. 앞서 pull 명령어가 VCS의 명령어와 비슷한 것처럼 도커에서는 하나의 이미지를 저장소repository라고 부르기도 합니다. TAG는 임의로 붙여진 추가적인 이름입니다. 일반적으로 이미지의 버저닝을 하기 위해서 사용됩니다. 앞서 이야기했듯이 도커 명령어에서 이미지를 지정할 때 태그를 생략하면 latest가 사용됩니다. IMAGE ID는 이미지를 가리키는 고유한 해시 값입니다. CREATED는 이미지가 생성된 시간, SIZE는 이미지의 전체 용량을 나타냅니다. docker images는 앞으로 자주 사용하게 될 명령어이니 꼭 익혀두시기 바랍니다.

도커 허브(Docker Hub) - 이미지 공식 저장소

공식 저장소에 대해서 조금 더 살펴보도록 하겠습니다. docker info를 통해서 공식 저장소의 주소를 확인할 수 있습니다.

$ docker info
…
Registry:  [https://index.docker.io/v1/](https://index.docker.io/v1/) 
…

이 주소는 API 서버라서 직접 들어가도 Docker Registry API라고만 출력될 것입니다. 저장소에서 이미지와 관련된 정보를 살펴볼 수 있는 웹 사이트는 도커 허브입니다. 도커 허브의 centos 이미지 페이지에서 더 자세한 내용을 확인할 수 있습니다.

도커 허브(혹은 도커 공식 레지스트리)에는 아주 많은 이미지들이 등록되어 있습니다. 이러한 이미지들은 도커 사에서 공식적으로 제공하는 이미지와 사용자들이 직접 만들어 올린 이미지들로 나눠집니다.

도커 사에서는 우분투와 센트OS와 같이 일반적으로 다른 환경을 구축하기 위해서 사용하는 이미지와 MySQL이나 레디스Redis, 워드프레스Wordpress와 같이 자주 사용되는 애플리케이션에 대한 공식 이미지를 제공하고 있습니다. 앞서 다운로드 받고 실행해본 센트OS 이미지는 도커가 공식적으로 제공하는 이미지에 해당합니다. 일반적으로 도커가 제공하는 공식 이미지에는 네임스페이스가 없습니다. 네임스페이스는 이미지 이름에서 슬래시(/)로 구분되며, 슬래시 앞 부분이 네임스페이스가 되고, 슬래시 뒷 부분이 실제 이미지 네임이 됩니다. 도커의 공식 저장소에서는 사용자 이름을 네임스페이스로 사용합니다. 예를 들어 user1932라는 사용자가 wordpress라는 이름의 이미지를 만들면 전체 이미지 이름은 user1932/wordpress가 됩니다.

이렇게 이미지를 공유할 수 있는 도커 허브 서비스를 제공했던 게 도커가 초반에 자리잡을 수 있도록 중요한 역할을 했다고 할 수 있습니다. 더 자세한 내용은 뒤에서 다루겠습니다. 우선 도커에 대해서 조금 더 배우고 나서 공식 저장소에서 다른 사람들이 만든 이미지를 찾고, 직접 만든 이미지를 올리는 법에 대해서도 알아보겠습니다.

컨테이너(Container) 이해하기 - 격리된 환경에서 실행되는 프로세스

앞서 centos 컨테이너를 실행하는 것은 다음과 같은 두 단계를 거친다고 이야기했습니다.

  1. 도커 레지스트리에서 centos 이미지를 풀 받아서 로컬로 다운로드 받는다.
  2. 이 이미지를 통해서 컨테이너를 실행한다.

이번에는 두번째 단계에 대해서 좀 더 자세히 다뤄보겠습니다. 이미지는 어떤 환경이 구성되어있는 상태를 저장해놓은 파일 집합이라고 이야기한 바 있습니다. 바로 이 이미지를 불러와 이 환경 위에서 특정한 프로세스를 격리시켜 실행한 것을 컨테이너라고 부릅니다.

우선 컨테이너는 docker run를 통해서 실행할 수 있습니다. 여기서는 셸에 접속하기 위해서 -it를 옵션을 붙여서 docker run -it <이미지이름:태그> <명령어>centos 이미지를 기반으로 컨테이너를 하나 실행시켜보겠습니다.

$ docker run -it centos:latest bash
[root@d3fef9c0f9e9 /]#

셸이 root@d3fef9c0f9e9로 바뀐 것을 볼 수 있습니다. 우분투 환경에서 센트OS환경에 접속했습니다. 하지만 이미지에 접속했다는 말에는 함정이 있습니다. 이 말은 마치 가상머신에 SSH 프로토콜을 사용해 접근한 것과 같은 착각을 일으킵니다. 하지만 여기서는 SSH를 통해 접속하느 것이 아니라, 완전히 격리된 환경에서 직접 bash 프로그램을 실행했다고 이해하는 것이 더 정확합니다. 컨테이너란 사실 프로세스에 불과하기 때문에 bash 대신 SSH 서버를 실행하고 SSH 클라이언트를 통해서 접속하는 것도 가능합니다.

이제 새로운 명령어를 하나 배워보도록 하겠습니다. 앞서 사용가능한 이미지들을 확인하는 명령어는 docker images입니다. 이번에 사용할 명령어는 현재 실행중인 컨테이너 목록을 출력하는 명령어 docker ps입니다. 컨테이너가 배시 셸에 붙어있는 상태에서 별도의 쉘이나 터미널을 열고 docker ps 실행시켜보시기 바랍니다.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
d3fef9c0f9e9        centos:latest       "bash"              5 minutes ago       Up 5 minutes                            compassionate_turing

5분 전에 만들어진 컨테이너가 실행되고 있는 것을 알 수 있습니다. 여기서 우리가 실행한 컨테이너 정보를 알 수 있습니다. 이 예제에서는 centos:latest 이미지로부터 컨테이너를 생성했고, 이 격리된 환경에서 /bin/bash라는 명령어로 컨테이너를 실행했습니다. 그 외에도 맨 앞의 컨테이너 아이디는 앞으로 도커에서 컨테이너를 조작할 때 사용하는 컬럼이기 때문에 필수적으로 알아둘 필요가 있습니다. 마지막 컬럼은 임의로 붙여진 컨테이너의 이름입니다. 컨테이너를 조작할 때는 컨테이너 아이디를 사용할 수도 있고, 이름을 사용할 수도 있습니다. 이름은 docker run을 할 때 --name으로 붙일 수 있지만, 붙이지 않으면 임의의 이름이 자동적으로 부여됩니다.

위의 예제에서는 직접 명령어를 넘겨서 이미지를 컨테이너로 실행시켰습니다만, 보통 이미지들은 자신이 실행할 명령어들을 가지고 있습니다. 예를 들어 레디스Redis, 마리아DBMariaDB, 루비 온 레일스Ruby on Rails 어플리케이션을 담고 있는 이미지라면, 각각의 어플리케이션을 실행하는 스크립트를 실행합니다. 컨테이너는 독립된 환경에서 실행됩니다만, 컨테이너의 기본적인 역할은 이미지 위에서 미리 규정된 명령어를 실행하는 일입니다. 이 명령어가 종료되면 컨테이너도 종료 상태(Exit)에 들어갑니다. 이러한 죽은 컨테이너의 목록까지 확인하려면 docker ps -a 명령어를 사용하면 됩니다. 실제로 쉘을 종료하고 컨테이너 목록을 확인해보겠습니다.

앞서 실행된 센트OS의 배시 셸에서 exit 명령어로 셸을 종료합니다.

root@65d60d3dd306:/# exit

컨테이너가 종료되었는지 docker ps를 통해서 살펴보겠습니다.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

이미지 목록이 비어있는 것을 볼 수 있습니다. 종료된 컨테이너까지 보기 위해서는 -a 옵션을 사용해야합니다. 확인해보겠습니다.

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
d3fef9c0f9e9        centos:latest       "bash"              13 minutes ago      Exited (0) 34 seconds ago                       compassionate_turing

13분 전에 만들어진 d3fef9c0f9e9 컨테이너가 34초 전에 종료된 것을 알 수 있습니다. 여기서 중요한 점은 컨테이너는 SSH 서버가 아니라 배시 셸 프로세스이기 때문에, 셸을 종료하면 컨테이너도 종료된다는 점입니다. 이번엔 restart 명령어를 통해 이미지를 되살려보겠습니다.

$ docker restart d3fef9c0f9e9
d3fef9c0f9e9
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
d3fef9c0f9e9        centos:latest       "bash"              14 minutes ago      Up 8 seconds                            compassionate_turing
$

컨테이너가 되살아났습니다! 하지만 셸이 실행되지는 않습니다. 컨테이너에 실행된 프로세스와 터미널 상에서 입출력을 주고 받으려면 attach 명령어를 사용해야합니다.

$ docker attach d3fef9c0f9e9
[root@d3fef9c0f9e9 /]#

다시 도커 컨테이너 안으로 들어왔습니다! 여기까지 도커 컨테이너의 생명 주기를 보았습니다. 도커 컨테이너를 실행시키는 run부터 실행이 종료되었을 때 다시 실행하는 restart를 배웠습니다. 이 외에도 실행된 컨테이너를 강제로 종료시키는 stop 명령어가 있으며, 종료된 컨테이너를 삭제하는 rm 명령어도 있습니다. 잠시 기억을 더듬어 올라가 이 글의 아주 처음에 run 명령어와 함께 사용한 --rm 플래그는 컨테이너가 종료 상태로 들어가면 자동으로 삭제를 해주는 옵션입니다.

이쯤에서 이미지와 컨테이너를 정확하게 짚고 넘어갈 필요가 있을 것 같습니다. 이미지와 컨테이너가 아주 헷갈리는 개념은 아닙니다. 이미지가 미리 구성된 환경을 저장해 놓은 파일들의 집합이라면, 컨테이너는 이러한 이미지를 기반으로 실행된 격리된 프로세스입니다.

이미지는 가상 머신에서 사용하는 개념과 비슷합니다. 하지만 보통 가상 머신에서는 저장된 이미지를 기반으로 특정 상태의 가상 머신을 복원합니다. 컨테이너는 가상 머신처럼 보이지만 가상 머신은 아닙니다. 가상 머신이 컴퓨터라면, 컨테이너는 단지 격리된 프로세스에 불과합니다. 보통 도커 컨테이너를 처음 다루는 예제에서 셸을 많이 다루기 때문에 컨테이너는 마치 가상 머신처럼 보이는 착각을 일으킵니다. 다시 한 번 강조합니다. 컨테이너는 가상 머신이라기보다는 프로세스입니다. 이 사실을 기억해주시기 바랍니다.

도커와 버전 관리 시스템

누누이 강조하지만 이미지와 컨테이너는 다릅니다. 좋습니다. 그렇다면 한가지 질문을 던져보죠. 그렇다면 이 컨테이너를 지지고 볶고 삶고 데치고 하면 이미지에는 어떤 변화가 생길까요?

당연히 이미지에는 아무런 변화도 생기지 않습니다. 아주 간단한 예를 들어보자면, 윈도우 CD로 윈도우를 설치해서 사용한다고 해서 설치한 윈도우 CD에 어떤 변화가 생기지는 않는 것과 같은 이치입니다. 이미지는 어디까지나 고정되어있습니다. 도커에서 이미지는 불변Immutable한 저장 매체입니다. 그도 그럴 것이 한 번 생성된 이미지를 실행한다고 변형이 된다고 하면 이미지를 통해 어떤 환경을 재현한다는 건 아무런 의미가 없어져버립니다. 마치 VCS에서 특정 커밋이 항상 그 커밋인 것과 비슷합니다. 이미지는 불변이지만, 그 대신 도커에서는 이 이미지 위에 무언가를 더해서 새로운 이미지를 만들어내는 일이 가능합니다. 좀 더 정확히 말하면 컨테이너는 변경 가능Mutable합니다. 도커의 또 하나 중요한 특징은 바로 계층화된 파일 시스템을 사용한다는 점입니다. 특정한 이미지로부터 생성된 컨테이너에 어떤 변경사항을 더하고, 이 변경된 상태를 이미지로 만들어내는 것이 가능합니다. 바로 이러한 점 때문에 도커에서는 저장소Repository, 풀Pull, 푸시Push, 커밋Commit, 차분Diff 등 VCS에서 사용하는 용어들을 차용하는 것으로 보입니다.

마치 깃Git 저장소에 새로운 커밋을 추가하듯이, 도커에서 새로운 이미지를 생성하는 과정을 따라가보겠습니다. 이번에는 우분투 이미지를 사용하겠습니다. 먼저 우분투 제니얼Ubuntu Xenial 이미지를 다운로드 받고, 앞서 배운대로 배시 셸을 실행해봅니다.

$ docker pull ubuntu:xenial
…
$ docker run -it ubuntu:xenial bash
root@65d60d3dd306:/#

이 컨테이너에 깃 클라이언트를 설치해보겠습니다. 위에서 실행한 셸에 다음 명령어를 입력합니다.

root@65d60d3dd306:/# apt-get update
…
root@65d60d3dd306:/# apt-get install -y git
…
root@65d60d3dd306:/# git --version
git version 2.7.4

우분투의 패키지 관리자인 apt-get을 통해서 버전 관리 시스템인 깃Git을 설치했습니다. 여기서 도커는 마치 자신이 [[VCS]]라도 된 것처럼, 어떤 컨테이너와 이 컨테이너의 부모 이미지 간의 파일 변경사항을 확인할 수 있는 명령어를 제공합니다. git diff 명령어로 프로젝트의 변경사항을 확인하듯이, docker diff 명령어로 부모 이미지와 여기서 파생된 컨테이너의 파일 시스템 간의 변경사항을 확인할 수 있습니다. 셸을 그대로 두고 다른 터미널에서 다음 명령어를 실행해봅니다.

$ docker diff 65d6
…
C /var/log
C /var/log/alternatives.log
C /var/log/apt
C /var/log/apt/history.log
A /var/log/apt/term.log
C /var/log/dpkg.log

여기서 A는 ADD, C는 Change, D는 Delete의 줄임말입니다. 결과를 보면 알 수 있지만 패키지 하나를 설치하는 것만으로 아주 많은 파일들이 바뀐 것을 알 수 있습니다. 앞서 얘기했지만 컨테이너에서 어떤 작업을 한다고 원래의 이미지가 달라지지는 않습니다. 이를 확인하기 위해서 원래의 이미지에서 새로운 컨테이너를 하나 더 실행하고 git 명령어가 있는 지 확인해보겠습니다.

$ docker run -it --rm ubuntu:xenial bash
root@33f6039322df:/# git --version
bash: git: command not found
root@33f6039322df:/# exit

분명히 이전 컨테이너는 git 명령어가 추가되었지만 이번에 실행한 컨테이너에는 추가되지 않았습니다. 이제 ubuntu:xenial 이미지에 깃이 설치된 새로운 이미지를 생성해보도록하겠습니다. 이 또한 VCS와 매우 비슷합니다. 도커에서는 이 작업을 commit이라고 합니다.

$ docker commit 65d6 ubuntu:git
sha256:487a3619305e68483059caa21eb54d1d812ced4282df9e2ba05ec46ed9a2b8f4
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              git                 487a3619305e        15 seconds ago      256.8 MB
ubuntu              latest              f753707788c5        2 days ago          127.2 MB
ubuntu              xenial              f753707788c5        2 days ago          127.2 MB

이미지 만들기 별 거 없죠? 단지 커밋을 하고 뒤에 이름을 붙여주면 바로 새로운 도커 이미지가 생성됩니다. 이미지로부터 컨테이너를 실행시키고 이 컨테이너의 수정사항을 통해서 새로운 이미지를 만들었습니다. 그렇다면 정말로 이 이미지를 통해서 컨테이너를 실행시키면(run) git 명령어가 있을까요? 직접 확인해보죠.

$ docker run -i -t ubuntu:git bash
root@2a00b9b2b7cc:/# git --version
git version 2.7.4
root@2a00b9b2b7cc:/# exit

감동적인 순간입니다. git이 설치되어 있는 걸 활인할 수 있습니다! 하지만 이 이미지는 별로 필요가 없군요. 냉정하게 필요없는 이미지는 삭제해버리겠습니다. 하나 알아두셔야 하는 중요한 사항은, 이미지에서 파생된 (종료상태를 포함한) 컨테이너가 하나라도 있다면 이미지는 삭제할 수 없습니다. 따라서 먼저 컨테이너를 종료하고, 삭제까지 해주어야합니다. docker rm은 컨테이너를 삭제하는 명령어이고, docker rmi는 이미지를 삭제하는 명령어입니다. 이 두 명령어를 혼동하지 않아야합니다.

먼저 컨테이너를 지우고, 이미지를 삭제해보겠습니다.

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                        PORTS               NAMES
2a00b9b2b7cc        ubuntu:git          "bash"              2 minutes ago       Exited (130) 3 seconds ago                        cranky_franklin
$ docker rm 2a00b9b2b7cc
2a00b9b2b7cc
$ docker rmi ubuntu:git
Untagged: ubuntu:git
Deleted: sha256:487a3619305e68483059caa21eb54d1d812ced4282df9e2ba05ec46ed9a2b8f4
Deleted: sha256:9b6621e819f094c16ea9f63af90f7cb564a48133c05504fad0f28563865f957d

브라보! 이번에는 도커 이미지의 생명주기를 배웠습니다. 도커 이미지를 pull로 받아오고 commit으로 파생된 이미지를 만들고 rmi 명령어로 삭제까지 해보았습니다. 컨테이너와 이미지의 생명주기만 이해하고 나면 도커의 대부분을 이해한 거나 다름 없습니다. 도커를 통해서 하는 일의 대부분은 이 이미지와 컨테이너 개념으로 커버가 가능합니다. 이제 남은 일은 자신에게 필요한 이미지를 만들고 이 이미지를 통해서 컨테이너를 실행하는 일입니다. 물론 이제 기본적인 개념들을 배웠으니 중앙 저장소인 도커 허브에서 이미 만들어져있는 다양한 이미지들을 활용할 수도 있습니다.

무엇을 하건 도커의 모토를 잊지 마시기 바랍니다. 거짓말 조금 보태서(?!) 완성된 이미지는 언제 어디에서라도 실행 가능합니다.

Dockerfile로 이미지 생성하고 어플리케이션 실행시키기

로컬에 도커 이미지를 추가하는 방법은 크게 세 가지가 있습니다. 먼저 pull을 사용하는 방법입니다. 그리고 컨테이너의 변경사항으로부터 이미지를 만드는 법에 대해서도 소개했습니다. 두번째 방법은 매우 흥미롭지만, 어딘가 2% 부족합니다. 이를 보완해주는 Dockerfile이라고 불리는 도커 이미지 생성을 위한 DSL이 있습니다.

Dockerfile은 특정한 이미지로부터 새로운 이미지 구성에 필요한 일련의 명령어들을 저장해놓는 파일입니다. 미리 만들어둔 docker-moniwiki라는 프로젝트를 통해서 간단히 Dockerfile을 설명하고, 실제로 모니위키 어플리케이션을 가진 이미지를 생성하고 컨테이너를 통해서 실행시켜보도록하겠습니다. 먼저 프로젝트를 클론 받습니다.

$ git clone https://github.com/nacyot/docker-moniwiki.git

이 프로젝트에는 3개의 파일이 있습니다만, 실질적으로 Dockerfile 하나밖에 없다고 보셔도 무방합니다. 전체 파일 내용은 다음과 같습니다.

FROM ubuntu:12.04
MAINTAINER Daekwon Kim <propellerheaven@gmail.com>

# Run upgrades
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
RUN apt-get update

# Install basic packages
RUN apt-get -qq -y install git curl build-essential

# Install apache2
RUN apt-get -qq -y install apache2
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
RUN a2enmod rewrite

# Install php
RUN apt-get -qq -y install php5
RUN apt-get -qq -y install libapache2-mod-php5

# Install Moniwiki
RUN apt-get install rcs
RUN cd /tmp; curl -L -O http://dev.naver.com/frs/download.php/8193/moniwiki-1.2.1.tgz
RUN tar xf /tmp/moniwiki-1.2.1.tgz
RUN mv moniwiki /var/www/
RUN chown -R www-data:www-data /var/www/moniwiki
RUN chmod 777 /var/www/moniwiki/data/ /var/www/moniwiki/
RUN chmod +x /var/www/moniwiki/secure.sh
RUN ./var/www/moniwiki/secure.sh

EXPOSE 80
CMD ["/usr/sbin/apache2", "-D", "FOREGROUND"]

보시는 바와 같이 Dockerfile은 모니위키를 설치하는 일련의 과정과 서버를 실행하는 명령어로 구성되어 있습니다. 각각의 부분을 간략히 살펴보겠습니다.

FROM ubuntu:12.04
MAINTAINER Daekwon Kim <propellerheaven@gmail.com>

먼저 맨 위에 정의된 FROM은 어떤 이미지로부터 새로운 이미지를 생성할 지를 지정합니다. 다음으로 MAINTAINER는 이 Dockerfile을 관리하는 사람을 입력해줍니다.

RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
RUN apt-get update

RUN은 직접 쉘 명령어를 실행하는 명령어입니다. 이 때 바로 뒤에 명령어를 입력하게 되면 쉘을 통해서 명령어가 실행됩니다. 위의 두 줄은 패키지 관리자 apt-get에 저장소를 추가하고 저장소 정보를 갱신하는 명령어입니다.

# Install basic packages
RUN apt-get -qq -y install git curl build-essential

# Install apache2
RUN apt-get -qq -y install apache2
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
RUN a2enmod rewrite

# Install php
RUN apt-get -qq -y install php5
RUN apt-get -qq -y install libapache2-mod-php5

다음 부분에서는 모니위키 설치에 필요한 패키지들과 apache2 서버, php 프로그램을 설치 및 설정해줍니다. 아파치 설치 과정에서 나오는 ENV를 통해 환경 변수를 지정할 수 있습니다.

# Install Moniwiki
RUN apt-get install rcs
RUN cd /tmp; curl -L -O http://dev.naver.com/frs/download.php/8193/moniwiki-1.2.1.tgz
RUN tar xf /tmp/moniwiki-1.2.1.tgz
RUN mv moniwiki /var/www/
RUN chown -R www-data:www-data /var/www/moniwiki
RUN chmod 777 /var/www/moniwiki/data/ /var/www/moniwiki/
RUN chmod +x /var/www/moniwiki/secure.sh
RUN ./var/www/moniwiki/secure.sh

이제 실제로 모니위키를 설치합니다. 여기서는 모니위키를 curl을 통해서 다운로드 받고, 압축을 풀고 모니위키 실행에 필요한 권한 관련 설정을 해주고 있습니다.

EXPOSE 80
CMD ["/usr/sbin/apache2", "-D", "FOREGROUND"]

이제 마지막입니다. EXPOSE는 가상 머신에 오픈할 포트를 지정해줍니다. 마지막줄의 CMD는 컨테이너에서 실행될 명령어를 지정해줍니다. 이 글의 앞선 예에서는 docker run을 통해서 /bin/bash를 실행했습니다만, 여기서는 아파치 서버를 FOREGROUND에 실행시킵니다.

자 기다리고 기다리던 대망의 순간이 왔습니다. 직접 이 Dockerfile을 빌드할 차례입니다.

$ docker build -t nacyot/moniwiki .
Uploading context 71.68 kB
Uploading context
Step 1 : FROM ubuntu:12.04
---> 8dbd9e392a96
Step 2 : MAINTAINER Daekwon Kim <propellerheaven@gmail.com>
---> Running in a2af31ca9d62
---> c42835b9308b
Step 3 : RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
---> Running in d305ce1fea04
---> f4cb16c39b0e
Step 4 : RUN apt-get update
...
---> c63d093aacfb
Step 21 : EXPOSE 80
---> Running in cee6a6048c83
---> 7436a638e52c
Step 22 : CMD ["/usr/sbin/apache2", "-D", "FOREGROUND"]
---> Running in 2f251c355290
---> 0a148bb4de2f
Successfully built 0a148bb4de2f

docker build 명령어는 -t 플래그를 사용해 이름과 태그를 지정할 수 있습니다. 그리고 마지막에 .은 빌드 대상 디렉토리를 가리킵니다. 이 때 알아두면 좋은 게 하나 있습니다. 위에서 정의한 RUN 명령 하나 하나는 명령 하나마다 이미지가 됩니다. 기본적으로 이 빌드를 통해서 생성되는 최종 이미지는 nacyot/moniwi가 됩니다만, docker images -a를 통해서 살펴보면 이름없는 도커 이미지들이 다수 생성되는 것을 알 수 있습니다.

$ docker images -a
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
<none>              <none>              c63d093aacfb        4 minutes ago       670.4 MB
<none>              <none>              7436a638e52c        4 minutes ago       670.4 MB
nacyot/moniwiki     latest              0a148bb4de2f        4 minutes ago       670.4 MB
<none>              <none>              49825025193f        4 minutes ago       670.4 MB
<none>              <none>              5b374f859553        4 minutes ago       670.4 MB
<none>              <none>              c8afe13ab509        4 minutes ago       670.4 MB
<none>              <none>              bb65aa482123        4 minutes ago       663.1 MB
<none>              <none>              fa7f2059c9ba        4 minutes ago       655.8 MB
<none>              <none>              1a18589e4d9a        4 minutes ago       648.5 MB
<none>              <none>              fdd759b53314        4 minutes ago       646.1 MB
<none>              <none>              786c8ad1df43        4 minutes ago       610.5 MB
<none>              <none>              14e8b032683a        4 minutes ago       610.5 MB
<none>              <none>              6be08754c2ae        4 minutes ago       547.9 MB
<none>              <none>              67e17f5a39f1        4 minutes ago       547.9 MB
<none>              <none>              55ec9487188b        4 minutes ago       547.9 MB
<none>              <none>              88a61604a1d0        4 minutes ago       547.9 MB
<none>              <none>              7edabbb84352        4 minutes ago       547.9 MB
<none>              <none>              78e9afc826cd        4 minutes ago       503 MB
<none>              <none>              8066398c160f        5 minutes ago       272.5 MB
<none>              <none>              f4cb16c39b0e        6 minutes ago       128 MB
<none>              <none>              c42835b9308b        6 minutes ago       128 MB

이미지 아이디가 빌드 과정에서 출력되는 아이디와 같은 것을 알 수있습니다. 필요한 경우 중간 이미지에 접근하거나 직접 중간 이미지로부터 다른 이미지를 생성하는 것도 가능합니다. 정말 좋은 소식은 도커는 이러한 빌드 순서를 기억하고 각 이미지를 보존하기 때문에 같은 빌드 과정에 대해서는 캐시를 사용해 매우 빠르게 빌드가 가능하다는 점입니다. 실제로 Dockerfile을 만드는 과정에서는 많은 시행 착오를 겪게되는데, 중간에 빌드가 실패하더라도 성공했던 명령어까지는 거의 시간 소모 없이 빠르게 진행되도록 설계되어있습니다.

빌드 자체는 꽤나 번거로운 일입니다. 도커의 가상화가 굉장히 빠르다고 해도 어플리케이션 실행환경을 구축하는 일은 상당히 시간도 많이 걸립니다. 더욱이 빌드 자체는 완벽히 ’재현 가능’하지 않습니다. 하지만 이렇게 Dockerfile을 통해서 배치화를 시켜두면 Dockerfile이라는 정말 작은 파일 하나로 어플리케이션 배포 환경을 구축할 수 있다는 장점이 있으며, 또한 쉽게 유연하게 사용할 수 있습니다. 아주 흥미로운 이야기를 하나 해드리자면 도커 생태계에 있는 오픈소스 어플리케이션들은 아예 Dockerfile을 프로젝트에 포함하고 있습니다. 대표적으로 도커 모니터링 툴인 쉽야드Shipyard가 있습니다. 여기서 제공하는 Dockerfile을 빌드해서 이미지를 만들고, 이 이미지로 컨테이너를 가동하면 바로 스쉽야드 어플리케이션을 사용할 수 있습니다. 전율이 느껴지시나요?

Dockerfile은 단순히 어플리케이션 설치를 스크립트로 만들어주는 게 아니라, 배포환경 구축까지 한꺼번에 해주는 역할을 합니다. 오래전 제로보드Zeroboard나 테터툴즈Tattertools 한 번 설치해보겠다고 <? phpinfo() ?> 찍어가며 phpaphche가 잘 연동되었는지 확인해보고 안 되면 이유도 못 찾아 혼자 서러워했던 적이 있는 분이라면 이해하리라 믿습니다. 다른 예를 들어볼까요? 설치가 까다로운 걸로 악명높은 오픈소스 웹 어플리케이션 중에 깃랩Gitlab이라는 어플리케이션이 있습니다. gitlab-docker에서 제공하는 Dockerfile 하나면 이제 깃랩도 두렵지 않습니다. 그저 Build하고 Run하면 깃랩 서버가 실행됩니다.

네, 얘기가 길어졌네요. 다시 모니위키로 돌아갑니다. 빌드가 끝났으니 실행을 해보겠습니다.

$ docker run -d -p 9999:80 nacyot/moniwiki
746443ad118afdb3f254eedaeeada5abc2b125c7263bc5e67c2964b570166187

다시 docker run 입니다. 이번에는 -d-p 플래그를 사용합니다. 앞서서 자세히 설명하진 않았습니다만, -d 플래그는 -i 플래그의 반대 역할을 하는 플래그로, 컨테이너늘 백그라운드에서 실행시켜줍니다. -p는 포트포워딩을 지정하는 플래그입니다. :을 경계로 앞에는 외부 포트, 뒤로는 내부 포트입니다. 참고로 컨테이너 안에서 아파치가 80포트로 실행됩니다. 따라서 여기서는 localhost에 9999로 들어오는 연결을 여기서 실행한 컨테이너의 80포트로 보내도록합니다.

이제 정말 마지막입니다. 로컬의 9999 포트에 접근해 정말로 모니위키가 실행되는지 보도록 하겠습니다.

http://127.0.0.1:9999/moniwiki/
모니위키 설치 페이지
모니위키 설치 페이지

올레! 잘 돌아가네요.

정리

도커의 활용가능성은 무궁무진합니다. 당연히 실제 배포에도 사용할 수 있고, 유연하고 날렵한 격리된 환경을 활용해 실험적인 개발을 진행할 수도 있습니다. 베이그런트Vagrant보다도 훨씬 빠른 가상 환경을 활용할 수 있으며 접근 방법은 다르지만 서버환경을 자동적으로 구성할 수도 있습니다. 미리 만든 이미지를 자신의 저장소(registry)에 등록해 여러대의 머신에 컨테이너들을 자동적으로 배포할 수도 있고, 오픈소스 프로젝트에서 Dockerfile을 제공해 설치를 색다른 방법으로(?) 지원할 수도 있습니다. 도쿠Dokku를 사용하면 정말 쉽게 미니 히로쿠Heroku와 같은 PaaS 플랫폼을 구축해 볼 수도 있습니다.

단순히 생산성을 넘어, 도커의 매력은 상상력을 자극한다는 점입니다. 특히나 서버 자동화가 화두인 요즘에 인프라스트럭쳐가 코드로 변해버리는 묘한 체험을 하게 해줍니다. 인프라스트럭쳐가 코드가 되면 뭔가 신기한 일들이 벌어집니다. 이런 상황에서 아직까지 낯설게 느껴질지도 모르는 개념이고 툴입니다만 직접 설치하고 이것저것 해보다보면 저처럼 분명 반하실 거라 생각합니다 :)

이 문서는 2014년 1월에 발행했던 “도커(Docker) 튜토리얼 : 깐 김에 배포까지”를 2016년 10월 상황에 맞게 보완한 문서입니다.