Dockerを使ってHaskellアプリをHerokuにデプロイする

コンパイル時間に制限されないデプロイ方法

Posted by Kadzuya Okamoto on April 30, 2017Tags: Localize, Heroku

これまで、HaskellのコードをHerokuで実行しようとすると、コンパイルがHerokuの制約時間内に終わらず、面倒なハックが必要でつらい状態でした。 でも、HerokuDockerをサポートするようになった今なら、Haskell製のウェブアプリケーションをHeroku上で公開するのはずっと簡単です。 この記事では、Servant(HaskellWebフレームワークの1)で作ったアプリケーションを、Dockerの力を借りてHerokuにデプロイする方法について、具体的なプログラムを使って順を追って説明していきます。

Link to
here
本記事について

この記事は、Releasing a Haskell Web App on Heroku with DockerとしてHaskell-jpオフィシャルスポンサーである株式会社ARoW公式ブログに公開されている英語の記事を、許可を得て日本版にローカライズしたものです1

Link to
here
はじめに

今回、実際にHerokuにデプロイして試せるように、サンプルアプリを用意しました。 この記事の最初の章では、このサンプルアプリをローカル環境で動かす方法について述べます。 「今度はDockerをつかってアプリを動かしてみよう!」では、同じくローカル環境において、Dockerを使って動かす方法について触れます。 Herokuで動かす」で、ついにHerokuにこのサンプルアプリをHerokuにデプロイする方法についてお伝えします。

もし、ローカル環境で動かしたりするのが面倒で、「いきなりHerokuにデプロイしたい!」という方は、 Herokuで動かす」から読んでいただいても問題ないように構成しているつもりです。

Link to
here
Docker使わずに サンプルアプリを実行してみる

今回用意したサンプルアプリは、以下の通りAPI2つだけ提供する、とても単純なものです。

  • かんたんなコメントのようなものを送信するためのAPI
  • これまでに送信された全コメントを表示するためのAPI

このサンプルアプリでは、コメントを保存するのにPostgreSQLを利用しています。

では、まずはDockerHerokuをつかわないで、実際にローカルな環境でこのアプリをビルドして実行する手順を追っていきましょう。

Link to
here
ローカル環境でサンプルアプリをビルドする

まず最初に、このサンプルアプリを公開しているgithubレポジトリをcloneして、アプリをビルドしてみましょう。

もしかしたら、PostgreSQLのライブラリが入っていなくて、ビルドに失敗してしまうかもしれません。

Arch Linuxの場合は、以下のコマンドで必要なライブラリをインストールできます。

Ubuntuユーザの方は、以下のコマンドで大丈夫です。

上記以外のプラットフォームでは別のコマンドを使うことになると思うので、いい感じにググってください。

では、PostgreSQLの必要なライブラリを入れたところで、stack buildをもう一度試してみましょう。今度はうまくいきましたよね?

うまくビルドできたら、アプリの実行をしてみます。

わーお!なにかエラーが出ちゃいますね…

servant-on-heroku-api: libpq: failed (could not connect to server: Connection refused
        Is the server running on host "localhost" (::1) and accepting
        TCP/IP connections on port 5432?
        could not connect to server: Connection refused
        Is the server running on host "localhost" (127.0.0.1) and accepting
        TCP/IP connections on port 5432?
)

サンプルアプリがPostgreSQLに接続しようとして失敗しているようです。 このアプリは、コメントをPostgreSQLに保存しているので、PostgreSQLがローカルな環境で動いていないと、うまく動きません。

Link to
here
PostgreSQLのセットアップ

お使いの環境によって、PostgreSQLのインストール方法はまちまちなので、 そのプラットフォームが提供しているドキュメントにしたがって、PostgreSQLのインストールを行ってください。

たとえば、Arch Linuxの場合はこのドキュメントです。 Ubuntuならここにドキュメントがあります。

さて、PostgreSQLをインストールして、動いているのが確認できたら、もう一度アプリを起動してみましょう。

わーお… またもやエラーです…

servant-on-heroku-api: libpq: failed (FATAL:  role "mydbuser" does not exist
)

