Crystal勉強会でFrostネタで登壇し、Herokuにデプロイするまでを纏める

ドーモ、読者=サン、ペンギン@ucmskyです。

結構ご無沙汰してますが、先日1月22日にクックパッドさんで、第3回Crystal勉強会が開催され、ありがたいことに15分のセッションの枠をいただくことになりました。

実際は20分以上喋り尽くしました。

そして先日、codeiq magazineさんに記事にしていただきました!
いえーい。ピースピース。
実践的なテーマの発表も増えた「東京Crystal勉強会」第3回の様子をレポート!

私のセッション資料は、こちらです。
CrystalでもRailsを使いたいですか?

「バグを見つけたら、”俺が直す”くらいのつもりで触れ」と僕はキメ顔でそう言った。

後、「Kemalはいいぞ」

Crystalの簡単な紹介

ここでざっとCrystalについて完結に説明させていただきます。
平たく言うと、Rubyライクな言語なのですが、以下の特徴を持っています。

  • ビルドしてLLVMバイナリを出力する
  • 静的型付け
  • マクロによるコードジェネレート

文法に関してはとりあえず以下の解説に目を通していただけると感覚は掴めると思います。

Crystal 入門

ご覧のとおりのRubyライクです。
ただしRubyとの大きい相違点として、メタプログラミングで使用するeval系のメソッドなどが使えなくなっています。(ですのでメタプログラミングRubyで記載されている内容になるともう全く、RubyとCrystalは別物と言って過言ではないです)
ただ、とりあえず動かしてみるような場合に関しては、Rubyの知識でも概ね問題はないと思います。(実際に僅かな読み替えで対応可能でした)

尚、去年のRubyKaigi2015でもCrystalを扱ったセッションもありました。

Introducing the Crystal Programming Language

Heroku社の中の人の@leinweberさんによるCrystal概論の発表です。

ぶっちゃけ私がRubyKaigi2015のLTのCFPに送った内容とほぼ全部被りました。

その他細かい点の解説、対応しているプラットフォームの情報、更新履歴などは公式サイトや、日本コミュニティのサイト(ドキュメントなどは日本語に翻訳しています)に記載されています。
URLはこちら。

なお、Latestバージョンは2016年2月4日時点で0.11.1となっています。

Crystalを取り巻くツール

ツールの開発も活発に行われています。Webで公開されているもので有志の人が纏めた一覧がこちらです。

awesome-crystal

DBアクセス用のツール、特にpostgresqlへのアクセスツールは前述のRubyKaigiで登壇された、@leinweberさんが作られました。

Web系のツールの開発も活発に行われていて、Rubyでよく使われるSinatra系のツールや、WebではおなじみのRuby on Railsライクなツールも出始めています。

Sinatraライクなツール Kemal

シンプルな構成になっているフレームワークですが、現時点でも高い品質となっています。 実際に公開されているアプリ開発にも使用されており、第3回Crystal勉強会でもWebアプリ開発事例としてKemalを使用したアプリが紹介されていました。

Crystal で実際のウェブサービスは作れるのか?

Frost as Crystal on Rails

そしてもう一つ関心を抱かれるのが、CrystalがRubyライクというなら、Railsのようなツールは無いのか? だと思うのですが、幾つか候補はあります。

一つがAmethyst、もう一つが今回取り上げるFrostです。
どちらもまだバージョンは0.2を超えるかどうかという始まったばかりのプロジェクトです。
Frostを取り上げた理由については

  • 全体的な枠組みが出来つつある
  • ActiveRecordライクなツールがある程度使える

という感じです。

サイトはこちら Frost

チュートリアルは同じリポジトリの
guides
に同梱されています。

Latestバージョンは2016年2月4日時点で0.2.1となっていますが、Crystal本体の破壊的変更(コアライブラリのファイル名やファイルパスがよく変わるので)の影響で動作しなくなっています。
(時間を見つけて私も調査してみます)

動作確認については、Crystal 0.10.2で確認している以下のリポジトリをcloneすると取り敢えず動きます。

frost_sample

チュートリアルでまだ書かれていない部分で、私が確認した点は以下のとおりです。

どんな感じのツールか

取り合えずRailsでいうところのrails new では以下のディレクトリなどが生成されます。

