DaruとStatsampleを使ったデータ分析を試してみた

はじめに

こんにちは、Misoca開発チームの洋食(yoshoku)です。 私は、Python十年選手な機械学習メンですが、MisocaではRuby小僧なRailsキッズしてます。 データ分析のフィールドでは、RやPythonがメジャーなプログラミング言語ですが、 SciRubyプロジェクトを中心に、 Rubyでもデータ分析やるぜ的なバイブスが上がってきています。 そんなわけで、レペゼンMisocaで、基礎的なデータ分析を試してみました。 f:id:yoshoku:20170802232449p:plain

DaruとStatsample

Daru(Data Analysis in RUby)は、その名のとおり、 Rubyでのデータの操作や可視化を実現するライブラリで、 PythonのPandasに相当するものです。 Statsample は、 統計的な分析・検定手法を提供するライブラリで、 PythonのStatsModelsに相当するものです。 どちらもgemコマンドでインストールできます。

$ gem install daru statsample

requireもライブラリ名そのままです。

require 'daru'
require 'statsample'

いろいろやってみよう

データの読み込み

Daruでは、データを、ExcelファイルやCSVファイルを読み込むことや、SQLの実行結果から得ることができます。 今回はCSVファイルから読み込むことにします。 使用するデータは、UCI Machine Learning Repositoryで公開されている、ワインの品質に関するデータを選びました。 このデータは、拡張子はCSVですが、項目はセミコロン(;)で区切られています。 Daruでは、from_csvメソッドでCSVファイルを読み込みます。 このとき、セパレータを指定できるので、カンマで区切られていなくても、読み込むことができます。

df = Daru::DataFrame.from_csv 'winequality-red.csv', {col_sep: ';'}

平均や標準偏差を見てみよう

ワインデータには、アルコール度数などのワインのパラメータと、ワインの品質が10段階で評価された値が含まれています。 いくつかのパラメータの平均や標準偏差を見てみましょう。 データから特定の項目を複数とり出したい場合は、項目名をカンマ区切りで指定します。

ds = df['fixed acidity','alcohol']

describeメソッドにより、各データ項目の平均や標準偏差を確認することができます。 Daruでは、to_sメソッドは、単なる文字列ではなく、htmlを出力するものが多いです。 これは、ReportBuilderに渡して、実行結果をhtmlで書き出すことを想定している様です (いまは Jupyter Notebook + iruby な時代ですが、せっかくなので使ってみましょう)。

rb = ReportBuilder.new
rb.add(ds.describe.to_s)
rb.save_html('result.html')

とすると、データ数・平均・標準偏差・最小値・最大値の表が作成されます。 ※表が大きくなるので小数点5桁までとしました。

alcohol fixed acidity
count 1599 1599
mean 10.42298 8.31963
std 1.06566 1.74109
min 8.4 4.6
max 14.9 15.9

相関行列を計算してみよう

相関行列も、corrメソッドにより、簡単に計算できます。

ds = df['fixed acidity','residual sugar','density','alcohol']
rb.add(ds.corr.to_s)

出力された相関行列をみると、「fixed acidity」と「density」の間に相関関係がありそうです。

fixed acidity residual sugar density alcohol
fixed acidity 0.99999 0.11477 0.66804 -0.06166
residual sugar 0.11477 1.0 0.35528 0.04207
density 0.66804 0.35528 1.0 -0.49617
alcohol -0.06166 0.04207 -0.49617 1.0

そんなわけで、Statsampleを使って散布図を見てみましょう。

rb.add(Statsample::Graph::Scatterplot.new(df['fixed acidity'], df['density']))

とすると、散布図が出力されます。

f:id:yoshoku:20170731170156p:plain:w320

※今回「fixed acidity」と「density」との間になにか相関関係があることは確認できましたが、 その意味するところは、私はワインの専門家ではないのでわかりません。 パっと思いつくのは「説明変数としてはfixed acidityかdensityのどちらか一方を削っちゃうかも」ぐらいです。 データ分析では、そのデータに対する知識も必要ですね。

重回帰分析をしてみよう

最後に、Statsampleを使って重回帰分析をしてみます。 ワインの各パラメータから、品質を表現してみましょう。 Statsample::Regression.multipleに、データと、データ中で目的変数とする項目名(ここでは品質を表すquality)を与えるだけでOKです。

model = Statsample::Regression.multiple df, 'quality'
puts model.summary

得られたモデルのsummaryメソッドで、決定係数などを確認できます。 ワインの品質には「free sulfur dioxide」「sulphates」「alcohol」あたりが重要そうな感じです。

# ここでは省略しますが説明変数のt値の一覧なども出力されます。
  R=0.600
  R^2=0.361
  R^2 Adj=0.356
  Std.Error R=0.648
  Equation=21.965 + 0.025fixed acidity + -1.084volatile acidity + -0.183citric acid +
  0.016residual sugar + -1.874chlorides + 0.004free sulfur dioxide +
  -0.003total sulfur dioxide + -17.881density + -0.414pH + 0.916sulphates + 0.276alcohol
  ...

