Haskell on Heroku

Heroku で Yesod が動かせると聞いたので、やや今さらな感もありますが、最近注目を集めている PaaS 型のクラウドプラットフォーム Heroku で Yesod を動かすまでのメモ。


Heroku では、OS や必要なライブラリをセットにしたものを Stack と呼びます。これまでは Ruby 用の 2 種類のスタックが用意されていたものに加えて、Node.js や Clojure, Java, Python, Scala に対応した Celadon Cedar Stack が新たに作られました。現在 Cedar stack はパブリックベータという位置付けですが、上記以外にも、Yesod などの Haskell アプリケーションもデプロイすることができます。

ここでは RubyGemsHaskell Platform が既にインストールされており、Yesod アプリケーションが作成できるが、今まで Heroku を使ったことがないユーザ向けに説明します(つまり私のことです)。

基本的に、

あたりを参照すれば良いはずです。

基本的に Cedar stack において Haskell を動かす場合、コンパイルした実行ファイルを Node.js アプリケーションとしてデプロイすることになります。Node.js アプリケーションとして認識させるために package.conf を適切な内容で作成し、Web アプリケーションの起動方法を Procfile に指定すれば大体の作業は終わりです。

Heroku アプリの動作方法を記述する Procfile を適切に設定すれば、Heroku Cedar stack の Ubuntu 10.04 LTS (64bit) で実行可能なプログラムなら基本的にはどんなプログラム言語でも使えるはずです。

Heroku gem

はじめに heroku gem をインストールします。

	$ gem install heroku # sudo や --user-install --no-ri --no-rdoc あたりはお好みで

上でインストールされたコマンドラインツール heroku を使ってログインし、公開鍵を登録します。

	$ heroku login

Yesod

おなじみの、

	$ yesod init

を実行して、適当に質問に答えます。ここでは Project name を yesod-test とした場合について説明します。

	$ cd yesod-test

Yesod 0.9 以降の yesod init を使った場合は deploy/Procfile が作成されているので、deploy/Procfile の説明にある通り、

	$ mv deploy/Procfile Procfile

としてプロジェクトのトップに移動します。次に、Heroku に Node.js アプリケーションとして認識させるため package.json を作成します。*1

	$ echo '{ "name": "yesod-test", "version": "0.0.1", "dependencies": {} }' >> package.json

Heroku へのデプロイは git を使って行なうので、生成されたファイルを git の管理下に置きます。

	$ git init
	$ git add .
	$ git commit -m "initial commit"

Cedar stack にアプリを作成します。当然ながら --stack cedar を忘れると動きません。

	$ heroku create --stack cedar
	Creating strong-mist-1328... done, stack is cedar
	http://strong-mist-1328.herokuapp.com/ | git@heroku.com:strong-mist-1328.git
	Git remote heroku added

メッセージにもある通り、自動的に Git remote に heroku が登録されます。heroku へのデプロイはこの git remote を利用して行うことになります。

コンパイル

コンパイルする際は 64 ビット環境向けの ghc を使う必要があります。また Heroku Cedar stack には Haskell の共有ライブラリは含まれていないため、ghc に -static オプションを渡して、スタティックリンクを行なう必要があります。

production フラグが指定されている場合にはスタティックリンクを行なうように、yesod-test.cabal 中の ghc-options を書き換えます。*2

	... (略)
	executable         yesod-test
	    if flag(devel)
	        Buildable: False
	
	    if flag(production)
	        cpp-options:   -DPRODUCTION
	        ghc-options:   -Wall -threaded -O2 -static -- ← ココ
	    else
	        ghc-options:   -Wall -threaded -O0
	... (略)

production フラグを付けてコンパイルします

	$ cabal configure -fproduction
	$ cabal build

これで dist/build/yesod-test/yesod-test にアプリケーションが作成されたはずです。*3

作成した実行ファイルを コミットする際はデプロイ用のブランチを切ることをお勧めします。

	$ git checkout -b deploy
	$ git add dist/build/yesod-test/yesod-test
	$ git commit -m "deploy"

最後に、

	$ git push heroku deploy:master

として、デプロイします。

	$ heroku open

とすれば、ブラウザが開かれます。

上手くいきましたか?動いた人はおめでとうございます。そうでない人はもう少し作業が必要です。

heroku trouble shooting

503 Applicatoin Error が出ましたか?その場合は

	$ heroku logs

としてログを取得します。Yesod アプリケーション自体はきちんと作られていて、自分の環境では実行できていても、

	2011-11-15T17:38:20+00:00 app[web.1]: ./dist/build/yesod-test/yesod-test:
          error while loading shared libraries: libyaml-0.so.2: cannot open shared object file: No such file or directory
	2011-11-15T17:38:20+00:00 heroku[web.1]: Process exited

