blog.waterlow.work

Ruby, Rails, js, etc...

Rails アンチパターン - ファイア・アンド・フォーゲット(Fire and Forget)

引き続きRails AntiPatternsという本を読んでいます。

https://www.amazon.co.jp/dp/B004C04QE0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

前回は4 Controllersの2つめ「Fat Controller」についてまとめました。 waterlow2013.hatenablog.com

今回は5 Servicesの1つめ「Fire and Forget」についてまとめていきます。 ここで言うServicesとは外部サービスのことで、サービスクラスやアプリケーションサービスのことではありません。

Fire and Forget

ファイア・アンド・フォーゲット - Wikipedia

外部サービスを利用するとき、レスポンスの扱いとしては以下のようなものがある。

  • レスポンスをチェックし、エラーごとに適切な処理を行う。
  • 成功、エラーのみチェックする。
  • 何もチェックしない。

今回のアンチパターンは3つ目のケース。コントローラで直接呼んでいるとなるとユーザに500エラーを返すことになってしまう。

Solution: Know What Exceptions to Look Out For

発生するエラーを列挙しておき、適切にハンドリングする。たとえばHTTPクライアントなら以下のようなエラーがある。

HTTP_ERRORS = 
  [Timeout::Error,
   Errno::EINVAL,
   Errno::ECONNRESET,
   EOFError,
   Net::HTTPBadResponse,
   Net::HTTPHeaderSyntaxError,
   Net::ProtocolError]

rescue => eしないのは、Timeout::ErrorRuntimeErrorを継承していないためらしいが、今ならRuntimeErrorを継承しているので、雑にやりたければrescue => eでもいいと思う。

Message in a Bottle

  • config.action_mailer.raise_delivery_errorsの設定に気をつける
  • エラーには大きく分けて2種類ある。クライアント側とサーバ側
  • クライアント側のエラー(メールアドレスがおかしい等)はユーザに即通知する
  • サーバ側のエラーはスタッフに通知する等して、何か対応を入れる。

You Don’t Know What You Don’t Know

  • rescueしすぎない
  • 少なめのrescueから初めて必要なものを追加する
  • エラーログサービス(NewRelic, errbit等)も重要
  • エラーハンドリングの詳しくはchapter10を

まとめ

特に違和感を感じたところはなかったです!外部サービスを叩くものは常にエラーが起こりうるという扱いをするのが自然なんだなと思いました。
あとは早めにchapter10を読みたい。

Rails アンチパターン - ファットコントローラー(Fat Controller)

引き続きRails AntiPatternsという本を読んでいます。

https://www.amazon.co.jp/dp/B004C04QE0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

前回は2 Modelsの1つめ「Voyeuristic Models」についてまとめました。

waterlow2013.hatenablog.com

今回は4 Controllersの2つめ「Fat Controller」についてまとめていきます。 Solutionが2つあるのですが、今回は1つ目について書きます。

Fat Controller

コントローラーからビジネスロジックを削除し、それをモデルに適切に配置します。
コールバック、セッター、データベースのデフォルトなど、Active Recordによって提供される機能は、このタスクの重要なツールセットです。

Solution: Use Active Record Callbacks and Setters

サンプルコード

class ArticlesController < ApplicationController
  def create
    @article = Article.new(params[:article])
    @article.reporter_id = current_user.id
    begin
      Article.transaction do
        @version = @article.create_version!(params[:version], current_user)
      end
    rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
      render :action => :new and return false end
    end
    redirect_to article_path(@article)
  end
end

コントローラで手続き型っぽく書いてしまっているというざっくりとした理由の他に、以下のような具体的理由があります。

  • transactionをコントローラで呼んでいる
  • saveが使われていない
    • transactionを貼らないといけない?
  • 例外を制御フローに使っている
    • 例外的状況ではない
    • GOTO文と可読性が変わらない

目標

class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to article_path(@article)
    else
      render :action => :new
    end
  end
end