この他、Statsampleには、t検定やカイ二乗検定といった検定も用意されているので、A/Bテストなどにも使えます。

まとめ

RやPythonと比べると、実装されていない機械学習アルゴリズムもあったりしますが、 Rubyでも、回帰分析や主成分分析など、基本的なデータ分析はできます。 ログからエビデンスを集め、お客様が必要とするサービスを、 データドリブンにデベロップしていく基盤が用意されつつあります。

採用

Misocaではサービス開発の基盤作りにLOVEなバイブスあふれたエンジニアを募集しています!

自転車で秋田からMisocaにきました

はじめまして!7月からMisoca開発チームにジョインしたfukayatです。

製造業勤務を経て、Misocaにジョインしました。

プログラマとしての業務経験も、Webの経験もないのでこれからどんどん勉強していきます。

ジョインしたらブログを書くという流れに乗って、自己紹介とMisocaにきて感じたことを綴ります。

どこからきたのか

秋田県の出身です。

のどかな田園風景広がる、時間もゆっくりとした場所で生まれました。

自転車が大好きです。

f:id:fukayat:20170728173049j:plain

なにをしてきたのか

学生時代

学生時代は部活動でEVエコランと飛行ロボコンというのをやってました。

EVエコランとは、オリジナルの電気自動車を製作し、大会から支給されるバッテリーのエネルギーでどれだけ長い距離を走れるかを競う競技です。

電気回路の設計・製作やモータの開発などに携わっていました。

f:id:fukayat:20170727095609j:plain ※真ん中の車体が我々の製作した車体です。

飛行ロボコンとは、オリジナルのラジコン飛行機を製作し、大会から課されるミッションをこなしてポイントを稼ぎ、競う競技です。

機体の設計・製作、自動制御システムの設計・プログラム・製作、パイロットに携わっていました。

f:id:fukayat:20170727095118j:plain

これだけではイメージが沸かないと思うのでYouTubeにある飛行動画をどうぞ!


2015年式Pegasus飛行練習9 20150807

卒業後

卒業後は製造業に従事していました。

製品開発に携わりたいはずだったのに、現場配属だったのではじめはがっかりしていましたが、 もともとプログラミングが好きだったので、個人的にオンライン講座を受講したり、ArduinoRaspberry Piをいじったりしていました。

現場で2ヶ月半働いてみて、「やっぱり自分の好きなプログラミングを活かした仕事に就きたい」という思いが強くなり、転職を決意しました。

なぜMisocaを選んだのか

地元就職は難しい!

「地元大好き人間」なのでもちろん地元への転職を考えていました。

しかし、地元にはプログラマとしての採用口がほとんどなく、あったとしても魅力的な業務ではありませんでした。

リモートワーク

そんなときに偶然リモートワークという言葉に出会い、その魅力にとりつかれて仕事を探した結果がMisocaです。

リモートワークを推奨しているこの会社なら、僕のわがままを叶えてくれるだろうと感じ、応募しました。

現在は名古屋にいますが、将来的には秋田に戻ってリモートワークで働きたいと考えています。

転職してみた感想

前職に1年ちょっとしかいなかったこと、Webの経験がほとんどないこと、プログラマとしての経験も浅いことから、 そもそも転職できるのか、まわりについていけるのか、かなり不安でしたが、諸先輩方の暖かいフォローのおかげですぐに慣れることができました。

Misocaの先輩方、ありがとうございます!

Misocaについて思ったこと

すべてのスピードがはやい!

Misocaにきてはじめに思ったのは、ミーティングのスピードがすごく速いということです。

バンバン意見が飛びかって、すぐに内容が決まってしまいます。

担当者を決めたり、役割を決めたりするときには率先して手を挙げている姿が印象的です。

「わからない」と自信をもって言える

Misocaにはわからないことを「自信をもって」わからないと言える環境があります。

コーディングで行き詰まってしまっても、社内チャットで誰かにひとこと「助けて!」と声をかければ教えてくれます。

基本的にリモートにいるメンバーは常時ビデオチャットでつながっているので、リモートにいるメンバーとも画面共有を利用して情報交換をすることができています。

残業はしない

事前に「基本的に残業はしません」と聞いていて、正直にいって、そんなの嘘だと思っていました。

ところが本当に残業はなくて、むしろちょっと多めに働いてしまった分はその分早く帰ることができます。

限られた時間内でいかにパフォーマンスを発揮するかを常に考えているから実現できていることだと思います。

おわりに

最終的に、転職をして本当によかったと感じています。

まだまだプログラマとしても、社会人としても未熟ですが、わからないことを少しずつ吸収しながら成長していきたいと考えています。

さらに、秋田に戻ったときにはセンサやドローンを駆使した農家として活躍したいと考えています。

これからよろしくお願いします!

f:id:fukayat:20170728154329j:plain

採用

Misocaでは自転車が大好きなエンジニアを募集しています!

CodeRetreatに参加しました! 〜ペアプロのすゝめ〜