パッと見た感覚では、大枠の構想は固まっているように見えます。どこに何を配置するかは、そのままRailsの仕様に沿うようです。

Routing

公式サンプルでは以下のRoutingの設定が記載されています。


# config/routes.cr
Frost::Routing.draw do
  resources :posts, only: %i(show index) 
  (中略)
end

showとindexを使用していますが、onlyの設定を外すとエラーになります。(まだ幾つかのメソッドが実装されていないのが原因)
幾つか試した結果、newとcreateを追加することは出来ました。
(editとdeleteメソッドは無理の様子)


# config/routes.cr
Frost::Routing.draw do
  resources :posts, only: %i(show index new create)
  (中略)
end

ActiveRecord

ある意味一番気になる箇所となる、ActiveRecord的なO/Rマッパーの実装ですが、試した結果、検索系の幾つかと作成処理は使用可能でした。
FrostのDB処理を行っているコードを見る限りでは、削除系も定義はされていました。
(編集処理も同Frostのコードを見る限りでは普通にオブジェクトを扱うように行えるように見えました)

検索系

  • #{ModelName}.find(id)
  • #{ModelName}.find_by({column1: hoge , culumn2: huga })
  • #{ModelName}.all.pluck( column1 ) #未確認

レコード追加処理

  • hoge = Hoge.new
  • hoge.column1 = aa
  • hoge.column2 = bb
  • hoge.save

削除処理

全体的に未確認です。コードを見る感じでは以下の使い方は出来そうですが。。

  • #{ModelName}.delete_all
  • hoge = Hoge.find(id)
  • hoge.delete

Herokuで動かしてみる

勉強会までに間に合わなかったのですが、Herokuで動かすことも出来ます。というかFrostやKemalでも使えるように拡張build-packを作りました。(まだまだ動作が不安定ですが)

heroku-buildpack-crystal

作成にあたり、Elixirのビルドパックを多大に参考にさせていただきました。

ぶっちゃけElixirのbuildpackの8割コピペですが

heroku-buildpack-elixir

作り始める時点では実はビルドパックの構造を殆ど知らなくて、公式のビルドパックに少しずつ設定を足しながらの作業でした。 (compileファイル起動時に渡されるパラメータも知らなかったし、DATABASE_URLなどDBアクセスに必要な設定の渡し方も知らなかったほど)

Heroku Buildpackの作り方

使い方等は上記リポジトリのREADMEに記載していますが、ここでビルドパックにはどういう設定を記載すればいいのか簡単に説明します。

作り方の記載の全てはHeroku公式ドキュメントに記載されています。

Buildpack API

本ブログでも過去にビルドパックの作り方について解説している記事があります。(かれこれ2年以上前の記事ですが)

Herokuにバイナリを組み込むbuildpackを作成する

使用言語はbashであれば問題無いです。別にbashでなくてもよく、極端な話、herokuのDyno上で実行可能な言語であれば何でも構いません。実際に公式のRubyのビルドパックではRubyが使用されていたり、Groongaのビルドパックも同様にRubyで記載されています。

ビルドパックに必須となるファイルは以下の3つです。

  • detect
  • compile
  • release

上記3つのファイルをのカレントのbinディレクトリ以下に設置します。

detect

このビルドパックを適用する対象のプロジェクトかどうかを判断します。
Rubyの公式ビルドパックを参考にしますと、こんな感じになっています。


#!/bin/sh

if [ -f "$1/Gemfile" ]; then
  echo "Ruby"
  exit 0
else
  echo "no"
  exit 1
fi

Rubyのプロジェクトであるなら、ビルドパスのカレントにGemfileがあるはず、という判定をしています。
(ここでGemfileのパスに指定している$1がビルドパスを表しています(後述))
Crystalだとこれをshard.yml(ライブラリ管理ツールshardsが読み取る設定ファイル)に置き換えます。(v0.8以降のデファクト)

compile

多分一番処理が多くなる箇所であり、ビルドパックの中心となるファイルです。
ファイル名の通りプロジェクトのビルドを行うファイルですが、ビルドに必要となるコンパイラ、インタプリタ本体のダウンロード、ビルド、ビルド後の処理などは全てこのファイルで設定します。
実行時に3つの引数が渡されます。

  • 第1引数 ビルドパス
  • 第2引数 キャッシュディレクトリパス
  • 第3引数 環境変数の情報一覧を格納したディレクトリのパス