ActiveRecordのsaveメソッドの戻り値で判断します。scaffoldで作られたものと同じですね。
create_version!メソッドを消す必要があるので、見ていきましょう。

def create_version!(attributes, user)
  if self.versions.empty?
    return create_first_version!(attributes, user)
  end

  # mark old related links as not current
  if self.current_version.relateds.any?
    self.current_version.relateds.each do |rel|
      rel.update_attribute(:current, false)
    end
  end

  version = self.versions.build(attributes)
  version.article_id = self.id
  version.written_at = Time.now
  version.writer_id = user.id
  version.version = self.current_verison.version + 1
  self.save!
  self.update_attribute(:current_version_id, version.id)
  version
end

def create_first_version!(attributes, user)
  version = self.versions.build(attributes)
  version.written_at = Time.now
  version.writer_id = user.id
  version.state ||= "Raw"
  version.version = 1
  self.save!
  self.update_attribute(:current_version_id, version.id)
  version
end

必要に応じて他のメソッドversionモデルも見ていきます。

消せるものを消す。

  • version.written_at = Time.nowはcreated_atとupdated_atで代用できる。
  • version.state ||= "Raw"はDBのデフォルト値を使う(必要ならgemをつかってもいい)
  • version.article_id = self.idself.versions.buildで勝手に入る。
def create_version!(attributes, user)
  if self.versions.empty?
    return create_first_version!(attributes, user)
  end

  # mark old related links as not current
  if self.current_version.relateds.any?
    self.current_version.relateds.each do |rel|
      rel.update_attribute(:current, false)
    end
  end

  version = self.versions.build(attributes)
  version.writer_id = user.id
  version.version = self.current_verison.version + 1
  self.save!
  self.update_attribute(:current_version_id, version.id)
  version
end

def create_first_version!(attributes, user)
  version = self.versions.build(attributes)
  version.writer_id = user.id
  version.version = 1
  self.save!
  self.update_attribute(:current_version_id, version.id)
  version
end

コールバックをつかう

コールバックにするのは以下の3つ。

  • version.versionの設定
  • current_version.relatedsのupdate
  • self.update_attribute(:current_version_id, version.id)

version.versionの設定

class Version < ApplicationRecord
  before_validation :set_version_number, on: :create
  validates :version, presence: true

  private

  def set_version_number
    self.version =
      (article.current_version ?
        article.current_version.version : 0) + 1
  end
end

self.versions.empty?self.current_versionの存在確認でOK。create_first_version!メソッドが必要なくなるので。以下のようになる。

def create_version!(attributes, user)
  # mark old related links as not current
  if self.current_version && self.current_version.relateds.any?
    self.current_version.relateds.each do |rel|
      rel.update_attribute(:current, false)
    end
  end

  version = self.versions.build(attributes)
  version.writer_id = user.id
  self.save!
  self.update_attribute(:current_version_id, version.id)
  version
end

* current_version.relatedsのupdate

class Version < ApplicationRecord
  before_validation :set_version_number, on: :create
  before_create :mark_related_links_not_current, 
                          if: :current_version
  validates :version, presence: true

  private

  def current_version
    article.current_version
  end

  def set_version_number
    self.version = (current_version ? current_version.version : 0) + 1
  end

  def mark_related_links_not_current
    current_version.relateds.each do |rel|
      rel.update_attribute(:current, false)
    end
  end
end

途中細かなリファクタリングをはさんでいますが、最後の結果だけ書いています。

def create_version!(attributes, user)
  version = self.versions.build(attributes)
  version.writer_id = user.id
  self.save!
  self.update_attribute(:current_version_id, version.id)
  version
end

だいぶスッキリしてきました!

self.update_attribute(:current_version_id, version.id)

class Version < ApplicationRecord
  before_validation :set_version_number, on: :create
  before_create :mark_related_links_not_current, 
                          if: :current_version
  after_create :set_current_version_on_article
  validates :version, presence: true

  private

  def set_current_version_on_article
    article.update_attribute :current_version_id, self.id
  end

  def current_version
    article.current_version
  end

  def set_version_number
    self.version = (current_version ? current_version.version : 0) + 1
  end

  def mark_related_links_not_current
    current_version.relateds.each do |rel|
      rel.update_attribute(:current, false)
    end
  end