こんにちは、Misoca開発チームのころちゃん(corocn) です。最近はイカ2の前夜祭に参加しました。

今回は07/08に名古屋で開催された CodeRetreat に参加した感想を書きたいと思います。 参加者は約20人で、Misocaからは、dominion525(主催)、eitoballcorocnの3名が参加しました。

CodeRetreat

CodeRetreatとは、プログラマのスキル向上を目的として、世界各地で定期的に開催されているイベントです。 ざっくりと説明すると

  • テーマは全員共通「コンウェイライフゲーム」を開発する
  • ペアプロで開発する
  • 言語やツールは自由だが、ペアと相談して決める
  • 1セッション45分として、セッションが終わったらコードは全部消す
  • ペアを変えて5〜6セッション回す。後のセッションになるほどルールが厳しくなる。

のような特徴があります。詳しくは以下を参照してください。

意気込み

※どんな方でも安心して参加いただけます

会場

毎週月曜日に名古屋ギークバーが開催されている、Club Adrianaさんで行いました。 何回か来たことがありますが、ここは飯が美味いのです。

当日の流れ

ファシリテーターの原田さんから、挨拶を兼ねて進め方やライフゲームの説明。

参加者の使用言語は、Java, C#が半数、Ruby半数な感じで、Scala, F#, Groovyチョットデキル勢がいました。 JavaScriptPHP扱える人も勿論いましたが、今日は別の言語でやるよ!って人が多かったように思えます。 共通言語がないからC言語で書いたというペアも。私はRubyで参加しました。

以下、各セッションのルールと内容をまとめてみました。 なお、CodeRetreatにおけるルールはオプションなので、採用しなくてもOKです。疲れたからこのセッション休憩しますもあり。

session 1

  • ルール
    • 自由に開発してみよう
  • やったこと
    • 順当に対象セルの周りを調べて生存判定をするコードを書きました

みんなもくもくペアプロしてますね。

session2

  • ルール
    • TDDで開発すべし
  • やったこと
    • TDDが良い感じで回せて、全体にお披露目 (ただしルールによりコードは削除 😭
    • 誕生なんて生ぬるいので bakutan! メソッドを実装
    • 常にHi-Fiveしながら開発!! (めっちゃ楽しい)

振り返りとして、良さげな実装を皆で見ます

昼食

  • 無限生ハム原木と美味しい昼食でひと休み
  • みなさん休憩せずに、ずっと設計の話をしているようでした。みなさん好きですねー。

生ハムは切るのが難しい!

この他にもローストビーフやパスタが提供されました。飲酒してる人もちらほら。

session3

  • ルール
    • 配列禁止
    • TDDサイクルを10回以上回すべし
  • やったこと
    • 色々迷走して上手く実装できず。MAP使えば良かった・・・
    • 死滅セル=無職、生存セル=有職という独自の世界観の構築に成功

session4

  • ルール (Evil Mute A/B pairing)
    • 会話禁止
    • 片方がテストを書き、片方(Evil coder)が対象コードを実装するべし
    • Evil coderはテストを通るだけのミニマムなコードを書くべし
    • ただしEvil(邪悪)なので、テストは通るが、テスターの意図を外したような実装をすべし
  • やったこと
    • ルールは共通言語が一緒のみ可能なので、今回は断念。
    • セルが周りに生存メッセージを投げる方法を試してみました。

おやつタイム

www.youtube.com

session5

  • ルール
    • 疲れ切ってて全く覚えてないのですが、最後は「良いコードを書こう!」だったようです。
  • やったこと
    • ペアの方がJavaScriptで実装しましたが、テストツールが上手く動かずはまりました。上手くアドバイスできず 😭
    • DOM使えば配列いらんな。とか思いながら進めました。

懇親会

  • 懇親会は6割程度の方が参加していました。
  • 今後のCodeRetreatの活動の話、TDDの話、普段書いているコードや、普段参加している勉強会の話をしていました。
  • 前回はみんな疲れ切ってて 懇親会自体行わなかったそうですが、今回は休憩を長めにとったせいかみんな元気に酔っ払っていましたね。

まとめ

ざっと紹介しましたが、ルールのおかげでいい感じに負荷がかかり、どなたでも同じぐらいの疲労度で1日楽しめるイベントという印象でした。 学生から社会人まで幅広い業種の方が参加しており、普段一緒にコードを書かない人とペアプロできるので、かなりオススメのイベントです。

次回は Global Day of Coderetreat と呼ばれるイベントが2017年11月に開催予定です。 Misocaではペアプロを積極的に取り入れていますが、普段ペアプロをしない方もぜひぜひ参加してみてはイカがでしょうか。

採用

Misocaではペアプロ大好きなエンジニアを募集しています!

AWS SSM を使って、Thor タスクを実行する

AWS SSM を使って、Thor タスクを実行する

id:eitoball です。Misoca では、減量ブームでそれに乗っかろうとしましたが、一生懸命、筋トレをしたためか、体重が増えてしまい途方に暮れているこのごろです。

先月、id:dominion525 に誘われて、AWS Summit Tokyo 2017 に1日だけ参加してきました。それ以来、AWS の様々なサービスに興味を持つようになっています。

Slackの発言

というのを見かけたので、少し早い夏の自由研究として、AWS SSM(EC2 System Manager)で、Run Command という機能を使って、EC2 インスタンスにログインすることなく、Rails アプリケーションの Thor タスクを実行するようにしてみました。

実行までの流れ

今回は、AWS SSM の Run Command を使って、特定のEC2インスタンスに対して、決められた Thor タスクを実行するようにします。Rails アプリケーションが動作する EC2 インスタンスを1つ準備して以下のような流れを実施しました。

  1. EC2インスタンスの設定
  2. ユーザーの作成・ロールの付与
  3. System Manager ドキュメントの作成・登録
  4. RunCommand によるタスクの実行・結果の閲覧

    1. EC2 インスタンスの設定

まず、EC2インスタンスに割り当てられている IAM ロールに「AmazonEC2RoleForSSM」というポリシーを付与します。インスタンスに IAM ロールが割り当てられていない場合は、新たにロールを作成して、割り当てて下さい。詳しくは、AWS EC2 のドキュメントなどを参考にして下さい。 そして、インスタンスに SSM エージェントをインストールします。Ubuntu 16.04 LTS (64 bit)の場合だと以下のようになります。他のOS等のインストール方法については、ドキュメントに記載されています。

$ curl -L -O https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb
$ sudo dpkg -i amazon-ssm-agent.deb
$ sudo systemctl enable amazon-ssm-agent

2. ユーザーの作成・ロールの付与

特定のインスタンスに対してのみタスクを実行するためだけのユーザーを作成します。今回は、作成したユーザーに以下のようなポリシーを直接、付与しました。「i-12345abcde67890fg」は、今回、使用している EC2 インスタンスのIDです。「RunThorTask」は、次のステップで作成する System Manager ドキュメントの名前です。ssm:SendCommand などを許可する場合、 System Manager ドキュメントと実行する EC2 インスタンスの両方を Resource に含める必要があります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:ListCommandInvocations",
                "ssm:GetCommandInvocation",
            ],
            "Resource": [
                "arn:aws:ec2:ap-northeast-1:*:instance/i-12345abcde67890fg",
                "arn:aws:ssm:ap-northeast-1:*:*",
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:SendCommand",
                "ssm:DescribeDocument",
                "ssm:GetDocument"
            ],
            "Resource": [
                "arn:aws:ec2:ap-northeast-1:*:instance/i-12345abcde67890fg",
                "arn:aws:ssm:ap-northeast-1:*:document/RunThorTask"
            ]
        }
    ]
}