どうやら、このサンプルアプリ用に、PostgreSQLのユーザとデータベースを用意しないといけないようですね。 実際にサンプルアプリのソースコード(src/Lib.hs)を見てみると、DATABASE_URLという環境変数の値を見てPostgreSQLサーバに接続しているのがわかります。

DATABASE_URL環境変数が指定されていない場合は、以下のデフォルト値が使われます。

postgres://mydbuser:[email protected]:5432/mydb

mydbuserというユーザ名で、mydbpassというパスワードを使ってmydbという名前のデータベースにアクセスしようとしているということですね。 では、実際にこのユーザとデータベースをPostgreSQLで作成してみましょう。 次のコマンドはArch Linuxでしか動かないかもしれません。 もし動かないようであれば、お使いのプラットフォームが提供するドキュメントを参照してください。

最初に、mydbuserという名前のユーザを、mydbpassというパスワードで作成しましょう。

次にmydbという名前のデータベースを作成します。

mydbusermydbデータベースにアクセスできるようにするのも忘れちゃいけませんね。

ここで、PostgreSQLの再起動をしておいた方が無難でしょう。

これで、実際にmydbデータベースに、mydbuserとしてログインすることができるようになったはずです。

Link to
here
APIを実際にたたいてみる

では、PostgreSQLのセットアップが無事終了したところで、次のコマンドでアプリケーションを立ち上げてみましょう。

無事に立ち上がったら、コメントを送ってみます。 アプリが立ち上がった状態で、別のターミナルなどを開いて次のコマンドを打ってみてください。

よさそうですね!

では、全コメントを取得してみます。

いいですね! DG (Dennis Gosnell / 原著者)さんが「チョベリグ!」と言っています。 以上で、ローカル環境でアプリを動かすことができたので、次はDockerを使ってみましょう!

Link to
here
今度はDockerをつかってアプリを動かしてみよう!

Dockerはコンテナ技術を用いたプログラムで、これを使うと仮想環境下でアプリをビルドしたり実際に動かしたりすることができます。 以降では、読者のみなさまがある程度Dockerについて知っている前提で進めていきますが、たぶんそんなによく知らなくても「まぁそんなもんなんだろう」と思いながら読んでいただければ差し支えないと思います。 実際、日本語ローカライズ版を作ってる僕だって、そんなにDockerに詳しいわけではありません。

Link to
here
Dockerをインストールする

Dockerのインストール方法は環境によってまちまちなので、ご自身の環境に合わせて信用できるドキュメントを参照してください。 Arch Linuxの場合Ubuntuの場合はリンク先を読めばなんとかなると思います。

Dockerのインストールが終わったら、以下のコマンドを実行してDockerがちゃんと動いているか確認してみてください。

Link to
here
Dockerを使ってビルドする

では、実際にDockerを使ってサンプルアプリをビルドし、そのアプリを動かすためのDockerイメージを作成します。

アプリケーションをビルドするには、docker buildコマンドを使います。

このコマンドを実行すると、実行したディレクトリ内に存在するDockerfileという名前のファイルにしたがってアプリをビルドしてくれます。 このDockerfileには、アプリをビルドするための具体的な手順がすべて記述されており、その手続きにしたがって、まったく別の環境でもDockerさえあればアプリを実行できる「イメージ」を作成できます。

ためしに、このサンプルアプリに含まれるDockerfileの中身を見てみましょう。 以下の各処理を実行するようになっています。

  1. apt-getコマンドを使って、依存パッケージをインストール
  2. stackをインストール
  3. stack.yamlを見て、実際に必要なバージョンのGHCstackを使ってインストールする
  4. *.cabalファイルの記述にしたがって、アプリが使っているHaskellパッケージをインストールする
  5. stackを使って実際にアプリをビルドする
  6. rootユーザでアプリを実行したくないので、root権限をもつ別のユーザを作成しておく
  7. 実際にアプリを実行する

前述したdocker buildコマンドを実行してservant-on-herokuという名前のイメージを作成するには1時間近くかかるので2、その間にご飯を食べたり録画しておいたアニメを2本見れます。

Link to
here
DockerをつかってAPIをテストする

docker buildが終わったら、docker imagesでローカル環境に存在する全イメージを一覧表示してみましょう。

