HabitatでSinatraアプリのパッケージを作る

6/14にChef社の新しいプロダクト、Habitatが発表されたのでひとまず触ってみました。bignewsとかいってカウントダウンまでしてたのにカウントダウン終了前にTwitterやBlogに書いてて笑った。
チュートリアルを一通りやって、さて簡単なRubyアプリを動かそうということで、SinatraアプリをHabitatで動かした。

Dockerのイメージにエクスポートしてそれを動かす、までしかしていないのでサービスディスカバリとかどうなってるのかはよくわからない。

コツ的なもの

plan.shやhooksの書き方

https://app.habitat.sh/#/pkgs/core にあるパッケージを参考にするのがよい。ブラウザからはplan.shとconfigを見ることができる。
initやrunといったhooksは、pkg_build_depsに指定してstudio上でbuildすると、「/hab/pkgs/core/ruby-rails-sample/0.0.1/20160614073830/」のような場所にダウンロードしてくれるので、hooksディレクトリを覗けばよい。

hooksのデバッグ

最初、全然起動しなくて、しかもエラーが出力されてなかったのでかなり大変だった。
initやrunに「exec 2>&1」を書いておくと標準エラーを標準出力に出すのでエラーログを見ることができる。

作成記録

備忘録。大体チュートリアルと同じ。概念とか間違えてるかも。

docker環境の用意

dockerを使うので用意する。チュートリアルと同じ。環境変数や変数はcore/ruby-rails-sampleを参考にした。

[text]
docker-machine create –driver virtualbox habitat
eval $(docker-machine env habitat)
[/text]

plan.shの作成

Habitatではアプリや付随するライブラリをまとめたものをパッケージと呼ぶらしい。
で、パッケージを作るためにplan.shというファイルを作成する。
これは、どういうパッケージをインストールするか、ビルド時にどういう処理をするのかを定義しているファイル。
シェルスクリプトなので割ととっつきやすい?

今回作成したplan.shは以下。いろいろ試行錯誤したので不要な設定等があるかもしれない。

[text]
$ mkdir habitat_sinatra
$ cd habitat_sinatra
$ vim plan.sh
$ cat plan.sh
pkg_oriigin=origin
pkg_name=habitat_sinatra_sample
pkg_version=0.0.1
pkg_maintainer="Takaishi Ryo <ryo.takaishi.0@gmail.com>"
pkg_license=()
pkg_source=https://github.com/takaishi/${pkg_name}/archive/${pkg_version}.tar.gz
pkg_shasum=2977dff904580d64f72a833e9ce228fdff19ea6956aba36a5d8000ddd9ffe4c7
pkg_filename==${pkg_version}.tar.gz
pkg_deps=(core/ruby core/bundler core/bash)
pkg_build_deps=(core/ruby core/bundler core/bash)
pkg_lib_dirs=(lib)
pkg_include_dirs=(include)
pkg_expose=(8080)
pkg_bin_dirs=(bin vendor/bundle/bin)

do_build() {
local _bundler_dir=$(pkg_path_for bundler)

export GEM_HOME=${pkg_path}/vendor/bundle
export GEM_PATH=${_bundler_dir}:${GEM_HOME}
gem install bundler -v 1.11.2 –no-ri –no-rdoc
bundle install –path vendor/bundle
}

do_install() {
cp Gemfile ${pkg_prefix}
cp Gemfile.lock ${pkg_prefix}
cp app.rb ${pkg_prefix}
cp -r .bundle ${pkg_prefix}
cp -r vendor ${pkg_prefix}
}
[/text]

pkg\_\*\*の変数が基本的な設定で、do\_\*\*の関数はコールバック。コールバックでイメージの作成時にどういう処理を行うか定義します。
変数とコールバックは結構いろいろ種類があるみたい

このplan.shでやっているのは、主に

  1. pkg_sourceからアプリのリリースファイルをダウンロード&展開
  2. アプリを動かすのに必要なgemをbundleコマンドでダウンロード
  3. アプリを動かすのに必要なファイルを自分のパッケージにコピー

の3つ。

まず、ダウンロード。このplan.shには書かれていないけど、デフォルトで、do_downloadコールバックでpkg_sourceで指定したファイルをダウンロードするようになっている。
ダウンロードしたファイルのチェックや展開も、それぞれdo_verifyやdo_unpackでやる。

次にgemのダウンロード。do_buildでやっている通り。do_buildはデフォルトではmakeしているけど、このplan.shではそのコールバックを上書きしてgemをダウンロードしてる。

最後にファイルのコピー。デフォルトだとmake installを実行するけどここでは必要なファイルをコピーする。