3. System Manager ドキュメントの作成・登録

特定の Thor タスクを実行するための コマンドの System Manager ドキュメントを作成します。今回は、あるユーザー(john.doe)が、 RVM(Ruby Version Manager)を使って、実行することとしています。Rails アプリケーションは、/var/www/vhosts/www.example.com/current にインストールされているとしています。 下記のような JSON ファイル(document.json)を作成します。今回は、allowedValues で、実行することのできる Thor タスクを listhelpversion の3つに制限しています。

{
    "schemaVersion": "2.0",
    "description": "Thor タスクを実行します。",
    "parameters": {
      "action": {
        "description": "Thor タスクを実行します。",
        "type": "String",
        "allowedValues": [
          "list",
          "help",
          "version"
        ]
      }
    },
    "mainSteps": [
      {
        "action": "aws:runShellScript",
        "name": "runThorTask",
        "inputs": {
          "runCommand": ["sudo -u john.doe /bin/bash -l -c \"source /home/john.doe/.rvm/scripts/rvm && rvm use . && RAILS_ENV=production bundle exec thor {{ action }}\""],
          "workingDirectory": "/var/www/vhosts/www.example.com/current"
        }
      }
    ]
}

作成したドキュメントを AWS CLI を使って登録します。

$ aws ssm create-document --content "file://document.json" --name "RunThorTask" --document-type "Command"

4. RunCommand によるタスクの実行・結果の閲覧

全ステップで登録した System Manager ドキュメントを使って、タスクを実行します。

$ aws ssm send-command --document-name "RunThorTask" --instance-ids "i-12345abcde67890fg" --parameters '{"action":["list"]}' --region ap-northeast-1

上記のコマンドを実行するとJSONが出力されます。結果の閲覧に必要な CommandId 以外は、省略しています。

{
    "Command": {
        "CommandId": "abcd1234-ab12-ab12-ab12-abcdef123456",
    }
}

CommandId を使って、実行結果を確認します。

$ aws ssm get-command-invocation --command-id "abcd1234-ab12-ab12-ab12-abcdef123456" --instance-id "i-12345abcde67890fg"

実行が成功している場合、以下のような結果が出力されます。