end

メインのデータ更新以外は全てコールバックに、という流れですね。 create_version!消すまではもう一息という感じです。

def create_version!(attributes, user)
  version = self.versions.build(attributes)
  version.writer_id = user.id
  self.save!
  version
end

create_version!を消す

ここでcreate_version!を消します。 create_version!versionに関するコードはコントローラに移します。

class ArticlesController < ApplicationController
  def create
    @article = Article.new(params[:article])
    @article.reporter_id = current_user.id
    @version = self.versions.build(attributes)
    @version.writer_id = current_user.id

    if @article.save
      redirect_to article_path(@article)
    else
      render action: :new
    end
  end
end

これで目標とする形にかなり近づきました。

その先

この書籍ではnested_attributesを使う方法が書かれていました。最近のRailsバージョンだとaccept_nseted_attributes_forを使うのかと思いますが、実はこの機能、頃合いを見て消される方針のようです。

https://github.com/rails/rails/pull/26976#discussion_r87855694

代替としては、自分が知っているのはFormオブジェクト使うやり方があるのですが、だいぶ本に載っている内容からはそれるので今回はパスします。 Formオブジェクトの作り方はいろいろあると思いますが、パーフェクトRailsあたりに載っていた気がしますし、有料ですがgorailsにも。

gorails.com

まとめ

fat controller解消は何かパターンがあるわけではなく、リファクタリングとコールバックの積み重ね、最終的にどうしようもない所だけFormオブジェクトにするという泥臭い感じになるのだなと思いました。ただこうゆうことをサクッとできないとfat controllerと付き合うしか無いと思うので、この辺できるようにしておきたいです。
scaffoldに近づけるのが理想的という話はめちゃくちゃ同意できました!

[2017-05-07 20:50追記] 2つ目の解決策であるMove to a Presenterはactive_presenterというgemを使うのですが、今はメンテされていない様子です。内容としては上で言うFormオブジェクトっぽいものを宣言的にくためのgemのようでした。こちらに関しては深入りしないことにします。

Rails アンチパターン - 覗き見モデルクラス(Voyeuristic Models)

はじめに

引き続きRails AntiPatternsという本を読んでいます。

https://www.amazon.co.jp/dp/B004C04QE0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

前回は2 Domain Modelingの1つめ「Authorization Astronaut」についてまとめました。

waterlow2013.hatenablog.com

今回は1 Modelsの1つめ「Voyeuristic Models」についてまとめていきます。

Voyeuristic Models

Ruby on RailsMVC、およびオブジェクト指向プログラミングが提供する構造を認識していない場合などに、オブジェクト指向の基本的な原則を破ってしまうことがあります。またActiveRecordなどの便利な機能に乗りすぎてしまう場合もあります。
ここでは、MVCオブジェクト指向プログラミングの教えに違反するいくつかのサンプルを見ていきます。

Solution: Follow the Law of Demeter(デルメルの法則に従う)

デメテルの法則 - Wikipedia

サンプルで見ていこうと思います。以下のようなモデルがあるとします。

class Address < ApplicationRecord
  belongs_to :customer
end
class Customer < ApplicationRecord
  has_one :address
  has_many :invoices
end
class Invoice < ApplicationRecord
  belongs_to :customer
end

顧客は住所を1つ、請求を複数持ちます。請求の住所行を表示するビューは、次のようになります。

<%= @invoice.customer.name %>
<%= @invoice.customer.address.street %>
<%= @invoice.customer.address.city %>,
<%= @invoice.customer.address.state %>
<%= @invoice.customer.address.zip_code %>