のようなエラーが出ている可能性があります。はい、見ての通り共有ライブラリ libyaml-0.so.2 が見つかっていません。

heroku run を使うと、heroku の仮想環境下でコマンドを実行することができます。試しに

	$ heroku run ldd dist/build/yesod-test/yesod-test

としてみてください。私の場合は

	Running ldd ./dist/build/yesod-test/yesod-test attached to terminal... up, run.37
		linux-vdso.so.1 =>  (0x00007fffb7dff000)
		libz.so.1 => /lib/libz.so.1 (0x00007fe9963ba000)
		libyaml-0.so.2 => not found
		librt.so.1 => /lib/librt.so.1 (0x00007fe9961b1000)
		libutil.so.1 => /lib/libutil.so.1 (0x00007fe995fae000)
		libdl.so.2 => /lib/libdl.so.2 (0x00007fe995daa000)
		libgmp.so.10 => not found
		libffi.so.5 => not found
		libm.so.6 => /lib/libm.so.6 (0x00007fe995b26000)
		libpthread.so.0 => /lib/libpthread.so.0 (0x00007fe995908000)
		libc.so.6 => /lib/libc.so.6 (0x00007fe995585000)
		/lib64/ld-linux-x86-64.so.2 (0x00007fe9965d9000)

のように出力されました。

ghc の -static オプションは、可能な限りHaskell パッケージの共有ライブラリを利用せず静的にリンクを用いるという意味で、実行ファイル自体は動的リンクになっています。したがって、それ以外に依存しているライブラリがあっても静的なリンクは行なわれません。そのため ldd の結果を見れば分かる通り、Heroku 環境には存在しない libyaml, libffi, libgmp が発見できていないことがわかります。

static linked なバイナリを作る方法

解決方法のひとつは、static linked なバイナリを作成することです。yesod-test.cabal のさきほど書き換えた部分を更に

	ghc-options:   -Wall -threaded -O2 -static -optl-pthread -optl-static

のように変更し、ghc に対して更に -optl-pthread -optl-static フラグを指定します。これはリンカに対して -pthread -static を指定するという意味になります。しかし、この方法は上手くいかない場合が多いようです。
ログに

	yesod-test: /dev/urandom: openFile: invalid argument (Invalid argument)

のようなメッセージが出ている場合は、libc のバージョンが異なる可能性があります。この場合は、static linked なバイナリを作成する方法では上手くいきません。

共有ライブラリを含めてデプロイする方法

解決方法のふたつめは、必要な共有ライブラリも含めてデプロイする方法です。実行時に環境変数 LD_LIBRARY_PATH を指定するか、コンパイル時に -optl-Wl,-rpath,'$ORIGIN' 等として RPATH を埋め込む方法の二種類がありますが、今回は LD_LIBRARY_PATH を用いる方法を解説します。

Heroku 環境に含まれていない libyaml, libffi, libgmp を dist/build/yesod-test 以下にコピーし、git の管理下に置きます。

LD_LIBRARY_PATH を指定するため、シェルスクリプト run を作成し、実行権限を付けます。run の中身は

	#!/bin/sh
	LD_LIBRARY_PATH=$PWD/dist/build/yesod-test ./dist/build/yesod-test/yesod-test -p $PORT -e production

のようにします*4

最後に Procfile を

	web: ./run

のように書きかえてデプロイすれば動くはずです。ダメだったら……

ぐだぐだまとめのようななにか

結論: Heroku で Haskell 使いたければ Ubuntu 10.04 LTS の仮想環境作るなり chroot なり使ってコンパイルしよう。Ubuntu 10.04 でコンパイルした場合は static linked なバイナリで問題なく動くはず。もちろん、共有ライブラリを含めてデプロイする場合でも、libgmp(Ubuntu 10.04 では libgmp3 という名前のはず) は Heroku 環境にも存在しているので多少はマシでしょう。ただ、この場合でも libyaml は無いはずなのでやっぱり似たような手順が必要になるかと思います。とはいえ、static linked なバイナリだとサイズが大きくなりすぎるので、やはり公式にサポートが欲しいところ。もっと良い方法があれば教えてください。

Heroku が公式にサポートされていないフレームワークでも package.json を置いて Node.js アプリと認識させ、Procfile に起動方法を書いてしまえば動かせるので、Haskell だろうと C++ だろうと(いるのか?) Heroku で動かせるんじゃないですかねー?

*1:Heroku はアプリケーションの種類が認識できない場合にはデプロイを拒否します。以前は空の Gemfile を置いておけば良かったのですが、現在ではこの方法は使えないようです。

*2:ghc はデフォルトでは -static フラグが有効になっているので、実はこの手順は必要無いのですが、後の説明のため残しておきました

*3:本来であれば cabal install した物を使うべきですが、ここでは割愛します。

*4:今回はシェルスプリプトを作成しましたが、環境変数は heroku config から指定することもできます