{
    "Comment": "",
    "ExecutionElapsedTime": "PT11.275S",
    "ExecutionEndDateTime": "2017-07-01T00:00:00.000Z",
    "StandardErrorContent": "",
    "InstanceId": "i-12345abcde67890fg",
    "StandardErrorUrl": "",
    "DocumentName": "RunThorTask",
    "StandardOutputContent": "...",
    "Status": "Success",
    "StatusDetails": "Success",
    "PluginName": "runThorTask",
    "ResponseCode": 0,
    "ExecutionStartDateTime": "2017-07-01T00:00:00.000Z",
    "CommandId": "abcd1234-ab12-ab12-ab12-abcdef123456",
    "StandardOutputUrl": ""
}

感想

  • EC2 System Manager で、色々なコマンドをログインしないで実行できるのは便利だと思います。また、IAM を使って、細かい実行権限の管理ができそうなのも便利だと思います。
  • 今回は使いませんでしたが、System Manager ドキュメントの作成には、 rezept を使ってみるのもいいなぁと思いました。

    採用

Misoca では、AWS の様々なサービスの活用に興味あるエンジニアも募集しています。

📝ParsletによるPEGパーサ

mzpです。 先日、北海道旅行に行きました。

先日文書番号ルールの設定機能をリリースしました。 今回は、その実装に利用したパーサライブラリParsletを紹介します。

✨文書番号ルールの設定機能

f:id:mzp:20170628130956p:plain

文書番号ルールの設定機能は、請求書などの右上に記載される番号の初期値を設定できる機能です。

このルールは{Y}{M}{D}-{連番} といった形式で指定でき、 {} 内に文字列によって何に置換されるかが決まります。 ここで指定できる文字列には以下のようなものがあります。

文字列意味
{Y} 文書作成時の年 (4桁)
{M} 文書作成時の月 (2桁)
{D} 文書作成時の日 (2桁)
{連番} 作成開始時点での文書ごとの連番

💬PEGの採用

当初は1文字ずつ見ていく、いわゆる手書きの再帰下降パーサを検討していました。しかし、今後、構文規則が拡張されていくことが予想されたため、パーサライブラリを利用するように方針を切り替えました。

どのパーサライブラリを採用するかも迷いましたが、PEGという構文規則に基づくライブラリが、言語を問わず存在して便利そうだったので採用しました。

このときの会話は以下のようになっています。

📝処理の流れ

文字列のパース

文書番号のルールを記述した文字列をパースし、扱いやすいデータ構造にします。

例えば、{Y}{M}{D}-{連番} は以下のようなデータ構造になります。

# {Y}{M}{D}-{連番}
[
  { rule: { identifier: 'Y') } },
  { rule: { identifier: 'M') } },
  { rule: { identifier: 'D') } },
  { word: '-' },
  { rule: { identifier: '連番' } }
]

このパーサは以下のように定義しています。

class Parser < Parslet::Parser
  RESERVED_WORDS = %w({ } Y M D 連番).freeze

  rule(:word) { match('[^{}[:space:][:cntrl:]]').repeat(1) }
  rule(:identifier) { RESERVED_WORDS.map { |w| str(w) }.reduce(&:|) }
  rule(:rule) { str('{') >> identifier.as(:identifier) >> str('}') }
  rule(:numbering_rule) { (rule.as(:rule) | word.as(:word)).repeat }

  root :numbering_rule
end

パースした結果の変換

{ rule: { identifier: 'Y') } } といったハッシュのままだと処理しずらいので、文字列を内部的に扱いやすい形に変換します。

先ほどの例は、以下のようになります。

# [
#  { rule: { identifier: 'Y') } },
#  { rule: { identifier: 'M') } },
#  { rule: { identifier: 'D') } },
#  { word: '-' },
#  { rule: { identifier: '連番' } }
# ]
[
 :current_year,
 :current_month,
 :current_date,
 '-',
 :document_sequence_number
]

この変換は以下のように定義しています。

class Transform < Parslet::Transform
  IDENTIFIER = {
    'D' => :current_date,
    'M' => :current_month,
    'Y' => :current_year,
    '{' => :left_brace,
    '}' => :right_brace
  }.freeze

  rule(word: simple(:s)) { s.to_s }
  rule(identifier: simple(:i)) { IDENTIFIER[i.to_s]  }
  rule(rule: simple(:r)) { r }
  rule(rule: sequence(:a)) { a }
  rule('') { [] }
end

文書番号の生成

変換した結果を元に文書番号を生成します。

先ほどの例からは、以下のような文書番号が生成されます。

# [
#   :current_year,
#   :current_month,
#   :current_date,
#   '-',
#   :document_sequence_number
# ]
20170630-001

これは文字列ならばそのまま出力する、シンボルならば内部で定義したメソッドを呼ぶ、という形で実装しています。

class Evaluator   
  def execute(rule)
    transformed = Transform.new.apply(Parser.new.parse(rule))
                                              
    transformed.each_with_object(::String.new) do |r, s|    
      s << case r                                                                                             
               when ::String then r
               when ::Symbol then respond_to?(r, true) ? send(r) : ''
               end
    end
  end

  private
                          
  def current_time
    @current_time ||= Time.current
  end               
           
  def current_year
    current_time.year.to_s
  end                                               
           
  def current_month
    format('%02d', current_time.month)
  end                                     
           
  def current_date
    format('%02d', current_time.day)
  end             
           
  def document_sequence_number
    # ...
  end