ビルドパスとはまさにアップロードするアプリが格納されるパスです。つまりビルド対象となるパスとなります。
キャッシュパスはビルドするツールを格納するパスです。CrystalならCrystal本体をダウンロードしてここに格納します。
Elixirの場合だとElixir本体とErlang本体、mixによってダウンロードされるパッケージなどもここに格納しています。
最後に環境変数一覧が格納された環境変数のパスが渡されます。
ここで言う環境変数とはHeroku側ですでに設定されている環境変数のことで、環境変数名の名前のファイルに環境変数の値が記載されたものが格納されたディレクトリとして渡されるようです。
FrostやKemalを動かそうとする場合、大体のケースでDBアクセスが必要になりますので、Heroku側で標準で用意されているPostgreSQLを利用する場合などにこのタイミングでアクセス先のDBの情報を読み取ります。
(Frostの場合、ActiveRecord的にDBのカラム名からメソッド名を解決している箇所があるので、ビルド時にDBにアクセスしに行けないとエラーになります)

Crystalのビルドパックではだいたい以下の流れで処理を行っています。

  • ユーザ指定のカスタムコンフィグを読み取る
  • Herokuの環境変数を読み取る
  • Crystal本体のダウンロードとインストール
  • shardsによる関連ライブラリのインストール
  • アプリのビルド
  • ビルド後に実行するコマンドがあれば実行(未実装)

公式のCrystalのビルドパックでは使用するCrystalのバージョンがハードコーディングされていたので、異なるバージョンのCrystalを使う場合都度ビルドパックを書き直す必要がありました。
それをユーザ指定できるようにしたかったので、これはこれで手間ですが、ビルドパスにHeroku用のビルドコンフィグファイルを用意してもらって(ちなみになければなくてデフォルトの設定が走ります)、それを読み取るようにしています。
大体設定しないといけないのがCrystalのバージョンと、ビルドコマンドです。 なんのファイルをビルドするのかも公式のビルドパックではハードコーディングしていました。app.crという名前だった気がしますが、その名前でファイルを作っていないとビルドできませんでした。
この状態だとFrostと激烈に相性が悪いので(Frostだとビルド対象のファイル名はそのままプロジェクト名となる)それも変えたかったので。
後は大体Elixirの流れをそのまま持ってきています。(Elixir固有の設定までベタ打ちしてたのは修正予定です)

release

一番最後に実行されるファイルです。 環境変数の設定とアドオン追加は初回のgit push時にのみ実行されるので、ビルドパスに.profile.dディレクトリを作ってその中にbashスクリプトを設置しておいたほうが柔軟に運用できると思います。 その他、Procfileがない場合の動作を設定できますが、Procfileで制御したほうがまず問題ないかと思います。
複数のビルドパックを指定した場合は、一番最後に実行されたビルドパックのreleaseが実行されます。 極端な話、このファイルに関しては設定があれば少し便利という具合です。

謝辞

最後に、登壇のお声掛けいただいた @pine613さんに感謝を。
RubyKaigi2015のLTのCFP提出するなど年末の行動力以外取り柄がなかった私ですが、初のLT以外のセッションで色々と緊張しっぱなしでした。
次回も何かやりますのでよろしくお願いいたします。

会場提供いただきましたクックパッド様、ありがとうございました。
セッションが始まったらやたら表示が途切れてしまった中対応してくださった、@mirakuiさんと@rosylillyさんには感謝です!
……元々いろいろケチってHDMIケーブルでつないでいたのをVGAアダプタに通してたのでマシンパワーが足りなさすぎでした。。
VGAケーブル用意しておきます。。

ビルドパック作成時にわたわたしてた時にいつもいい感じに援護射撃くださる @zundanさん、ありがとうございました。
またPRくださった、@5t111111さんと@HKDnetさん、まともなテストもできてないポンコツアプリをいい感じにしていただいて感謝です!
最後に、いっつもしょうもない質問に対応していただいているCrystal-JPコミュニティの皆様方、ありがとうございました。


コメント(0)