wataメモ

日々のメモをつらつらと書くだけ

CarrierWaveでSTI(Single Table Inheritance)した時のstore_dirについて

 またSTI系のネタになってしまったが、CarrierWaveでmountするモデルをSTIにしていると、generateしたままのstore_dirだとtypeが変わった時にパスが変わってしまう。

def store_dir
  "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end

 ここの「model.class.to_s.underscore」部分が子クラスの名前になってしまう。 なので以下のように修正。

def store_dir
  "uploads/#{model_name}/#{mounted_as}/#{model.id}"
end

private
def model_name
  klass = model.class
  while klass.superclass != ActiveRecord::Base
    klass = klass.superclass
  end
  klass.to_s.underscore
end

 STIは面白い考えだが気にしなければいけないことも多い。

ActiveRecordでSTI(Single Table Inheritance)時に自動でenum定義されるようにした

 Ruby on Railsでサービスを作ってみたのおまけSTI時にenumを書くとDRYの原則に則さないと書いたが、まあ無いなら作ればいいじゃないかということでactiverecord-sti-enumというrubygemを作ってみた。 (プロダクトレベルで使える物ではないのであしからず)

やりたいこと

 ActiveRecordSTIを使っている時にenumで追加されるメソッドを使いたい場合はenum定義を書く必要があった。 それだと継承クラスを追加した時にenumもメンテしなければいけないのでDRYではない。 それを子クラスを追加しただけで、自動でenumメソッドが追加されるようにしたい。

class Question < ActiveRecord::Base
  include ActiveRecord::Sti::Enum # これを書くだけでenumを書きたくない

  # 今までは下記のようにenumを定義する必要があった
  # enum type: {
  #   single:   'SingleChoice',
  #   multiple: 'MultipleChoice',
  #   date:     'DateChoice',
  #   free:     'FreeChoice',
  # }
end