さきほど作成したservant-on-herokuのイメージが作成されているのがわかりますね?

では、servant-on-herokuのイメージを走らせてみましょう。次のコマンドを実行すれば、Docker内でこのサンプルアプリが動くはずです。

あぁ… またPostgreSQLの例の問題が出てしまったみたいですね…

servant-on-heroku-api: libpq: failed (could not connect to server: Connection refused
        Is the server running on host "localhost" (::1) and accepting
        TCP/IP connections on port 5432?
could not connect to server: Connection refused
        Is the server running on host "localhost" (127.0.0.1) and accepting
        TCP/IP connections on port 5432?
)

これはどういうことでしょうか。 servant-on-herokuコンテナはDockerコンテナとして動いているため、初期設定では我々のローカル環境が見えず、もちろんローカル環境にセットアップしてlocalhost:5432で動いているPostgreSQLも見えないのです。

では、ちょっとしたワザを使ってこの問題を解決してみましょう。 servant-on-herokuコンテナを動かしている時に、Dockerに我々のローカル環境のネットワークインタフェースを使うように指示することができます。

ほら、こうすれば、確かにDockerコンテナからPostgreSQLにアクセスできているようです。

servant-on-herokuコンテナが動いている状態で別のシェルを立ち上げて、前の章でやったようにcurlコマンドでAPIが動いているか確かめてみましょう。 まずはコメントの投稿です。

今度はコメントの取得をしてみます。

この通り、無事にEK (Edward Kmett / Haskell界のすごい人)さんが「圏論を、圏論をもっとくれぇええええい!」と言っているコメントが追加されました。

ちなみに、Docker内でシェルを開いて、手動でDockerイメージをいじりながらいろいろ確かめてみるには、次のコメントのようにすればOKです。

では、Docker上でアプリがちゃんと動いたことを確認したところで、ようやくHerokuの出番です。

Link to
here
Herokuで動かす

Docker上でビルドと実行ができていさえすれば、Herokuにデプロイするのは難しくありません。 まず最初にHerokuのアカウントを作りましょう。

Link to
here
Herokuアカウントを作成する

Herokuアカウント作成ページでアカウントを作成してください。 もちろん、すでにアカウントを持っているのであればあえて別のアカウントを作りなおす必要はないですよ!

今回はHerokuの無料枠を使ってアプリをデプロイするので、クレジットカードの登録は必要ないです。 こわくないですね!

ここで説明する内容は、ほとんどHeroku公式ドキュメントを参照しているので、なにかわからないところがあったらそちらをチェックしてみてください。

Link to
here
Herokuのコマンドラインプログラムをインストールする

HerokuCLIで操作するためのコマンドを用意してくれているので、これを使って便利にHerokuを使い倒せます。 ちょうど、AWSCLIとか、Digital OceanCLIプログラムと同じような感じです。

Arch Linux使いの方は、下記のコマンドでHerokuCLIプログラムをインストールできます。

このコマンドは、herokuコマンドのバイナリを直接取得してインストールしてくれます。

他の環境の方は、Herokuの公式ドキュメントをご覧ください。

CLIプログラムのインストールができたら、コマンドライン上でログインして、権限を必要とする操作ができる状態にしておきましょう。

このコマンドを実行すると、ユーザ名とパスワードをたずねられるので、事前に作成しておいたアカウントの情報を入力してください。

Link to
here
Heroku上でアプリケーションを登録する

今回のサンプルアプリをHerokuで公開するには、まずHeroku上でアプリケーションの登録をする必要があります。

以下のコマンドを実行すると、servant-on-herokuという名前のアプリケーションをHerokuに登録できます。 必要に応じてservant-on-herokuの部分を別の名前に変更してアプリケーションを登録してください。

以下のコマンドで、いま新規登録されたアプリケーションについての情報を一応取得できます。

Web URLの項目だけ、あとで使うのでどこかにメモしておいてください。 他の項目は、いまは特に気にしなくて大丈夫です。

Link to
here
Heroku Docker Pluginをインストールする

Herokuのコマンドラインプログラムは、プラグインを追加することで、どんどん便利な機能を使えるようにできます。

今回は、Heroku Container Registryというプラグインを使いましょう。