適切なカプセル化を考えると、@invoiceはcustomerを経由してaddressにアクセスするべきではありません。たとえば、顧客が請求先住所と配送先住所の両方を持つようにアプリケーションを変更すると、これらのオブジェクトを通過して通りを取得するコード内のすべての場所が壊れ、変更する必要があるためです。

幸いにも、ActiveRecordには、delegateというメソッドがあります。このデリゲートメソッドを使用すると、addressオブジェクトのことを知ることなく顧客の住所を表示できます。

class Address < ApplicationRecord
  belongs_to :customer
end

class Customer < ApplicationRecord
  has_one :address
  has_many :invoices
  delegate :street, :city, :state, :zip_code, to: :address
end

class Invoice < ApplicationRecord
  belongs_to :customer
  delegate :name, :street, :city, :state, :zip_code, to: :customer, prefix: true
end

viewのコードは以下のようになります。

<%= @invoice.customer_name %>
<%= @invoice.customer_street %>
<%= @invoice.customer_city %>,
<%= @invoice.customer_state %>
<%= @invoice.customer_zip_code %>

これやったほうがいいとは思うのですが、関連するオブジェクトをそのまま取って出すだけなら最初のままでもいい気がします。ただ悩ましいところではある…。これちょっと修正したいときに.gsubとかviewに書いてしまう事があって、それを助長する気がしています。そしてDRYでない子0ドが生まれていくという。なので1回これに従ってみるのもいいかなと思えてきました。

Solution: Push All find() Calls into Finders on the Model(生のクエリメソッドは全てモデルに書く)

これもサンプルを見ます。

<html>
  <body>
    <ul>
      <% User.order(:last_name).each do |user| -%>
        <li><%= user.last_name %> <%= user.first_name %></li> <% end %>
    </ul>
  </body>
</html>

これはビューに直接sql呼び出しが書いている状態であり、重複するロジックがそれぞれの場所で生まれてしまう可能性があります。
ひとまずコントローラに移します。

class UsersController < ApplicationController
  def index
    @users = User.order(:last_name)
  end
end
<html>
  <body>
    <ul>
      <% @users.each do |user| -%>
        <li><%= user.last_name %> <%= user.first_name %></li> <% end %>
    </ul>
  </body>
</html>

これでモデルのロジックはなくなりましたが、コントローラにロジックの記載が残っています。これをモデルに移します。

class User < ApplicationRecord
  scope :ordered, -> { order(:last_name) }
end
class UsersController < ApplicationController
  def index
    @users = User.ordered
  end
end

Solution: Keep Finders on Their Own Model (生のクエリメソッドは自身のクラスにしか書かない)

検索コールをRailsアプリケーションのコントローラレイヤーからモデル上のカスタムファインダに移動することは、メインテナンス可能なソフトウェアを作成する正しい方向への強力なステップです。しかし、一般的な間違いは、適切な責任の委任を無視して、それらの呼び出しを最も近いモデルに移動することです。

耳が痛い話だ!!!

class UsersController < ApplicationController
  def index
    @user = User.find(params[:id])
    @memberships =
      @user.memberships.where(active: true).
        limit(5).
        order("last_active_on DESC")
  end
end

今まで学んだことに基づいて、そのスコープチェーンをUserモデルのメソッドに徹底的に移動します。UsersControllerを扱っているので、これが最良の場所のようだと思います。

class UsersController < ApplicationController
  def index
    @user = User.find(params[:id])
    @recent_active_memberships = @user.find_recent_active_memberships
  end
end
class User < ApplicationRecord
  has_many :memberships

  def find_recent_active_memberships
    memberships.where(active: true).
      limit(5).
      order('last_active_on DESC')
  end
end

これは間違いなく改善です。 しかし、もう少しすることができます。Userモデルはメンバーシップモデルの実装について、active、last_active_onというカラムのことを知ってしまっています。これは、まだメソッドを十分にプッシュしていないという手がかりです。

(途中経過を省き最終型のみ載せます)

class User < ApplicationRecord
  has_many :memberships

  def find_recent_active_memberships
    memberships.only_active.order_by_activity.limit(5)
  end