end

💖感想

Parsletがだいぶ分かりやすかったと思う。 実際、学習を含めても1時間くらいで実装できた。

また、手書きするより見通しがよくなってよかったと思う。

🔊 採用

Misocaでは構文解析に興味あるエンジニアを募集してます。

Webpacker is installed 🎉 🍰

こんにちは、@mugi_uno です。

庭に花壇を作ったところ変に日焼けをしてしまい、半袖で外に出るのが恥ずかしいです。

つい最近、Misocaのフロントエンド周りのビルドをWebpackerを利用したものに置き換えました。 様々な知見が得られたので書いてみたいと思います。

Webpacker?

フロントエンド用のビルドツールであるwebpackRailsから簡単に使えるようにするためのRubyGemsです。

github.com

f:id:mugi1:20170619094144p:plain

Rails5.1からはこのWebpacker経由で、rails new の際に同時にwebpack用の設定ファイルを生成することもできます。

流行りのフロントエンド環境を構築しようとする際、最近ではwebpackを利用するケースが多いですが、実際にはES6やReactなどが混ざることも多く、慣れていないと設定するだけでもなかなか敷居が高いです。

さらに、Railsと組み合わせる場合には、アセットパイプラインとの兼ね合いをどうするか?などといった課題も登場するため、考えるだけで鼻血が出そうになります。

Webpackerを利用することで、その辺りをいい感じに解消してくれるというわけです。

Misocaでの導入理由

browserify-rails めっちゃ時間かかる問題

もともとはアセットのビルドに browserify-rails を利用していましたが、ビルド時間がMisoca内で問題になっていました。キャッシュによる高速化が効いてはいるものの、何かの拍子にフルビルドが走ると数分待たされることも。

CIでも上記の影響を受けるため、開発効率にあまりよろしくない影響を与えていました。

(開発効率が落ちた図) f:id:mugi1:20170609181108p:plain f:id:mugi1:20170609181116p:plain f:id:mugi1:20170609181127p:plain

なんとかしたい

最初はbrowserify-rails自体のチューニングを検討しましたが、webpackでのビルドを一度試してみたところ、そもそもフルビルドでも20秒くらいで終わることがわかりました。

MisocaではRails5.1を導入済みだったのもあり、Webpackerを使って本格的に導入を試みることに。

webpackerがやってくれること

※以下内容は rails@5.1.1/webpacker@2.0 を前提としています。

大きく分けると以下の3つかと思います。

  1. rakeタスクの提供
  2. ボイラープレートの生成
  3. viewヘルパーの提供

rakeタスクの提供

Webpackerを追加すると、いくつかのrakeタスクが利用可能となります。 特徴的なものを簡単にご紹介します。

webpacker:install

ボイラープレートを生成する。(後述)

webpacker:check_node / webpacker:check_yarn

webpackerの実行には Node.jsとYarnが一定バージョン以上である 必要があります。

このタスクで、システムに必要なバージョンのNode.js/Yarnがインストールされているか検証できます。

webpacker:verify_install

webpacker:check_node/webpacker:check_yarn などを実行し、Webpackerによるビルドが可能な状態かを検証できます。

ちなみにブログタイトルの「Webpacker is installed 🎉 🍰 」は、このタスク成功時の出力結果です。

f:id:mugi1:20170616133609p:plain

webpacker:compile

bin/webpack を呼び出し、webpackビルドを実行します。 その際に RAILS_ENVの値を参照し、各環境に応じたビルドを実行してくれます。

また、実行前には自動的に webpacker:verify_install によるチェックが行われます。

assets:precompile からのコールについて

以下は assets:precompile のenhanceで事前タスクとして登録されています。

  • yarn:install
  • webpack:compile

これにより、例えば既存のデプロイプロセスなどで assets:precompile を実行していれば、そこでwebpackビルドが自動的に実行されます。

ただ、ここで実行されるyarn:install については、デフォルトでは --pure-lockfile が付与されないので、意図しないバージョンによるビルドが行われてしまう可能性がある点に注意が必要です。

Misocaでは、デフォルトの yarn:install タスクをオーバライドし、--pure-lockfile が付与されるように一手間加えています。

ボイラープレートの生成

