Haskell製サービスを本番投入

数週間前、私たちはGiant Robotブログで新しいコメント機能をひそかにリリースしました。パラグラフやコードブロック上でホバーすると、右側に小さなアイコンが表示され、その記事の該当するセクションにコメントできる機能です。

この機能を公開して、これまでずっと言いたかったことがようやく言えます。ついにHaskellのサービスを一般出荷しました! ここでは、リリースの概要および仕組みを説明し、私たちが遭遇したさまざまな問題に対するソリューションをいくつか紹介します。

アーキテクチャ

コメントを処理するのは、記事ページにJavaScriptのスニペットをインクルードすることによってブログに”インストール”されたサービスです。私たちがGitHubに登録したCarnivalという別のサービスは、コメントの追加、検索、および編集を担うRESTfulなAPIと、ユーザエクスペリエンスに対応するためのJavaScriptのフロントエンドで構成されます。バックエンドの部分は、YesodWebフレームワークを使ってHaskellで記述されています。

なぜHaskellなのか?

この質問に対する答えは、聞く相手によって変わります。言語としてのHaskellを愛好し、何だかんだと理由をつけてその言語を使いたがる人たちは、Haskellの持つ安全性や抽象化の質、開発の喜び、もしくはその他の多くの長所に魅力を感じているはずです。一方、最近Haskellを知って、盛んに開発が進められているものに採用したいと考えたり、それまで慣れ親しんだ言語とは全く異なるこの言語をもっと知りたいと思ったりする人たちもいるでしょう。

最終的に、私たちは、Haskellを使ってクライアントのプロジェクト要件に見合うものを構築できるか確かめたいと思っています。Haskellが適合しそうなクライアントが現れたら、私たちはコードの記述だけでなく、本番環境での運用に必要な全てのことをHaskellで担う自信を持つべきです。

開発プロセス

このサービスの開発を通して、正しいコードを短時間で書くという点で型安全性のメリットについての話が真実だと証明されました。型駆動型開発(TyDD)と受け入れテストの組み合わせを使うことで、ほとんどのAPIを1日かそこらで記述し、以降のイテレーションやリファクタリングがスムーズに進んだのです。インタプリタ型言語を使い慣れたプログラマにとっては、長時間のコンパイルはイライラするものであり、cabalの問題について小さな論争が起きたことも確かにありました。そんなわけで、Haskellにおける依存関係の管理で私が経験してきたことから見ると、sandboxとfreezingの導入は、決定的な改善になっています。

APIのみのサービスを記述するということは、JSONを多用して作業するということです。aesonライブラリを通してこの作業をすることは簡潔で、作業に必要とされた非常に限定的なバリデーションロジックを利用し、安全な(デ)シリアライゼーションができました。一般的にはRailsのAPIサービスを使って記述するバリデーションの多くは、型システムで扱うことが可能です。

ライブラリはmarkdowngravatar、そしてherokuのサポートといった私たちが必要とする大部分のパッケージのためにあります。しかし重要な例外が1つあります。それはプロバイダとして自分たちのUpcaseを使うのに必要な、OAuth 2.0による認証です。一般的にYesodには認証のための多大なるサポートがあり、OAuth 1.0にはプラグインがあります。しかしOAuth 2.0にあったのは時代後れの骨子でしかなかったのです。幸運なことに、骨子にすぎなかったものを正規のパッケージに移動させ、動作を確認し、自分たちで公開することはそれほど大変ではありませんでした。Yesodはこの機能をリリースしませんでしたが、認証ロジックを扱ったモジュール式の方法により、私たちは別の独立したパッケージを加えることができました。

デプロイメント

この試みには、できるだけ通常のプロセスを使ってHaskellで開発する、つまりHerokuにデプロイする、という目的があります。しかしHaskellアプリケーションのクリーンコンパイル(特にYesodやPandocのようなライブラリの使用時)は時間がかかるので、15分以内にビルドするという制限が問題になりました。

先に言っておくと、この問題はバイナリのデプロイ方法によっては回避することができます。VM(Herokuの構造と同じにします)上で、ローカルでコンパイルし、バイナリをHerokuのインスタンスにコピーするという方法もありました。しかし、これは通常のプロセスではありません。開発者であるならばgit push heroku masterで動作させるべきでしょう。

理論上は、これで動きます。ビルドの大部分はキャッシュされるので、15分を超えそうなものは最初のビルドだけです。これを緩和するために、最も人気があるHaskellのbuildpackAnvilというサービスをサポートし、最初のビルドを時間制限なしの環境で実行させます。しかし、何度も実行を試みてはよく分からないエラーメッセージが出た結果、私たちはAnvilベースのデプロイメントを諦めざるを得ませんでした。自分たちで何とかするしかありません。

結局、PX dynoをアップグレードしても15分以内に終えることはどうしてもできませんでした。私たちの代替Herokuはアプリケーションの時間制限を30分に増やすことができたので、いまのところはこれでビルド可能です。しかし、私はこれを一般的だとはみなしません。Pandoc(とその強調表示エンジン)への依存が、ほとんどのYesodアプリケーションよりもコンパイルに長い時間がかかる原因ではないかと考えています。なので、標準のbuildpackを使うことをお薦めします。あまりに時間がかかって壊してしまいたい衝動に駆られる前に、15分以内で終わらせることができるようにと願っています。

一度ステージングに成功すると、今度は別の問題に気付きました。ユーザがランダムにログアウトしていたのです。その原因はYesodのデフォルトセッションのバックエンドが、Cookieベースの鍵をファイルに保存することにありました。これはHerokuのデプロイメントにおける若干のデメリットです。第一に、ファイルシステムは一過性のものなので、dynoが再起動しようとすると全てのセッションが無効になります。次に、実行中のdynoが2つになることです。1つのdynoにログインしても、次に続くリクエストは2つ目のdynoにルーティングするのでログアウトさせられます。そのため、私たちは環境変数から鍵を読み込む代替のバックエンドを定義しました。この環境変数は各インスタンスに同じ値を設定できます。

もっとHaskellを

この試みは確かに成功したと考えています。デプロイメントに関するいくつかの問題が解決できました。これで次のHaskellプロジェクト(すでに準備中です)が、より円滑に進むでしょう。Haskellは私たちが遭遇したような問題の解決にふさわしい言語だと分かりました。Yesodと、ライブラリのすばらしいエコシステムのおかげです。