hookの作成

plan.shはできたけど、これだけじゃアプリが動かない。動かすために、hookを作成する。

initの作成

initは起動時に実行される処理を書くみたい。
以下のように作成した。

[text]
$ mkdir hooks
$ vim hooks/init
$ cat hooks/init
#!/bin/sh
#
export GEM_HOME={{pkg.path}}
export GEM_PATH="$(hab pkg path core/ruby)/lib/ruby/gems/2.3.0:$(hab pkg path core/bundler):$GEM_HOME"

ln -sf {{pkg.path}}/Gemfile {{pkg.svc_path}}
ln -sf {{pkg.path}}/Gemfile.lock {{pkg.svc_path}}
ln -sf {{pkg.path}}/vendor {{pkg.svc_path}}
ln -sf {{pkg.path}}/app.rb {{pkg.svc_path}}

cd {{pkg.svc_path}}
exec 2>1&
exec bundle install –path vendor/bundle
[/text]

リンクを貼っているが、これはパッケージのディレクトリに置いてあるGemfileやapp.rbをサービス用のディレクトリに配置している。
plan.shで一度コピーしてその後リンクを貼るというには2度手間な気もするけどどうなんだろう…
その後、bundle install。do_installでコピーしたはずの.bundleがどうもコピーされていないので再度ここで実行。

runの作成

最後に、Sinatraアプリを実行するrun。

[text]
$ vim hooks/run
$ cat hooks/run
#!/bin/sh
#
export GEM_HOME={{pkg.path}}
export GEM_PATH="$(hab pkg path core/ruby)/lib/ruby/gems/2.3.0:$(hab pkg path core/bundler):$GEM_HOME"
cd {{pkg.svc_path}}
exec 2>&1
exec bundle exec ruby app.rb -o 0.0.0.0 -p 8080
[/text]

サービス用ディレクトリに移動してapp.rbを実行する。

hab studioでパッケージを作る

開発用のサンドボックスとしてstudioというものがあって、そこでパッケージのビルドやDockerイメージへのエクスポートができる。

[text]
$ hab studio enter
[1][default:/src:0]# ls
README.md hooks plan.sh results
[2][default:/src:0]# ls /
bin dev etc hab home lib lib64 mnt opt proc root run sbin src sys tmp usr var
[/text]

ビルドとエクスポートは以下のようにする。まぁチュートリアルと同じ。

[text]
[3][default:/src:0]# build
[4][default:/src:0]# hab pkg export docker r_takaishi/habitat_sinatra_sample
[/text]

実行

いよいよ実行する。とはいってもチュートリアルと大体同じである。

[text]
$ docker run -it -p 8080:8080 r_takaishi/habitat_sinatra_sample
hab-sup(MN): Starting r_takaishi/habitat_sinatra_sample
hab-sup(GS): Supervisor 172.17.0.4: 6c921b33-1872-4c14-8ab2-4ce6821fcd67
hab-sup(GS): Census habitat_sinatra_sample.default: ebb498f9-7c52-4ec1-810f-5245a3d2aa5b
hab-sup(GS): Starting inbound gossip listener
hab-sup(GS): Starting outbound gossip distributor
hab-sup(GS): Starting gossip failure detector
hab-sup(CN): Starting census health adjuster
init(PH): Don’t run Bundler as root. Bundler can ask for sudo if it is needed, and
init(PH): installing your bundle as root will break this application for all non-root
init(PH): users on this machine.
init(PH): Using rack 1.6.4
init(PH): Using tilt 2.0.5
init(PH): Using bundler 1.11.2
init(PH): Using rack-protection 1.5.3
init(PH): Using sinatra 1.4.7
init(PH): Bundle complete! 1 Gemfile dependency, 5 gems now installed.
init(PH): Bundled gems are installed into ./vendor/bundle.
habitat_sinatra_sample(SV): Starting
habitat_sinatra_sample(O): [2016-06-15 12:12:49] INFO WEBrick 1.3.1
habitat_sinatra_sample(O): [2016-06-15 12:12:49] INFO ruby 2.3.0 (2015-12-25) [x86_64-linux]
habitat_sinatra_sample(O): == Sinatra (v1.4.7) has taken the stage on 8080 for development with backup from WEBrick
habitat_sinatra_sample(O): [2016-06-15 12:12:49] INFO WEBrick::HTTPServer#start: pid=72 port=8080
[/text]

起動したのでアクセスしてみる。

[text]
$ curl http://$(docker-machine ip habitat):8080/
Hello, habitat!%
[/text]

やったぜ。