webpacker:install を実行すると、webpackに必要な各種ファイルが自動的に生成されます。

  • webpack.config.js に相当するファイル群
  • ES6ビルド用のbabelrc
  • パス設定などを集約したYAMLファイル(webpacker.yml
  • などなど

また、Misocaでは利用していませんが、一部ライブラリに特化した形でのインストールも可能です。

  • webpacker:install:react
  • webpacker:install:vue
  • etc..

これにより、ES6+Reactなどを利用したフロントエンド開発を行うための環境を手早く構築することができ、ダイジェスト付きのjs生成なども簡単に行うことができるようになります。

viewヘルパーの提供

Sprocketsの場合は javascript_include_tag が提供されていますが、同様にWebpackerでは javascript_pack_tag が提供され、Webpackerによってビルドされたファイルを利用する場合はこちらを利用する必要があります。

(スタイルシート用に stylesheet_pack_tag もあります。)

導入のために行った作業

方針設定

がむしゃらに始めると迷宮に入っていくかもしれません。今回は「Webpackerの導入」にフォーカスしたいため、以下のような方針にしました。

  • jsのみを対象とし、cssは対象としない
  • ビルドを通すため以外のコードの書き換えは行わない

assets/javascripts 配下を webpackのentryとする

webpackはビルドの起点をentryとして設定に記述する必要がありますが、デフォルトでは javascript/packs 配下になっています。

そのままだと既存資産のjsファイルすべてを javascript/packs 配下へ移動する必要がありますが、Webpacker導入作業中もプロダクト開発は行われているため、頻繁にコンフリクトが発生することが予想されました。

さすがにつらいので、entryに assets/javascripts を加えることで、導入完了後にあらためて移動だけを行うことにしました。

shared.js に以下のようなコードを加えて、entryにObject.assignでマージすることで対応します。

const assetsPath = 'app/assets/javascripts';
const assetsEntry = sync(join('app/assets/javascripts', extensionGlob)).reduce(
  (map, entry) => {
    const localMap = map
    const namespace = relative(join(assetsPath), dirname(entry))
    localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry)
    return localMap
  }, {}
);

//= requireをCommonJS(またはES6 modules)に書き換え

Sprocketsで依存解決をしている場合、jsまたはcoffee内に以下のような記述が存在する可能性があります。

//= require jquery-ui
//= require_tree ./lib

これらは、CommonJSの require や、ES6 modulesの import/export を使った形に書き換える必要があります。

require('jquery-ui')
require('lib/foo')
require('lib/bar')
require('lib/baz')

なお、拡張子を省略した場合にうまくロードされない場合、config/webpacker.yml 内の extensions に追加する必要があります。

依存関係の解決をnpm利用に変更

必須ではありませんが、上記 //= require でロードされていたライブラリを、npmモジュールを利用するように変更しました。

地道に各ファイルのバージョンを調べ、同バージョンを yarn add していきます。

ただ、gemが提供しているjsファイルをrequireしていることもあります。その際は、jsのみがnpmモジュールとして提供されていないか確認し、

  • 提供されている → そちらを利用するように変更
  • 提供されていない → jsファイルのみを抽出して解決するか、フルパスで require する。

とした手順を踏んでいきました。

よく利用されているものでいえば、jquery-ujs などは単体で提供されています。

github.com

グローバル参照を解決

値がグローバル定義されているのを前提としているコードがある場合、グローバルに値を明示的に定義しないとアクセス不可となることがあります。(jQuery/$ などがよくあるケースだと思います。)

今回はグッとこらえてグローバル依存自体は残しました。意見のある方はこちらからどうぞ!

共通でロードするjsファイルがあれば、その先頭に以下のようなコードを足すだけで参照可能となります。

global.$ = global.jQuery = require('jquery');

なおMisocaで導入した際には、さらに以下のような対応も行っています。

  • ライブラリ群のみを個別の jsファイルに抽出した別ファイルとした。
  • ProvidePlugin を利用した、ライブラリから別ライブラリの参照。

ひたすらviewヘルパを置換

javascript_include_tag をひたすら javascript_pack_tag に置き換えていきます。

基本的には置き換えるだけで問題ないですが、javascript_include_tag の場合は複数スクリプトを単一タグでロードできるのに対し、javascript_pack_tagの場合はエラーとなるため、そこだけは分割する必要があります。

<%= javascript_include_tag('foo', 'bar', 'baz')%>

<%= javascript_pack_tag('foo')%>
<%= javascript_pack_tag('bar')%>
<%= javascript_pack_tag('baz')%>

foremanによる起動設定

開発時には、jsファイルを監視した上で自動ビルドしたくなりますが、その場合、rails serverとwebpackで二つのプロセスの起動が必要です。

単純にターミナルを2つ開くなどして起動してもいいですが、READMEには foremanを利用した方法が記載されています

こちらも併せて導入しておきました。

(基本的な導入手順はREADMEの記述のままなのでここでは割愛します。)

インクリメンタルビルドについて

webpackを利用したインクリメンタルビルドには2つの方法があります。

  • bin/webpack-dev-server
  • bin/webpack --watch

bin/webpack-dev-server を利用した場合にはホットリロードが有効になるなど、動作に違いがありますが、 好みや環境によってどちらを利用するか選択すれば良いと思います。

キャッシュを有効にしたいなどの理由から、Misocaでは bin/webpack --watch をデフォルトとし、foremanで利用するProcfileに記載しています。

