Herokuの'docker:release'の動き

Introducing ‘heroku docker:release’: Build & Deploy Heroku Apps with Docker

HerokuがDockerを使ったツールを提供し始めた.一通り触ってコードもちょっと読んでみたので現時点でできること,内部の動きについてまとめる.

TL;DR

自分の好きなDockerイメージをHeroku上で動かせるようになるわけではない.

何ができるのか

まず何ができるようになったのかについて簡単に書く.プラグインをインストールするとDockerコマンドが使えるようになる.

$ heroku plugins:install heroku-docker

カレントディレクトリの言語/フレームワークに応じた専用のDockerfileを生成する(これはなんでも好きに書けるDockerfileではないことに注意).

$ heroku docker:init
Wrote Dockerfile (ruby)

上記で作成したDockerfileをもとにDockerコンテナを起動してコンテナ内でアプリケーションを起動する.

$ heroku docker:start
...
web process will be available at http://192.168.59.103:3000/

起動したコンテナ内でOne-offコマンドを実行する.

$ heroku docker:exec bundle exec rake db:migrate

開発が終わったら上記のDockerイメージからSlug(アプリケーションのソースとその依存関係を全て含めた.tgz)を作成してそれをそのままHeroku上にデプロイすることができる.デプロイされるとそれは通常通りにDynoになりアクセスできるようになる.

$ heroku docker:release
$ heroku open

以下の制約を満たせば生成されたDockerfileを自分なりに編集することもできる.

  • heroku:cedarイメージをベースにする
  • /appディレクトリ以下に変更が入るようにする

詳しくは以下で説明する.

内部の仕組み

上記のワークフローで実際に何が行われているのかをコード/コマンドレベルで追ってみる.プラグインのソースはhttps://github.com/heroku/heroku-dockerにある.

init

まずdocker:initによるDockerfileの生成.これはカレントディレクトリのソースコードからプラットフォームを判別し,プラットフォーム専用のテンプレートから生成する.テンプレートはheroku/heroku-docker/platforms以下にある.現在はRuby,Node,Scalaがサポートされている.

プラットフォーム判別はBuildpackと同じ仕組み.例えばRubyの場合は以下のようにGemfileの有無により判別する.

detect: function(dir) {
      if (exists.sync(path.resolve(dir, 'Gemfile'))) return true;
      if (exists.sync(path.resolve(dir, 'Gemfile.lock'))) return true;
}

生成されるDockerfileを見ると大体以下のことをしている.

  • ベースイメージとしてheroku/cedar:14を利用する
  • /app以下に各種言語のRuntimeや依存パッケージをインストールする
  • ONBUILDでカレントディレクトリのソースを/appに取り込む

CedarというのはHeroku上でDynoを動かすプラットフォーム.ベースはUbunutでcedar-14.shスクリプトで作られる(つまり,DockerでローカルにHerokuプラットフォームを再現してその上にアプリケーションを載せている感じになる).

自分でスクラッチでDockerfileを書くこともできる.以下で最小限のDockerfileを生成できる.

$ heroku docker:init --template minimal

start

次にdocker:startコマンドによるアプリケーションコンテナの起動.このコマンドでは以下の3つのことを行う.

  • ベースイメージのビルド
  • アプリケーションを含めたイメージのビルド(ONBUILD
  • 作成したイメージをもとにコンテナの起動

まずベースイメージのビルドは以下のコマンドを叩いている.

$ docker build --force-rm --file="${dockerfile}" --tag="${id}" "${dir}"

タグ名はheroku-docker-${hash}でHash値はDockerfileの内容から生成される(のでDockerfileを更新していなければビルドは走らない).

次にアプリケーションを含めたイメージのビルド.これは以下のように新しく一時的なDockerfileを生成してビルドする(execImageIdはタグ名).

var contents = `FROM ${execImageId}`;
var imageId = `${execImageId}-start`;
var filename = `.Dockerfile-${uuid.v1()}`;

上で見たように生成されたDockerfileにおいてアプリケーションのコードをイメージに含める部分はONBUILDが使われていた.このステップはONBUILDを実行するために行われる.この仕組みにより,同じDockerfile+同じ言語のアプリケーションである場合にベースイメージの再度ビルドが不要になる(つまりアプリケーションコードの追加と依存関係の解決のみが走る).このステップがgit pushしたことと同じになる.

最後にコンテナの起動は以下のコマンドが実行される.

docker run -w /app/src -p 3000:3000 --rm -it ${mountComponent} ${envArgComponent} ${imageId} sh -c "${command}"

"${command}"にはProcfileの内容が使われる.DBのURLなどは.envファイルに環境変数を書いておけばよい.このファイルはここで読み込まれて-e KEY=VALUME形式で${envArgComponent}に展開される.

以上の仕組みでローカル環境でアプリケーションが起動する.

release

最後にdocker:releaseコマンドでSlugがHerokuにデプロイされる仕組み.このコマンドでは以下の3つのことを行う.

  • Slug(slug.tgz)の作成
  • 起動コマンド(process_types)の設定
  • Slugのアップロード

Slugの作成はdocker:startコマンドでDockerコンテナを起動し,そのコンテナに対して以下のコマンドを実行する.

$ 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}

/app以下をtgzで固めてcpコマンドでそれを取り出している.なのでDockerfileに独自の変更を加えるときは注意が必要で/app以下に依存をちゃんと含めるように書く必要がある.

例えばGraphicsMagickを依存に含めたいときは以下のようにDockerfileを書いて/app以下に変更が加わるように意識しなければならない.

RUN curl -s http://78.108.103.11/MIRROR/ftp/GraphicsMagick/1.3/GraphicsMagick-1.3.21.tar.gz | tar xvz -C /tmp
WORKDIR /tmp/GraphicsMagick-1.3.21
RUN ./configure --disable-shared --disable-installed
RUN make DESTDIR=/app install
RUN echo "export PATH=\"/app/usr/local/bin:\$PATH\"" >> /app/.profile.d/nodejs.sh
ENV PATH /app/usr/local/bin:$PATH

(Go言語で書かれた静的リンクされたバイナリを使うのは楽そう)

起動コマンド(process_types)の設定とSlugのアップロードはPlatform APIを叩いているだけ,詳しくは“Creating Slugs from Scratch”を参考.

まとめ

Dockerを使うことでHerokuアプリの開発をやりやすくしたという印象.Buildpackとの連携も進むのではないか(Buildpack+Dockerだとbuildingかなあ).ローカル開発環境にDocker(boot2docker)はあるっしょという前提でツールを提供する流れだ.DockerコマンドをラップしたりDockerfileを自動生成したりね.

参考