Herokuの'docker:release'の動き
Introducing ‘heroku docker:release’: Build & Deploy Heroku Apps with Docker
HerokuがDockerを使ったツールを提供し始めた.一通り触ってコードもちょっと読んでみたので現時点でできること,内部の動きについてまとめる.
TL;DR
- Herokuのデプロイ環境とおなじものをDockerでつくれる
- Buildpackを使わないで
Dockerfile
からSlugを作れる
自分の好きな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
を自動生成したりね.