end

class Membership < ApplicationRecord
  belongs_to :user

  scope :only_active, where(active: true)
  scope :order_by_activity, order('last_active_on DESC')
end

scopeにしたことによりロジックを組み合わせて使うことができます。

scopeにしたことが良いのと、limit(5)がUserモデルにあるのがすごくいい感じがしています。

このアプローチには、読みやすさとシンプルさの問題だけでなく、デメテルの法律の乱用などの短所もあります。
このアプローチを使用するかどうかは、あなた次第です。多くの高度なリファクタリングと同様に、これは簡単な答えがなく、好みにも大きくよるものです。

まとめ

正直ここまで徹底してやったことがなく、これを都度やっていくコストがどれくらいなのかわかりません。ただ、毎回コントローラに書いていってしまうと共通化、scope化するのがどんどん難しくなっていくので、ある程度の規模ではやるべきなのかなと思っています。モデル50個からとか?
とりあえず、次自分で何か書くときにはこれ徹底してみようと思います。

Rails アンチパターン - 使われない権限メソッド(Authorization Astronaut)

はじめに

最近Rails AntiPatternsという本を読んでいました。

https://www.amazon.co.jp/dp/B004C04QE0/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

幾つかの項目を読んだ所なかなかおもしろそうだったので一冊通して読もうと思っています。
翻訳版はないので、ざっくり訳しつつ自分の感想を書いていこうと思います。 2010年に発売されたもので、サンプルコードやgem等は古いので適宜読み替える必要があります。
また、サンプルコードは一部変更して掲載しています。

Authorization Astronaut

権限ロジックは仕様に基づいて、または将来の要件を予期してプログラムされます。その結果、一般的なユーザー認証では、次のようなユーザー・モデルが作られます。

class User < ApplicationRecord
  has_many :user_roles
  has_many :roles, through: :user_roles

  def has_role?(role)
    roles.exists?(name: role)
  end

  def can_post?
    has_roles?(%w(admin editor associate_editor research_writer))
  end

  def can_review_posts?
    has_roles?(%w(admin editor associate_editor))
  end

  def can_edit_content?
    has_roles?(%w(admin editor associate_editor))
  end

  def can_edit_post?(post)
    self == post.user || has_roles?(%w(admin editor associate_editor))
  end
end

can_*というメソッドは破滅への道です。いつ使われるかわからず、インタフェースも曖昧で一貫性がないです。今後も必要になる前から権限ロジックだけ先に追加されるようになります。 また、admin, editor等の権限名がハードコーディングされていて、変更に弱い状態です。

自分もこんな感じのをモデル見たことがあり、ああーなるほどという感じでした。モデルとコントローラを別の人で開発した場合にこんな感じになりがちな印象です。
ハードコーディングやめて定数にすればという人もいますが、本質的には変わらないと思います笑

ユーザにはロールをつけることができます。

class Role < ApplicationRecord
  has_many :user_roles
  has_many :users, through: :user_roles

  validates :name, uniqueness: true, presence: true

  def name=(value)
    write_attribute(:name, value.downcase)
  end

  def self.[](name)
    find_by(name: name.to_s)
  end

  def add_user(user)
    users << user
  end

  def delete_user(user)
    users.delete(user)
  end
end

Roleモデルにはいくつかの問題があります。管理ユーザーがロールを追加または削除できるようにする計画はありません。 また、Role.[]というメソッドは識別子の変更を吸収するのに使われるかもしれませんが、その意図で書かれていないため、機能しません。どこでも使われない可能性もあります。
add_user、delete_userも良いインタフェースではありません。

あるあるだw

要するに、これら2つのモデルは独立して書かれました。アプリケーションの仕様が固まる前に、将来何が必要になるかを見越して書かれています。これは認証ロジック実装でかなり発生するようです。仕様を固めている間に、認証ロジックが開発者が事前に開始できるものとして認識されるためです。
これは誤った仮定で、過剰に設計されたコードにつながり、あいまいで矛盾したインターフェイスを提供し、正しく使用されないか、まったく使用されなくなってしまいます。