ちなみに、webpack-dev-server では待ち受けポートがrails serverとは別(デフォルトでは8080)となりますが、このあたりは bin/webpack-dev-server や webpack用の設定ファイル内でいい感じに吸収してくれるので、特に意識しなくても大丈夫です。

まとめ

上記の他にも環境や既存のコードベースに応じた修正を行う必要があるかと思いますが、 導入にあたりコアとなった作業は以上です。

地味な作業も多かったですが、コツコツがんばりました。

最終的なdiffはこんな感じです。↓
+の部分はほとんどが yarn.lock ですが、なかなかのボリュームですね。

f:id:mugi1:20170616132756p:plain

成果としては、開発時の待ち時間も軽減され、なかなか良かったのではないかと思います。

Webpacker自体に関してですが、実際にはWebpackerが生成するボイラープレートに賛否両論あったりもするようです。個人的には、今まではRails+フロントエンドの環境構築は人それぞれだったものが、Rails Wayとして示されたこと自体が一つのメリットだと考えています。

カスタマイズしようと思った場合には、最終的にはある程度フロントエンド側の知識も必要となりますが、うまく使えれば様々な恩恵を受けられるかもしれません。

採用

MisocaではフロントエンドLOVEなエンジニアを募集中です!

MisocaのCI構成まとめ

こんにちは。4月からMisocaにjoinしました、tkykです。京都市内からリモートで働いています。盆地特有のねっとりとした暑さをやり過ごしつつコードを書いている今日この頃です。

f:id:tkykmw:20170609182049j:plain

さて、今回はMisocaのCI(Continuous Integration)環境がどうなっているか、その全体像を紹介したいと思います。

そもそもCIの目的とは?

ソースコードの一部に対する変更が、アプリケーション全体の動作を壊してしまっていないか、常時チェックするのが目的です。

そのために何をしているか

CI専用のサーバに、変更点を含むソースコード全体をチェックアウトして、依存ライブラリのインストールと必要な前処理を行い、すべてのテストを実行します。

すべてのテストがエラーなくpassしたことをもって、CIが通ったとしています。

Gitを使った開発フローとの統合

MisocaはGitHub上で開発を行なっているため、CIのプロセスもGitHubと密に連携しています。

リポジトリに新たなブランチをpushするたび、ブランチごとにCIが自動で実行されます。ブランチがPullRequestに紐づいている場合には、CIの結果がそのPullRequestに通知されます。

MisocaのCIを支えるツールたち

GitHubにpushしてから、CI結果が届くまでの流れはこのようになっています。

f:id:tkykmw:20170612132821p:plain

今のところMisocaではSaaS系のCIサービスは使用せず、EC2上に直接システムを構築しています。理由は、必要な性能(特にDBのI/O性能)を得るためのコストがかかりすぎるためです。

Jenkins

CIシステム全体を統括するのが、Jenkinsの役割です。

  • CIの実行方法を決定する
  • CIの実行結果を開発者に提供する
  • CIに関わる様々なタスクを実行する(例: テスト用DBのクリーンナップ)

など、多くの役目を担っています。ジョブの内容はGroovyコードで記述し、リポジトリの中で一体管理しています。

Jenkins の master/slave構成

多数のビルドを並列して実行できるようにするため、Jenkinsは1台のmasterと複数台(現時点では2台)のslaveで構成されています。またslaveそれぞれが同時に複数(現時点では2)のビルドを実行するよう設定されています。

現在のMisocaでは、masterはいくつかの重要なジョブのために予約されており、各CIジョブはslaveでのみ実行されます。

GitHubとの連携を担う GitHub Branch Sourceプラグイン

GitHub上での開発フローと、JenkinsによるCI実行とを統合するのが、GitHub Branch Sourceプラグインです。

このプラグインリポジトリに対するpushを検知し、ブランチごとにジョブを作成して、CIを実行します。PullRequestに対して結果を通知するのもこのプラグインの役割です。

rrrspec

rrrspecは、rspecを分散実行するためのソフトウェアです。

Misocaでのrrrspecの運用方法については、@kokuyouwindが書いた記事を参照してください。

rrrspec運用のためのInfrastructure as Code

上の紹介記事にある通り、rrrspec workerインスタンスはAuto Scalingによって増減します。すなわちいつでも起動・停止される可能性があるということです。よってworkerインスタンスは、起動してすぐにrrrspecクラスターに参加できるだけの、準備万端の状態で起動しなければいけません。

そのため、あらかじめ必要な設定・データをすべて盛り込んだAMIをつくっておき、Auto Scalingの起動設定でそのAMIを指定しています。

AMIは、VagrantChefを使って構成し、packerを用いて作成します。Auto Scalingやスポットインスタンスの設定はterraformによって管理しており、これらを一式まとめた構築レシピをGitで管理しています。

まとめ

こうしてCI環境が整備され、常時整合性に目を光らせているおかげで、我々エンジニアは安心してコードに手を入れられるようになっています。

採用

Misocaでは安心安全なCIのもとでコードを書きたいエンジニアや、CIシステムそのものを改善したいエンジニアを募集中です。