以下のコマンドで、このプラグインがインストールされます。

インストールが終わったら、次のコマンドを実行して、ちゃんと動いているか確認してください。

きっと、このプラグインのバージョンナンバーが表示されたはずです。

実際にプラグインを使うためには、以下のコマンドでHeroku Container Registryにログインする必要があります。

このコマンドによって、Container Registryのログイン情報が、~/.docker/config.jsonというファイルに追加されます。

Link to
here
アプリケーションをHeroku上で動かす

実際にアプリをHeroku上で動かすには、以下のコマンドを使います。

これを実行すると、実行したディレクトリ内にあるDockerfileの設定にしたがってDockerイメージを作成します。 内部では、ローカル環境でDockerイメージを作成するときに使ったのと同じdocker buildを呼んでいます。 前の章で実際にdocker buildを実行した方は、その際に作成したイメージがそのままDocker Container Registryに送られるので安心してください。また1時間も待つなんてイヤですよね。

では、heroku apps:infoをもう一度実行して確認してみましょう。

あれ? なにかおかしいですね… Dynos:のところになにも書いてありません。 dynoというのはHerokuが独自に使っている用語で、ウェブアプリを実行する1台のサーバのことを意味します。ここになにも書かれていないということは、アプリを実行しているサーバがいないということになります。

これをどうにかするためには、heroku ps:scaleを使います。

これで、“webdyno1台分作成され、その上で今回のサンプルアプリが動くようになります。3

では、次のコマンドを実行して、dynoがちゃんと動いていることを確認しましょう。

なんだか余計な情報もだらだら出てきますが、web dyno1台分動いていることが確認できます。

これで、サンプルアプリが動くようになったので、curlを使って、Web URLにアクセスしてみましょう。 (サンプルアプリのWeb URLは、heroku apps:infoに書いてありましたよね?)

なにかおかしいですね… なにもレスポンスが返ってきません。 なにかエラーが出ているはずなので、Heroku上で起こったエラーを実際に見てみたいです。

Link to
here
Heroku上で動いているアプリのエラーを見てみる

Herokuには、とってもすばらしいログ機能があり、アプリの標準エラーや標準出力を簡単にチェックできます。

とても便利ですね! どうやらこれまで何度も見てきた例のエラーがまた出ているようです…

今回は、Heroku上で動いているPostgreSQLデータベースをちゃんとセットアップしていないのが理由です。

Link to
here
HerokuPostgreSQLサポート

HerokuPostgreSQLについてしっかりサポートしてくれている上に、なんと無料枠まで設けてくれています。

以下のコマンドを実行すれば、PostgreSQLのアドオンが使えるようになります。

これで、heroku-postgresqlアドオンを、無料で使えるhobby-dev利用枠で使えるようになりました。

では、本当にPostgreSQLが作成されたか、以下のコマンドを使って確認してみましょう。

データベースの詳細情報については、pg:infoコマンドを使って見れます。

Link to
here
アプリケーションを再起動する

これでPostgreSQLのデータベースが動くようになったので、アプリを再起動しましょう。

もう一度ログを見て、本当にこれでエラーが出なくなったか確かめてみます。

すごーい!ついに、ついにちゃんと動いたみたいです!!

もう一度curlコマンドを使ってAPIがちゃんと動いているか確認してみます。

ちゃんとレスポンスが返ってきています! 今度はコメントを取得してみましょう。

いいですね! SPJ(Simon Peyton Jones / Haskellの父)さんが「目先の便利さにとらわれてHerokuに余計な機能をいれるのは、ダメ。ゼッタイ。」4と言っています。 これで全てうまくいったようです。

Link to
here
Heroku上のアプリは接続先のDBをどうやって見つけているのか

賢明な読者のみなさんは、「Heroku上のアプリはどうやってデータベースを見つけているんだろう?」と疑問に思ったかもしれません。 実は、Herokuにはアプリに環境変数を与える仕組みがあります。

この環境変数の設定値を確かめるには、以下のコマンドが使えます。

heroku-postgresqlアドオンでPostgreSQLのデータベースを作成した際に、DATABASE_URLという名前の設定値が追加されます。 Herokuはアプリの起動時にこの設定値を環境変数として与えているのです。 先に述べたとおり、今回のサンプルアプリは、DATABASE_URLという環境変数を接続先DBの情報として受け取るようになっています5