とはいえ、チームの文化とかいろんな要因で、やりたいことを理解する前(仕様が固まる前とは違う)にロジック部分だけ作ってしまい、こうゆうコードが生まれるのはたまに見かけます。

Solution: Simplify with Simple Flags

前のセクションで説明した複雑さの問題に対処するために、次のようにモデルをリファクタリングすることができます。

class User < ApplicationRecord
end
class AddColumnToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :admin,  :boolean, null: false, default: false
    add_column :users, :editor, :boolean, null: false, default: false
    add_column :users, :writer, :boolean, null: false, default: false
  end
end

https://github.com/waterlow/authorization_astronaut/pull/2

これによってRoleモデルが削除でき、Userモデルにはadmin?editor?などのメソッドが使えるようになります。フレームワークの機能をを効果的につかい、「コードが無い」という一番きれいな状態になります。今後1つ2つのロール追加くらいならばこの方法でも問題は無いでしょう。

なかなか衝撃的でした。3つあったテーブルを一つにしてしまって、しかもUserモデルには何も書かないというシンプルさ…。仕事だとこう言う選択は非常に勇気が要りますが、シンプルさを保つ事ができる非常に良い方法だと感じました。

それ以上にロールが追加される場合は、テーブルを増やしましょう。ただし、多対多用の中間テーブルは作らず、has_manyで完結させます。

class User < ApplicationRecord
  has_many :roles

  Role::TYPES.each do |role_type|
    define_method("#{role_type}?") do
      roles.exists?(name: role_type)
    end
  end
end
class Role < ApplicationRecord
  TYPES = %w(admin editor writer guest)
  validates :name, inclusion: { in: TYPES }
end

https://github.com/waterlow/authorization_astronaut/pull/3

これは、不必要なコードを排除し、過剰に設計されていないため、ユーザーの役割を扱う方法について疑問のない一貫したインターフェイスを提供するため、成功しています。

やっぱり前者のadmin?メソッドにこだわっていて、Railsの機能が提供してくれるものが一番シンプルだという考え方ですかね。私もこの辺は同意です。自前で書くよりはよっぽどいい。あと、DBの外部キー制約が使えなさそうだけど、モデルでバリデーション入れてるからいいよねという気持ちだろうと思います。

ここで概説した概念は、ユーザーの役割にのみ適用されるものではありません。また、アプリケーションドメインをモデル化し、モデルによって提供されるインタフェースを定義するときに、他の多くの状況にも適用できます。以下の簡単なガイドラインは、オーバーエンジニアリングを止め、定義されていない仕様とアプリケーションの変更の両方に直面する簡単なインターフェイスを提供するのに役立ちます。

•コードを書くときにアプリケーションの要件を超えて構築しないでください。
•具体的な要件がない場合は、コードを記述しないでください。
•すぐにモデルを作ろうとしないでください。付加的なモデルの追加を避けるために、ブーリアンや非正規化などの単純な方法がよく使用されます。
•データの追加、削除、または管理のためのユーザーインターフェイスがない場合、モデルは必要ありません。 ハッシュまたは可能な値の配列で作成された非正規化列で問題ありません。

最後のはシリアライズのことをいっているのかなと思います。UIがない場合はモデル作らなくてもシリアライズで問題ないという考えは、以前ActiveRecord serialize / store の甘い誘惑を断ち切ろうという記事を読んで以来やらないほうがいいと思っていましたが、認可のようなアプリのメイン機能とは異なる場面では使ってもいいかなと思いました。
また、UIがない場合にモデルを追加しないという決断は驚きでした。

個人的な意見としては、空のモデルを作ってもいいかなとは思ったのですが、モデルを作ってしまう時点で将来の要求とはずれてしまうかもしれませんし、やはりモデルを作らないで済むのが一番拡張性の高い状態かなと思いました。

まとめ

