이미지

며칠 전 5월 5일, 유명 PaaS 인 Heroku 가 Docker 로 배포하는 도구를 발표했습니다 (heroku). 요즘 회사에서 사내 개발자들을 위한 PaaS (Heroku 또는 AWS Beanstalk 와 비슷한) 를 만들고 있고, Docker 에도 관심이 많아서 한 번 살펴봤습니다. 제가 안 하면 아무도 안 해 줄 것 같기도 했습니다.

살펴본 결론은 이렇습니다. 우선 아무 Docker 이미지나 Heroku 에 올려서 돌릴 수 있는 것은 아닙니다. Heroku 가 자동으로 만들어 주는 Dockerfile 을 사용하거나, FROM heroku/cedar:14 이미지를 사용해야 Heroku에 올릴 수 있습니다. /app 아래에 파일들이 들어가야 한다는 조건도 있습니다. 그리고 ONBUILD 로 코드를 이미지에 포함시키기 때문에, 소스코드를 수정하면 heroku docker:start 를 다시 해 주어야 합니다. 개발용으로 volume mount 를 하는 등의 다른 방법이 분명 있을 것입니다.

장점은 (제 생각에) 앱에 있는 바이너리 의존성, 예를 들어 imagemagick 같은 것을 buildpack 방식에서는 복잡하게 해결해야 했는데, Docker 방식에서는 단순히 Dockerfile 에 의존성을 추가하고 PATH 를 추가하면 된다는 점입니다.

heroku-docker 설치

$ heroku plugins:install heroku-docker

샘플 앱 clone

$ git clone https://github.com/heroku/node-js-getting-started.git
$ cd node-js-getting-started

docker:init - 현재 디렉터리의 언어에 따라 Dockerfile 생성

$ heroku docker:init
  Wrote Dockerfile (node)

$ cat Dockerfile
  FROM heroku/cedar:14

  RUN useradd -d /app -m app
  USER app
  WORKDIR /app

  ENV HOME /app
  ENV NODE_ENGINE 0.12.2
  ENV PORT 3000

  RUN mkdir -p /app/heroku/node
  RUN curl -s https://s3pository.heroku.com/node/v$NODE_ENGINE/node-v$NODE_ENGINE-linux-x64.tar.gz | tar --strip-components=1 -xz -C /app/heroku/node
  ENV PATH /app/heroku/node/bin:$PATH

  RUN mkdir -p /app/.profile.d
  RUN echo "export PATH=\"/app/heroku/node/bin:/app/bin:/app/node_modules/.bin:\$PATH\"" > /app/.profile.d/nodejs.sh
  RUN echo "cd /app/src" >> /app/.profile.d/nodejs.sh

  EXPOSE 3000

  ONBUILD RUN mkdir -p /app/src
  ONBUILD COPY . /app/src
  ONBUILD WORKDIR /app/src
  ONBUILD RUN npm install

docker:init 내부 구현

docker:init 은 buildpack 과 동일한 방식으로 언어를 detect 합니다. 예를 들어 package.json 가 존재하면 Node.js 로 detect 합니다 (github). 구현은 platforms.detect(dir)fs.writeFileSync(dockerfile, contents) 부분에서 볼 수 있습니다. 현재 지원되는 언어는 node, python, ruby, scala 입니다. heroku docker:init --template minimal 으로 최소한의 Dockerfile 을 만들 수 있고, 여기서 생성되는 Dockerfile 이 minimal 템플릿입니다.

heroku/cedar:14 도커 이미지 내부 구현

이미지는 Docker Registry 에 올라가 있습니다. Cedar-14 는 ubuntu 14.04 LTS 를 기반으로 한 스택이고, 설치되는 ubuntu package 목록 도 따로 정리되어 있습니다.

Dockerfile 은 아래와 같이 단순합니다 (github).

FROM ubuntu-debootstrap:14.04
COPY ./bin/cedar-14.sh /tmp/build.sh
RUN LC_ALL=C DEBIAN_FRONTEND=noninteractive /tmp/build.sh \
  && rm -rf /var/lib/apt/lists/*

ubuntu-debootstrap 은 Docker Registry 에 있고, 공식 이미지 목록 에서 github.com/tianon/docker-brew-ubuntu-debootstrap 여기에 Dockerfile 이 있다는 것을 알 수 있습니다. 실제 ubuntu-debootstrap:14.04 의 Dockerfile 은 링크에서 볼 수 있습니다.

heroku/cedar:14 는 용량이 작은 최소한의 ubuntu docker image 에 cedar-14.sh 를 실행시킨 것입니다.

docker:start - 로컬에 컨테이너를 시작

$ heroku docker:start
  building image...
  Sending build context to Docker daemon 285.2 kB
  Sending build context to Docker daemon
  Step 0 : FROM heroku/cedar:14
  Pulling repository heroku/cedar
  66766a8c5395: Download complete
  c40496787f7c: Download complete
  f90b0e99841d: Download complete
  ac0bf528748f: Download complete
  Status: Downloaded newer image for heroku/cedar:14
   ---> 66766a8c5395
  ... docker 이미지 빌드 생략 ...
  Successfully built d3e547511a82

  starting container...
  web process will be available at http://localhost:3000/
  Node app is running on port 3000

docker:start 내부 구현

start.js 가 핵심입니다. Procfile 을 읽어서 여기에 적혀 있는 command 가 Docker 가 실행될 때 command 로 전달됩니다. docker.ensureStartImageensureStartImage()buildImage 를 호출합니다. 빌드 명령은 build --force-rm --file="${dockerfile}" --tag="${id}" "${dir}" 입니다. 실행은 runImage() 에서 run -w /app/src -p 3000:3000 --rm -it ${mountComponent} ${envArgComponent} ${imageId} sh -c "${command}" || true 형태로 수행합니다.

컨테이너에 명령 실행

$ heroku docker:exec npm install --save --no-bin-links cool-ascii-faces
$ heroku docker:exec ls node_modules

docker:exec 내부 구현

docker exec 를 한 번 감싼 형태입니다. 구현은 exec.jsrunImage() 에서 볼 수 있습니다.

docker:release - 배포

아래의 명령으로 배포 가능합니다.

$ heroku docker:release

이것은 heroku 가 기존에 사용하던 Buildpack 기반의 빌드시스템을 사용하지 않습니다. 대신, Docker 이미지에 앱의 코드와 의존성을 포함하고, 로컬의 Docker 이미지를 직접 배포합니다.

docker:release 내부 구현

Heroku 로 배포할 때 컨테이너를 올려서 /app 디렉터리를 압축합니다. 이 압축파일에는 language 의 runtime 과 앱의 소스코드를 포함합니다.

구체적으로는 release.js 에서 아래의 흐름을 따릅니다.

docker run -d ${imageId} tar cfvz /tmp/slug.tgz -C / --exclude=.git --exclude=.heroku ./app
docker wait ${containerId}
docker cp ${containerId}:/tmp/slug.tgz ${slugPath}
docker rm -f ${containerId}
slug.tgz

만들어진 slug 압축파일을 HTTP PUT 을 통해 업로드하도록 구현되어 있습니다 (github, heroku).