実装方法について

  1. 親クラス(にmoduleをincludeされた時にincludedでフックする
  2. includedフックでinheritedフックメソッドを親クラスに追加する
  3. inheritedフックでは継承されたクラス名をベースにenum定義を追加していく

 継承された時に呼ばれるinheritedフックを利用してenumを追加しているわけだが、これだと初期化の問題が発生する。 例えばDateChoiceクラスが初期化される前に「@question.date_choice?」とか呼ばれるとNoMethodErrorとなってしまう。 最初はmethod_missingを実装してメソッド名からクラス名にして初期化しようかと考えたが、 全部が初期化される前に「types」等で定義されているenum一覧を取得した時におかしくなってしまうのでmodel配下をrequireしてしまう方法で対応。

Dir.glob('./app/models/*.rb').each do |f|
  require File.basename(f)
end

まとめ

 rubyは動的にクラスやメソッドが増減するので、クラスドリブンの場合だとタイミングの問題が起こることになる。 今回作ったのはプロダクトレベルでは使えないものだが、こういうことはやろうとしてみて気がつく事や、基本的な理解が進んだりする。 これからもどんどん試していきたい。

Code ClimateでGPAを4.0にした

 Code ClimateでGPAを表記上4.0にした。 静的解析ツールはリソース監視と同様に常に掛けておいて、コードの質をチェックしていかないと行けない。 前回の「Ruby on Railsでサービスを作ってみた」メモのコードを静的解析を掛けて、いくつかissueがあったので修正した。 基本的には「Complexity」の修正だけだった。 前のSTIで修正した部分もあるが、今回はそれ以外の修正した例をメモしていく。

主な改修パターン

  1. 記述の簡潔にする
  2. メソッド分けを行う
  3. データ構造を見直す

 記述を簡潔にするのは、言語としての書き方や、ライブラリの機能をきちんと活用することで実現することが出来る。 冗長的に書いてしまっている部分のリファクタリングや、データ構造の見直しを行うことになる。

app/models/multiple_choice.rb

全体

全体before
  def sel_sum i
    r = []
    s = {}
    self.choices.each {|c| s[c[:value].to_s] = 0}
    self.answers.joins(:collaborator).where(collaborators: {status: 1}).each do |a|
      begin
        JSON.parse(a.text).each {|v| s[v] += 1}
      rescue
        if a.text.present?
          s[a.text] = 0 if s[a.text].blank?
          s[a.text] += 1
        end
      end
    end
    self.choices.each do |c|
      r << (s[c[:value].to_s].present? ? s[c[:value]] : 0)
    end
    r
  end
全体after
  def sel_sum
    s = Hash.new 0
    self.answers.joins(:collaborator).where(collaborators: {status: 1}).pluck(:text).each do |t|
      JSON.parse(t).each {|v| s[v] += 1}
    end
    self.choices.inject [] {|r, c| r << s[c[:value]]}
  end

 だいぶ短くなっているが、記述的に短く書いた部分とHashのデフォルト値を使った事によって簡潔にしている。 rubyのHashにはnewで初期値を渡す事ができ、存在しないキー値でアクセスされた場合に初期値を返すように出来る事を利用した。

初期化処理

初期化処理before
self.choices.each {|c| s[c[:value].to_s] = 0}

 この初期化部分が以下で済むようにした。

初期化処理after
s = Hash.new 0

 もちろん厳密に言えばこれは等価ではない。コントロールされた値だけを0にしているのではなく、どんな値でも0で返すからだ。

ループ部分

ループ部分before
self.answers.joins(:collaborator).where(collaborators: {status: 1}).each do |a|
  begin
    JSON.parse(a.text).each {|v| s[v] += 1}
  rescue
    if a.text.present?
      s[a.text] = 0 if s[a.text].blank?
      s[a.text] += 1
    end
  end
end
ループ部分after
self.answers.joins(:collaborator).where(collaborators: {status: 1}).pluck(:text).each do |t|
  JSON.parse(t).each {|v| s[v] += 1}
end

 この部分は単一選択と処理を共通化させたいと思っていた部分があったが、ここに来る時点では複数選択なのが確定しているので処理を簡潔にできた。 ついでにtextカラムしか使っていないのでpluckして性能改善もした。 Hashのデフォルト値を与えているのでいきなり「+= 1」出来る。
 pluckの後にeachせずにmapでJSON.parseして、flattenしてからeachでインクリメントしても良かったが、なんとなく自重した。
 Active Recordの部分はモデル側に書いておいた方が見通しが良かったかも知れない。

結果作成ループ部分

結果作成ループ部分before
r = []
self.choices.each do |c|
  r << (s[c[:value].to_s].present? ? s[c[:value]] : 0)
end
r
結果作成ループ部分after
choices.inject [] {|r, c| r << s[c[:value]]}

 よくArrayを外で宣言して、ループ内で何かしらのデータを突っ込んでそのArrayを返す処理。 injectメソッドを利用し初期値に空のArrayを渡す。そしてinjectの結果をそのままメソッドの戻り値とすることで、外側の変数が不要となった。 ここでも同じくHashのデフォルト値のおかげで三項演算子の利用がなくなっている。

app/controllers/answer_controller.rb

全体

全体before
class AnswerController < ApplicationController
  def create
    s = Surveys.current_group(request.subdomain).eager_load(:questions, :collaborators).find_by({
      id: params[:survey_id],
      questions:     {id:   params[:question_id]},
      collaborators: {uuid: session[:uuid]}
    })
    q = s.questions.first
    c = s.collaborators.first

    if params[:answer].present?
      a = c.answers.where(question_id: q.id).first_or_initialize answer_params
      # override text if question is multiple or date
      a.text = params[:answer][:text].to_json if q.multiple? || q.date?
      a.save!
    else
      # if no answer pathed destroy all answers for this question.
      c.answers.where(question_id: q.id).destroy_all
    end

    return redirect_to survey_question_path(s, q.next) if q.next
    c.done!
    redirect_to finish_survey_path
  end

  alias_method :update, :create

  private
  def answer_params
    params.require(:answer).permit :text
  end
end
全体after
class AnswerController < ApplicationController
  before_action :find_info, only: [:create, :update]

  def create
    @answer.attributes = answer_params
    # override text if question is multiple or date
    @answer.text = params.fetch(:answer, {})[:text].to_json if @question.multiple? || @question.date?
    @answer.save!

    return redirect_to survey_question_path(@survey, @question.next) if @question.next
    @collaborator.done!
    redirect_to finish_survey_path
  end

  alias_method :update, :create

  private
  def answer_params
    params.fetch(:answer, {text: ''}).permit :text
  end

  def find_info
    @survey = Surveys.current_group(request.subdomain).my_answer params, session[:uuid]
    @question = @survey.questions.first
    @collaborator = @survey.collaborators.first
    @answer = @collaborator.answers.where(question_id: @question.id).first_or_initialize
  end
end

 before_actionを利用してActive Record部分のメソッド分けを行った。 当初はDBアクセス処理が一箇所だけだったので分けていなかったが、createメソッドを簡潔にするためにbefore_actionにした。 単なるメソッドわけにしても良かったが、そうすると呼び出しを書かなければならなくなるのを嫌った。

パラメータハンドリング部分

パラメータハンドリング部分before
if params[:answer].present?
  a = c.answers.where(question_id: q.id).first_or_initialize answer_params
  # override text if question is multiple or date
  a.text = params[:answer][:text].to_json if q.multiple? || q.date?
  a.save!
else
  # if no answer pathed destroy all answers for this question.
  c.answers.where(question_id: q.id).destroy_all
end
パラメータハンドリング部分after
@answer.attributes = answer_params
# override text if question is multiple or date
@answer.text = params.fetch(:answer, {})[:text].to_json if @question.multiple? || @question.date?
@answer.save!

 パラメータが渡ってこない場合(1つも選択肢を選ばなかった場合)の為にif文を入れていて、渡ってきていなかったらdelete、渡ってきたらupdate or insertを行っていた。 レコードを消す必要は特になかったので、save!に集約し、パラメータが渡ってこない場合はfetchを使いデフォルト値を返すようにし分岐を消した。 Strong Parametersもrequireではなくfetchに変更している。

Strong Parameters部分before
params.require(:answer).permit :text
Strong Parameters部分after
params.fetch(:answer, {text: ''}).permit :text

まとめ

 GPA4.0に維持しようとすると、思ったよりも厳しくメソッドを簡潔にしないといけないと感じた。 ただ、これぐらい簡潔にすることを心がけるのはメリットも大きい。 より良い記述方法は無いのかと考えるきっかけにもなるからだ。 その場合に単純なメソッド分けに頼るのではなく、言語の仕様、ライブラリの使い方やデフォルト値の考えを導入することをしっかりと検討する方が良いだろう。

Ruby on Railsでサービスを作ってみた おまけ

 実装周りで補足した方が良いことを思いついてしまったのでおまけとしてメモ。いつもの様に過去記事はこちらからどうぞ。(part1part2part3part4part5part6

active_decorator

 今回active_decoratorを使ってFat Modelになるのを防いでいる。active_decoratorはビューで使うメソッドを別ファイルで管理することが出来るようにしてくれる。使い方も簡単でrakeで対象のモデルをジェネレートしてそこにロジックを記載していくだけ。

./bin/rails g decorator question

 こうすると以下のmoduleが作成される。

module QuestionDecorator
end

 この中にガシガシメソッドを書いていくわけだが、コンテキストとしてはモデルなのでモデルのattributesやメソッドにはアクセスすることが出来る。特徴として、モデル側に何の処理も書かなくても勝手に追加される。その代わり命名ルールに従う必要がある。そして、ビューの時しかデコレートされない。これはコントローラからビューに渡す時にフックしてデコレートしているからだ。コントローラから使うメソッドは元のモデルに記述する必要がある。
 そして親子関係のモデルの場合は、自動的に子供のモデルはデコレートされず、renderメソッドに渡した時にデコレートされる。つまりrender partialで使うのが自然な流れというわけだ。
 これはビューが大きくなるのを防ぐ意味がある。親をレンダリングするテンプレートと子供レンダリングするテンプレートを分けることになるからだ。子供のテンプレートが再利用出来ればなお良しというところだろう。

STIとの組み合わせ

 ここまでは普通のactive_decoratorを使っているだけなのだが、今回Single table inheritance(STI)と組み合わせて使ってみた。STIは単一継承テーブルで、DBのテーブル的には1つだが、特定のカラムの値によってモデルクラスを変える仕組みだ。何が嬉しいのかというと、モデルの中のメソッドで自分がどういう情報かによって分岐を入れることがある。今回のsurveyの場合で言うと質問がそれにあたる。回答が単一選択なのか複数かのか、フリーフォーマットなのか日付選択なのかが別れるわけだ。質問モデル1つでだけであると、メソッドの中で分岐を入れる必要が出てきて、メソッドが複雑化してしまう。最初からクラス分けをしておけば、分岐はしなくてよくなるということである。
 今回の例で言うと以下の様な継承関係になる。それぞれ特有の処理はそれぞれのクラスの中に書けばいいわけだ。共通のメソッドは親クラスに記述する。

class Question < ActiveRecord::Base
end

# 単一選択
class SingleChoice < Question
end

# 複数選択
class MultipleChoice < Question
end

# 日付選択
class DateChoice < Question
end

# フリーフォーマット
class FreeChoice < Question
end

 デフォルトではtypeというstringカラムを追加すればSTIとして認識される。そこに入る値としてはクラス名が入る。子クラスで検索をかければ同じクラスだけが取得出来、親クラスで検索すれば全パターンの種類が取れる。

# 親クラスで検索
Question.all
=> #<ActiveRecord::Relation [#<MultipleChoice id: 42, surveys_id: 1, no: 1, text: "好きなスポーツ", type: "MultipleChoice", value: "[{\"value\":\"1\",\"text\":\"野球\"},{\"value\":\"2\",\"text\":\"サッ...", image: nil, created_at: "2015-09-24 19:10:02", updated_at: "2015-09-30 08:51:01">, #<SingleChoice id: 43, surveys_id: 1, no: 2, text: "好きなフルーツ", type: "SingleChoice", value: "[{\"value\":\"1\",\"text\":\"りんご\"},{\"value\":\"2\",\"text\":\"み...", image: "88d78f8d-b99e-417c-b976-c7fb3784fb93.jpg", created_at: "2015-09-24 19:10:02", updated_at: "2015-09-30 08:51:1:13">, ...]>

# 子クラスで検索
SingleChoice.all
=> #<ActiveRecord::Relation [#<SingleChoice id: 43, surveys_id: 1, no: 2, text: "好きなフルーツ", type: "SingleChoice", value: "[{\"value\":\"1\",\"text\":\"りんご\"},{\"value\":\"2\",\"text\":\"み...", image: "88d78f8d-b99e-417c-b976-c7fb3784fb93.jpg", created_at: "2015-09-24 19:10:02", updated_at: "2015-09-30 08:51:01">, #<SingleChoice id: 60, surveys_id: 16, no: 1, text: "好きなドリンクは?", type: "SingleChoice", value: "[{\"value\":\"1\",\"text\":\"コーラ\"},{\"value\":\"2\",\"text\":\"オ...", image: nil, created_at: "2015-09-25 14:47:48", updated_at: "2015-09-25 14:47:48">, #<SingleChoice id: 61, surveys_id: 15, no: 1, text: "ほげもげ", type: "SingleChoice", value: "[{\"value\":\"1\",\"text\":\"hoge\"},{\"value\":\"2\",\"text\":\"...", image: nil, created_at: "2015-09-26 04:52:16", updated_at: "2015-09-26 04:52:16">, ...]>

 さて、どちらもFat Modelを回避するために使用出来るのだが、アプローチや考え方が違う。STIでのクラスは別クラスと言ってもモデルクラス(たまたま同じテーブルを指している)なので、親クラスのデコレートmoduleに書いたことは使えない。つまり

module QuestionDecorator
  def hello
    'hello'
  end
end

# questionがSingleChoiceクラスのインスタンスだとする
# slimテンプレートで呼び出せない
= question.hello

 なのでデコレータはSTI毎に作ることで回避した。だが、共通の処理とかもあるのでそれは親のデコレータに記述し、子デコレータが親をincludeする形で対応した。

module QuestionDecorator
  # 共通のメソッドを記述
end

# 単一選択デコレータ
module SingleChoiceDecorator
  include QuestionDecorator
end

# 複数選択デコレータ
module MultipleChoiceDecorator
  include QuestionDecorator
end

# 日付選択デコレータ
module DateChoiceDecorator
  include QuestionDecorator
end

# フリーフォーマットデコレータ
module FreeChoiceDecorator
  include QuestionDecorator
end

 ここまで分ける必要があるのかというのはあるが、こうすることでFat Model化はだいぶ防ぐことが出来る。(逆に理解していないとどこに書けばいいのかわからなくなりそうだが・・・)

enumについて

 STIを使わない場合はactiverecordenumをtypeに定義することで便利なメソッドが使えていた。良くあるのが判定と更新処理だ。

if question.single?
  # 単一選択の場合の処理
end

# 単一選択にtypeを設定して更新する
question.single!

 このメリットを失いたくなかったのでenum定義自体は残すことにした。

  enum type: {
    single:   'SingleChoice',
    multiple: 'MultipleChoice',
    date:     'DateChoice',
    free:     'FreeChoice',
  }

 こうすることでenumでできていた事を失わずにSTIのメリットも享受出来る。ただ、クラス名が重複してしまいDRYの原則からは外れている。STI側で上手いこと対応してくれないものだろうか。

まとめ

 組み合わせた場合に記述する方針としては以下のようになる。

場所 記述内容
STI 親クラス コントローラ、ビューで使う共通処理
STI 子クラス コントローラ、ビューで使う個別処理
親クラス デコレータ ビューだけで使う共通処理
子クラス デコレータ ビューだけで使う個別処理

 STIオブジェクト指向としては非常にマッチした設計の一つだ。今回はそれを使いつつ、ビューの処理を分けるactive_decoratorと組み合わせて使うことでFat Model退治を行うことが出来た。今回のように両方使わなくても、どちらか片方だけでも使うだけでも十分効果を発揮するだろう。

Ruby on Railsでサービスを作ってみたpart6

 今回で6回目「Ruby on Railsでサービスを作ってみた」記事投稿。過去記事も増えてきましたが読みたい方はこちらからどうぞ。(part1part2part3part4part5)今回のインフラ周りの話でこのシリーズは一応完結。(挨拶)

サーバ構成

 AWSをインフラとしては利用しているが、CloudFormationやTerraformは使わず、せっせと自分で色々構築している。VisualOpsで出力した結果は以下。

f:id:wata_htn:20151001172336p:plain

サブネット

 Multi-AZにするためにap-northeast-1b、ap-northeast-1cにそれぞれprivateサブネットとpublicサブネットを作成。第3オクテット目で分けていて0-9はpublicサブネット、10-19はprivateサブネットとした。

AZ publicサブネット privateサブネット
ap-northeast-1b 10.0.0.0/24 10.0.10.0/24
ap-northeast-1c 10.0.1.0/24 10.0.11.0/24

踏み台サーバ

 sshは直接サーバへはさせず、必ず踏み台サーバを経由するようにしている。SecurityGroupを更新するWebアプリを作成し、クライアント証明書が入っているブラウザからならどこからでも接続出来るようにしている。かつ、scriptコマンドによるコマンドをログに必ず残すようにもなっている。以前開発合宿で作った資料から抜粋。

f:id:wata_htn:20151001173855p:plain

f:id:wata_htn:20151002000116j:plain

 ただ、アカウント管理周りはLDAP等は用意していないので、この辺りは改善点ではある。

NATサーバ

 特に語ることもないAWSが用意しているAMIを使って構築。privateサブネットからの通信は内部IP以外はNATを通す様にルーティングを設定しておく。まあ、強いて上げれば「送信元/送信先の変更チェック」を外しておくぐらい。

Webサーバ

 Auto Scalingは設定していない。privateサブネットに配置。外からアクセスを受け付けるのと、sslの解除はELBに任せている。基本的にはWebサーバはhttpのプロトコルで受けることになる。なので場合によってはリダイレクトの設定はnginxに入れないと行けない場合がある。
 インスタンスを再起動するとELBから上手く繋がらなくなるので、復帰スクリプトも仕込んでおく。
 プロビジョニングについてはitamaeを使って、設定を反映させている。

画像配信サーバ

 S3からの画像をsmall_lightを通してリサイズするリバースプロキシとなっている。CDNを挟むことを基本イメージしている。本来はサブドメインでやりたかったがが、今回のサービスとしてはサブドメインでグループ分けを行う想定の為、Route53は使わずCloudFrontのドメインそのままを利用することにした。別ドメインなら無駄なCookieとかの送信もなくなるので良いかなということで今回は総判断した。
 画像に関しては、アップロードされた時にリサイズ処理を掛けるのと、アクセスが来た時にリサイズするパターンがある。アップロードされた時にリサイズをすると、画像の表示サイズを変えたい、追加したい場合にすべての画像を変換し直す必要が出てきてしまう。その為にアクセス時にリサイズし、そのサイズがあればキャッシュを返すパターンが望ましい。ただ、キャッシュがない場合に同時にアクセスが来た場合の処理を気にしないとエラーになってしまうこともあるので注意が必要。

f:id:wata_htn:20151002131522j:plain

 というかローカル開発ではDockerで画像配信サーバをまかなっているので、本番ではEC2 Container Serviceを試して見るのもありだったかも。

RDS

 個人で作っているのも有り今回はMulti-AZにはせず。文字コードを設定した以外は特筆すべき事も無い。

Circle CI

 本番環境リリース用にCircle CIからCodeDeployへ連携の設定をしている。bundle installはキャッシュしてかつ並列実行指定。一人プロジェクトだったのもあり、Slackは今回使っていない。なのでCircle CIの通知はCCMenuを使って通知を受けるようにしている。Slackが無いとChatOpsが出来ないのもやはり面倒なので、一人でも導入すべきだったかも知れない。

circle.yml

dependencies:
  cache_directories:
    - "vendor/bundle"
  override:
    - bundle -j4 --path=vendor/bundle

CodeDeploy

 今回はCapistranoは使わず、CodeDeployにしてみた。当たり前の話だがデメリットとしてAWS以外の環境にリリースすることになったら、ある程度script下のシェルが使えるがちょっと面倒そう。今回のリリースを図にするとこんな感じ。CodeDeployが東京リージョンに来たので、S3→Lambda→EC2からS3ファイルを取得→リリースということをやらなくて良くなったのは便利。
 ビルドに関してはzipファイルでS3にあげているが、古いビルドの削除はバケットのライフサイクルの設定を入れて任せている。

f:id:wata_htn:20151002130359j:plain 

 appspec.ymlの内容としては/opt/survey下にリリースして、Lifecycleでシェルをキックしてunicornを再起動しているだけだ。AfterInstallでridgepoleのapplyをしているが本来はこれはしない方が良いだろう。複数台サーバがある場合は何回も走ってしまうし、何よりDB反映のタイミングは遡及とかもありコントロールしたいだろう。

version: 0.0
os: linux
files:
  - source: /
    destination: /opt/survey
permissions:
  - object: /opt/survey
    pattern: "**"
    owner: webservice
    group: webservice
    mode: 755
hooks:
  AfterInstall:
    - location: script/codedeploy_bundle
      timeout: 300
      runas: root
  ApplicationStart:
    - location: script/codedeploy_start
      timeout: 300
      runas: root

NewRelic

 何かトラブルがあった場合にリソース情報の取得、可視化しておくことは非常に重要。かつ、開発時も常に自分のサービスのリソース使用状況は把握しておくのが良い。特にメモリやディスクは日頃から監視しておかないとサービス停止にも繋がりかねない。その為にNewRelicを入れている。無料でも基本的なリソースの監視はできるが、有料にすれば、アプリの細かいところまで見れるようになる。パフォーマンスチューニングの際に役に立つだろう。アプリでスループットをきちんと出すには、リソースを監視しながらやらないと、勘で修正してもなかなかうまくいかない。「推測するな、観測せよ」は鉄則。

script

 インフラというわけではないが、プロジェクト毎のコマンド等をscriptディレクトリにまとめるというGitHub Engineeringブログを見て、これはいいかもと思って適用している。特に決まったルールは無いが、GitHub:github/sripts-to-rule-them-allから持ってくるのもいいだろう。

やっていないこと

 やっていない事としては、プロセス監視、ログ監視、プロビジョニングの自動化(今は踏み台サーバから各サーバをitamaeしている)等だ。プロビジョニングの自動化についてはAMI化して、新しく再リリースがクラウドっぽいがCodeDeployを使ってみるのもありかと一瞬思ったがやはりそれは無いな。リソース監視および可視化もまだまだしていない部分もある。バックアップもきちんとしていないので、このままデータが吹き飛ぶと復旧には手間がかかることになる。

まとめ

 part6まで掛けて「Ruby on Railsでサービスを作ってみた」のメモを書いてみた。手順をすべて記載しているわけではないのでハウツーにはならないが、本ブログをきっかけに調べなければいけないこと、勉強しなければいけないことのとっかかりには出来るかもしれない。これから初めて「一からサービスを作ってみよう」と思う人の参考になれば幸いだ。

次回について

 冒頭に「一応」完結と書いたのは、実装周りでもう少し補足したいことがあるのでおまけ的に書いてみようと思ったからだ。興味があればお付き合い頂きたい。

Ruby on Railsでサービスを作ってみたpart5

 今回で5回目の「Ruby on Railsでサービスを作ってみた」記事投稿。いつもの様に過去記事はこちらからどうぞ。(part1part2part3part4)画面や機能周りで終わりではなく、開発環境やらインフラ周りのメモも書いていくので引き続きよろしくお願いしたい。(挨拶)

開発環境について

 今回は開発環境について書いていく。開発環境と言ってもpart1とはスコープが違っていて、開発に使っている、エディタやらコマンドやらツールやらの事だ。

OSについて

 開発はすべてMacBook Proで行っている。とは言っても、OS Xに直接rubyやら何やらを入れているわけではなく、VirtualBoxで仮想環境をローカルに作ってその中で開発することを基本としている。これは色々なバージョンのrubyが入ってしまったりmysqlが一杯立ち上がったり、汚れるのを嫌ってのことだ。仮想環境にしておけば、最悪ハード障害になってもバックアップを取っていれば別のマシンでも同じ環境で開発をすることが出来る。2011年頃からこのスタイルで開発を続けている。仮想環境も使いまわしたりせず、案件ごとに作るようにしている。

エディタについて

 Rubyの開発はRubyMineと言いたいところだが、Atomを使って開発をしている。仮想環境内のファイルアクセスについてはsshfsを使ってOS X上にマウントしてアクセスしている。Windowsで開発していた時はSambaを使ってUNCパス経由でアクセスしていたが、OS Xからだと通信が切れたりと相性が良くないので乗り換えた。

コンソールについて

 他を試していないのだがiTerm2を使ってsshアクセスしている。たまにscreenを貼ったり、tmuxを貼ったりしてrack起動画面とgulp watchとgit操作とlogのtail等を分割してやったりもする。画面分割も便利だが、どちらかと言うと仮想環境に繋ぎ直した後に、何個もssh接続してプロセス立ち上げてというのが面倒だからという理由のほうが強い。
 OS X上でよく使うvagrantコマンドはvにaliasしていて、「v ssh」みたいにしている。

シェルについて

 zshバリバリですよ。というわけではなくbashを普通に使っている。git-completion.bashとかgit-prompt.shは必ず入れてgit操作をしている。やはりよく使うコマンドはaliasを切る。「bundle exec」は「be」とか。
 シェルの話ではないが、タスクランナーであるgulpを今回は使わなかった。やはりgulp等はフロントエンド界隈の技術なのかもしれない。Railsでは結果的に同じことを別の手法で解決していて、それがシームレスな開発に繋がっているのが素晴らしい。前も少し感じたことだが、サーバサイドエンジニアにはwatch文化が少ない気がする。もっとファイルの変更をwatchして、色々なことをしていけばコマンドを打たなくて済んだりして開発に集中出来ると思うのだが・・・。まあ、この辺りは自分が知らないだけの可能性があるので今後も調べてみようと思う。

gitについて

 SourceTree等のGUIツールは使わずコマンドベースで作業している。これはCVS(古い!)時代からSubversion等もコマンドラインで操作をしていた。この辺はあまりマウスは使いたく無く、「キーボードで操作を完結させたい!」という思いがあるからかもしれない。タッチパネルだと指が痛くなるのでマウスはMagic Mouseを使っている。
 GitHubを使ってソース管理をしているので素のgitではなくhubを入れてgitにaliasして使っている。いちいちPull Requestを出すためにブラウザを開くのが面倒だったので重宝している。が、仮想環境での操作のため「git browse」が使えない。なのでPull Requestをマージするためには手でブラウザを開いている・・・。(誰か良い解決案あれば教えて下さい。VirtualBoxの機能を使えば通知は出来そうだが・・・。)
 master Pull Requestの作成はgit-pr-releaseを使わせて頂いている。一人開発なのであまり意味は無いが、Pull Requestコメントに変更点がまとまるのはありがたい。

プロビジョニングについて

 Chefを使っていたが、itamaeを知ってからはitamaeで行っている。そんな複雑なことはいつもやらないので十分ということもある。今回はローカル環境とAWS環境はitamaeによってプロビジョニングを行った。反省点としてはローカルはCentOS6.6で、AWSはAmazonLinuxだったのを意識して無かったことか。awsコマンドとかは不要なのでその辺が分けられるようにしても良かったかと思った。プロビジョニングソースはこちらにあります。

CIツールについて

 前はTravis CIを使っていたが、今回の案件でCircle CIを使ったので引き続き使うことにした。単純にdevelopとmasterブランチを監視してmasterの場合はAWSにCodeDeployでデプロイする形になっている。おしゃれなUIはやっぱり必要だと思う。

デプロイツールについて

 capistranoは使わずCodeDeployを使った。せっかくなのでmamiyaも使ってみようかと思ったが今回は見送った。CodeDeployはec2インスタンス管理的な意味で言うと優れているが、実際のデプロイ周りが少し微妙な気がする・・・。まあLifecycleでhookすれば何でも出来るので問題は無いのだが、もっと痒いところに手が届く感じにしてもらいたいものだ。

次回について

 次回はいよいよインフラ周りの話について書いていく予定。1回で書ききれるのかはわからないが。