Herokuに設定されている環境変数は、heroku config:get VAR_NAMEで取得できるので、次のコマンドを使ってDBに接続することもできます。

Link to
here
アプリのアップデート

Heroku上で動いているアプリをアップデートするのは、とっても簡単です。 単に以下のコマンドを実行するだけで大丈夫です。

このコマンドは、DockerイメージをビルドしなおしてHeroku container registryにアップします。 その後関係するdynoを全て再起動して、アップデート後のアプリを実行するようにします。

Link to
here
もっと良くするために

このサンプルアプリは、いまのままでもいい感じですが、いくつかまだ改善の余地があります。 一番手っ取り早い改善箇所は、Dockerfileでしょう。 Dockerfileをもっと良くするためのアイディアをいくつか挙げてみます。

  • Dockerfileのベースイメージに、もっとファイルサイズが小さいものを使う

    現状では、Herokuのイメージを使っていますが、 たぶんもっと軽いAlpine Linuxを使っても問題はないと思います。

  • stackGHC、その他よく使うHaskellライブラリが最初から入っているイメージをベースにする

    こうすることで、一番最初のdocker buildに要する時間をガッツリ削ることができます。

  • Dockerfileの一番最後で、stackGHC、全Haskellライブラリを削除するようにする

    こうすることで、Dockerイメージのサイズを少し減らせる可能性があります。 Heroku container registryにイメージをアップロードするのが、いくらか早くなるでしょう。

また、docker-composeなどを使って、ローカルで実行する際にもDockerを使ってPostgreSQL DBをセットアップするのも良いかもしれません。

Link to
here
まとめ

ローカル環境上でDockerが動いていれば、Heroku上でHaskellのコードを動かすのはとても簡単です。 Herokuの無料枠はアプリのプロトタイプを他の人に試してもらったりするのに最適です。 もちろん、そのままリリースしたら負荷にたえられないかもしれませんが、アプリ開発の最初期段階にコンセプトを検証したりするのには十分でしょう。

もし、検証の結果うまくいきそうだと分かったら、クレジットカードを登録して、 もっと負荷にたえられる有料利用枠でアプリを動かすようにするのだって簡単です。

Link to
here
脚注


  1. 僕が自分で許可して、原著者に日本語の内容をチェックしてもらいました。また、翻訳ではなくローカライズなので、原文の逐語訳ではなく、日本語話者にとって理解しやすいように一部加筆修正してあります。

  2. ここで挙げた7ステップはちょっと複雑です。 もちろん、1コマンドだけでGHCのインストールから依存ライブラリのインストール、 アプリ自体のビルドまで完了することもできますが、ここではDockerのキャッシュ機構を活用するために いくつものコマンドに分けて記述してあります。 Dockerのキャッシュ機構によって、docker buildを再実行するときには、入力値が変わったコマンドだけが実行されるようになっています。 たとえば、servant-on-heroku.cabalのファイルを変更してdocker buildを再実行すると、 .cabalファイルに書かれた依存ライブラリをインストールする(4)のステップからイメージを再ビルドし始めます。 キャッシュされているデータを利用するので、(1)から(3)までのステップを省略できるのです。

    同じように、src下のファイルだけを変更してdocker buildを再実行すると、 (5)のステップ以降のみが実行されます。 GHCや依存ライブラリをわざわざ再インストールする必要はないからです。

    このように、ステップをいくつかに分割することで、ビルド時間を大きく節約することができ、 2回目以降のビルドが数分で終わるようになります。最初のビルドは1時間もかかっていたのに、ちょろいですね。

  3. dynoにはいろいろな種類のものがありますが、 今回のような単純なWeb APIであれば別にこだわる必要はないです。

  4. 元ネタは同氏のAvoid success at all costsという言葉で、「目先の便利さにとらわれて、Haskellに余計な機能をいれるのは、ダメ。ゼッタイ。」みたいな意味。

  5. Herokuは、他にもPORTという環境変数も使っていて、アプリケーションにどのポートでリクエストを待ち受けるかを指定できます。