仕事でこうゆうリファクタリングをぶっこんでいくのはなかなか難しそうですが、必要になるまで作らないという視点は常に持っておきたいです。

【LT】表参道.rbでActiveRecordのテーブル名の決め方等について発表してきました。

はじめに

ActiveJobのキュー名の設定をどうするかを考えていた過程で、ActiveRecordのテーブル名の決め方を調べていました。色々と面白いところがあったので表参道.rbで発表してきました。(ただのテーブル名あてゲーです。)

基本はシンプルに「親のテーブル名を見に行く」だけだった

f:id:waterlow2013:20170408221729p:plain

こんな風にabstract_classが複数入っていると難しく見えがちですが、AdminUserが読まれたときにテーブル名がadmin_usersになり、そのサブクラスのtable_nameは、指定しない場合はすべてadmin_usersとなります。
abstract_classはレコードを作れないということだけで、テーブル名の決め方には関係ありませんでした。

まとめ

普段深く知らないまま使っている機能について、ソースを読んで理解を固めていくのは非常にいい感じでした。

【LT】encrypted secretsについて社内LT大会でしゃべってきました。

はじめに

Rails5.1で導入されたencrypted secretsについて所属する会社の社内LT大会で発表してきました。

内容

5分だったので大したことは書いていませんが、機能の説明以外はstaging問題やメインです。

所感(使えそう?)

  • KMSをつかうならyaml_vaultかな
  • Railsのレールに乗っている感じが良い
  • 暗号化はガンガン修正が入らない前提。修正がたくさん入る場合は別の策を考える。

staging問題について

詳細

  • デフォルトではkeyと暗号化ymlは一組しか作られない。
  • Rails.env == 'staging' # => trueでも本番と同じkeyを使うことになる。
  • ステージングのkeyが流出するだけで本番のkey流出となってしまう。

解決策

  • secrets.#{Rails.env}.yml.encとそれに対応するkeyを別で持っておく。
  • 例えばsecrets.staging.yml.enc, secrets.production.yml.enc(rails secrets:setup, rails secrets:editしたらリネームする)
  • デプロイ時にln -sする。

まとめ

  • なにはともあれ設定ファイルコミットできるのは良い!

rebuild.fmのエピソード検索ができるwebサイトを作った

はじめに

Rails5.1の新機能を試すために、簡単なRailsアプリを作りました。 rebuild.fmのゲストとshownoteにフィルタリングをかけて検索できるようにしたものです。

heroku: https://shielded-river-53880.herokuapp.com/
idcf: http://rebuildfm-search.waterlow.tk/

https://i.gyazo.com/aa86202d4b40af933e4c06234c8fc3f1.gif

リポジトリ以下です。
github: https://github.com/waterlow/rebuild-search

使っている技術等

  • Rails5.1.0.beta1
  • encrypted secret
  • webpacker
  • itamae (vpcのプロビジョニング)
  • mysql

とにかく今回はRails5.1を使ってみるのが目的でした。encrypted secretは、普通に本番運用でも使える物だと思いました。ただherokuでは必要なさそうなのと、本番とドッグフーディング環境等でconfig/secrets.yml.keyを分けたい場合に少し考えないといけません。
そちらへの解も考えていたので、今度個別の記事にします。

webpackerには深入りしてはいませんが、問題なく動いていそう…。 compression-webpack-pluginで圧縮した結果、chromeでエラーが起こっていたので圧縮しないようにしました。 https://github.com/waterlow/rebuild-search/commit/e34c52569b52dbdb88945ddaa16a5e621cbfccd9

herokuでもvpcでも共通で、かつスタンダードなスタックにしたかったのでpostgresqlを考えていましたが、centos&itamaeでのインストールが上手く行かずに断念してしまいました。ただ、今後移行する予定です。

vpsでサービス立ち上げ、デプロイ等までちゃんとやったことがなかったのでいい勉強になりました!

今後

テスト、ci、自動デプロイを整備して、ストレスなく開発できるようにする。