マイクロサービス時代に捧ぐ、Railsでの中規模APIサーバ開発のための技術構成


初めまして、qsona (tw) と申します。Ruby on Rails Advent Calendar 2016 6日目の記事になります。

Rails歴は10ヶ月で、もちろんAdvent Calendarへの参戦も初です。

全体的に生意気な内容と思いますが、 じゃんじゃんマサカリ投げてください お手柔らかにお願いします。

はじめに

環境

JSONを返すAPIで、データベースはRDBを想定してます。
あんまり関係ないですが一応、Rails5 (api mode) + MySQLを想定しています。

マイクロサービスとしてのバックエンドに使う技術スタックの必要な要件

マイクロサービスの良いところは、サービスごとに合った別々の技術が使えるということです。

とはいえ、一般的な組織であれば、学習コストの面などから、ファーストチョイスとなる言語があり、普通の要件に対してはその言語を使う、ということになると思います。

では、マイクロサービスにおいての、普通の要件とは何か? ある程度下のようなことは共通して言えるのではないでしょうか。

  • (JSONなどの)APIを提供する。
  • 小〜中規模。(大規模にはなるべくしない。)
  • ビジネスロジックは普通に多い。

このような要件に対し、Railsという選択はどうなのでしょう?

なぜマイクロサービスのAPIサーバに開発にRailsを使うのか

自分が10ヶ月前にRailsを書き始める前、自分のRailsに対する印象は、

  • フルスタックなMVCフレームワーク
  • 設定より規約
  • Rails wayに乗っている分には楽だが外れると辛い

というものでした。

しかし実際に、ViewなしでAPIのためにRailsを使ってみて、感じたことは大分違いました。

  • そんなにフルスタックじゃない。
    • MとCしかない。Mの層を自分で拡張する必要あり。
  • 設定もそこそこできる。
    • とくにActiveRecordまわりは、割と柔軟に色々設定できる
    • Rails4からActiveModelとして一部分離されているのも大きい
  • Rails wayから多少外れたことも割とできる、というかそこまでwayを感じない

というものでした。 レールに乗るというよりは馬に乗ってる感じで、うまく手綱を引いて自分の力で制御できると感じました。

そうすると、Railsを使うことに以下のようなメリットが浮かび上がったわけです。

  • 枯れている
  • 関連ライブラリが多い
  • ActiveRecordは、中規模の開発に非常に適している
    • 小さすぎるとオーバースペック感あるが、大きすぎる開発ではFat Modelの問題が起きる。
    • その真ん中くらいの規模に最適。
  • Rubyは柔軟に書け、かつAPIが充実してて手早く実装できる

マイクロサービスをやるということは、常に前提として、ある程度の設計スキルは求められます。そうであれば、各サービスもガチガチに縛られたスタックでなくても、

  • 基本的には手早く書く
  • ここぞという箇所ではきちんと層分けなどの設計をする

という考え方で進めることができます。

Railsはそんな僕の希望を結構叶えてくれるので、気に入っています。

(ぼくがかんがえたさいきょうの)構成

マイクロサービスのバックエンドとして、Railsをメインで使っていく中で、自分がどんな構成・方針でやっているのかを紹介します。

サンプルコードが一行もでないのは自分でもどうかと思いますが、文だけでお楽しみください。

データベース

正規化重視。 DB設計 > Rails way

基本的にデータベース設計を優先させる。

Viewを持たないことで、Rails wayから外れやすくなっていることも理由の一つ。

この記事をだいたい書き終わってから、良い記事を見つけた。これを紹介してだいたい終わりな気がしないでもない、、

※追記: 12/7現在、下記の記事が非公開になっていることをご指摘いただきました。おそらくキュレーションメディアの件のあおりで、ブログごと非公開になっています。時間が経てばまた公開されるのではないかと思うので、ひとまず残しておきます。

Rails だって硬いデータベース設計をしたい!そんなあなたに贈る Tips 4 選 - peroli Developer's Blog


マイクロサービスは極端な話、APIのインターフェイスさえ変わらなければ、中身を作り直したっていい。

Railsで作り始めたが、ビジネスが伸びて、より高速化や処理の並列化が求められるようになり、別のスタックで書き直すことになったとする。Railsのコードは無駄だっただろうか?いやそんなはずはない(反語)。ビジネスの軌道に最速で乗せるのに貢献したと評価できよう。

しかし、以下のようなものは、たとえコードを作り直したとしても生き残る、プロジェクトの資産である。

  • ドメインモデルの責務の切り分け(論理設計)
  • データベース設計

Railsアプリケーションは作り直せても、データベース設計は往々にして作り直せない。だったら、データベース設計にはしっかり力を入れるべきだろう。

ポリモーフィック関連は一切使わない

ポリモーフィック関連にすると、外部キー制約が使えず、データベース設計としてはアンチパターンとみなされる。

代わりに、中間テーブルを利用した方法を使う(SQLアンチパターン6章にある)。

きちんとrelation定義すれば、普通に使い勝手もよい。

参考: http://qiita.com/joker1007/items/9da1e279424554df7bb8

STIは極力使わない

STIは、1つのテーブルで複数のものを表すため、NOT NULL制約がつかないカラムが多数できる。これは、リレーショナルモデルの基本的な考え方に反するため、極力避けることにしている。

代わりに、以下の2つから選ぶ。

  • 親テーブルは作らず、全てのテーブルに共通のカラムをもたせてしまう。
  • 親テーブル1つと、子テーブルを個数分作り、子 belongs_to 親 にする

いずれの場合も、アプリケーションとしてはConcernsを利用して共通化する。

このように、 テーブル設計(物理)とアプリケーション設計(論理)は切り離して考えるべき、という立場を自分は常にとっている。Rubyはそれができる十分な力を持っている。

外部キー、ユニーク制約は全て利用する

データベース設計の一環。

(少し前まではRailsからはDBレベルの外部キー制約をつけられなかったりしたと思うのですが、今は普通にいけるので利用する)

関連(自分の記事): [Rails] [RDB] キー・複合キーをきちんと利用する

おまけ: idからauto_increment外すことも

主キーをサロゲート(代理)キーではなく、自然キーにすることもたまにする。

関連(自分の記事): Rails + mysql でテーブルのidのauto incrementをやめる

おまけ: 複合主キー (composite_primary_keys)

いけます。(?)

Railsではサポートしていませんが、Rails内部のメソッドを書き換えるような形で対応しているgemがあります。

composite_primary_keys (GitHub)

本番投入したことはないです。が、しばらく試していて特に問題起きてなかったので、よほど欲しかったら使っちゃうかもしれません。その場合はgemのソースコード全部理解して最悪自分でメンテできるくらいでないとですが・・・。

こういうのはViewまで考えるとRails way的に色々無理なのですが、Viewを考えないですむとwayの幅が広がる、ということは言えると思います。

ActiveRecordとドメインモデル

古くは「えせMVC」のように、昔からよく議論になるテーマです。

ActiveRecord オブジェクトにドメインロジックを書く

ActiveRecordにロジックを書くと、データベースの層とモデルの層がごっちゃになる(Fat Model)から良くない、という人もいる。

そういう人の多くはService層に書くという。僕はそれには同意しない。手続き型でコードを記述する、ドメインモデル貧血症パターンだ。オブジェクト指向言語であるRubyで書くにはもったいない。

僕は積極的にActiveRecordにロジックを書く。なぜなら、それがActiveRecordだからだ。 ActiveRecordにロジックをかかないのであれば、もはやそれはActiveRecordパターンである必要がない。

(Railsの)ActiveRecordというのは、(関連まで含めた) テーブルと、ドメインモデルとして考察すべきものの単位が、高い確率で一致する、という仮説のもとにあると思っている。実際経験として、中規模までであれば、80%くらい問題なく使えると思う。

じゃあ、Fat Model問題はどうするか? 自分の答えは以下。

  • 必要なときは、別途ロジックだけのModelをつくり、そこからActiveRecordを操作する。
  • そもそもそんな規模になるものをRailsで作らない。マイクロサービスに分割することを検討。

concernsに頼る

STIの項目にも書いたとおり、「アプリケーションロジックを共通化したいな」という理由がテーブル設計に影響することは一切ない。
ロジックはConcernsを利用して共通化させる。

自分はわりと早い段階でConcernsにしていくように心がけている。
まだ共通メソッドはない段階であっても、振る舞いが同種であることをわかりやすくするために、メソッドのないConcernを作ってincludeさせておくこともある。

non-ActiveRecordなモデルクラスはカジュアルに作る

値オブジェクト、外部APIからのマッピング、などでカジュアルに作る。
その80%くらいは include ActiveModel::Model する。initializeメソッドのトンマナが統一されるだけでもメリットあり。

Serviceには極力頼らない

  • まずはオブジェクト指向の基本として、オブジェクト(Model)の振る舞いとして捉えられないかから考える。
  • 単一モデルに関するメソッドであれば、それはモデルのクラスメソッドに書き、Serviceは登場させない。
  • モデルがFatになるという問題であれば、Concernsに分離するなどする。
    • あまり好まないという意見を複数いただいて、自分自身も、基本的にはコードが散らばるだけで何も本質的な解決になってないと思ったので、取り下げました。
  • モデルがFatになるとき、別のモデルに概念を分けられないか考える
    • user.xxx() というようなメソッドは、実は別のクラスYyyのメソッド、 yyy.xxx(user) なのではないか? ということを考える
  • それでもおさまるモデルがない、複数モデルにまたがるような処理であれば、サービスを登場させる。

(さて、、14日目のjokerさんのサービスクラスの記事、楽しみにしています!)

Serviceの書き方

サービスとは、複数のモデル・関心にまたがるような手続き的なコードを書く場所なので、外から使うメソッドは全て関数(クラスメソッド)になる。

その上で、必要ならその中で自分のサービスクラスをインスタンス化して使う場合もある。
より細かい単位のメソッドの結果をキャッシュしたい、など。

ActiveRecordのcallbackを使わない

after_save などのアレです。

callbackって、あれはイベント駆動プログラミングですよね。たとえばafter_saveというイベントを発火して、それにより関心の分離をされたロジックが発動する。

すると、callbackを使っていい場面は、次の2つを満たしているはずです。

  • 該当モデル側からは、callbackで発動するロジックのことをまったく知らない(意識しない)。
  • callbackの処理は、現在・未来を通じて、例外なく100%発動してよい

正直、これを満たす自信があるような処理は、ほぼない。

てことで、モデルのcallbackは使わないと決めています。中規模開発だったら、callback書かなくても、普通に手動でできるという判断でもある。

ルーティング・コントローラ

普通にActionController / routes を利用する

世の中には Grape のようにRubyでAPIサーバを書くための軽いフレームワークもあり、Railsと共存できる。

が、覚えることが分散してしまうので、極力Controllerで行きたいという気持ち。乗っかれるRailには乗ろう。

JSON Schema (JSON Hyper Schema) を利用する

上で紹介したGrapeの良いところは、リクエストで受けるパラメータをDSLを用いて宣言的に書けることで、たとえばswaggerとかドキュメントを自動生成することができることだ。ただし、レスポンスの型までは定義されない。

マイクロサービスをやっていると、他サービスの実装を見に行くコストは基本的に高い(高くあるべき)で、レスポンスの型がどうなのかというのはかっちり定まっていてほしいし、ドキュメントで確認したい。

そこで、Grapeではなく、JSON Hyper Schemaを利用して、リクエストとレスポンスの型を両方記述している。

そして、以下の2つのgemを利用して、バリデーション・ドキュメント自動生成に利用する。

RailsのStrong Parametersは、宣言的に書けないので、そこからドキュメント自動生成したりできないのが辛い。(部分的に使うことはある)

参考記事: 全てがJSONになる - ✘╹◡╹✘

外部APIへの接続

  • 外部APIから値を取得し、レスポンスを生データに近い形で返す
  • レスポンスから、値やオブジェクトなど必要な形にして返す

アーキテクチャ的な理想を言ったら、上の2つは層を分けるべきだと思う。しかし、その分手間は増える。
(そもそもそこを求めるなら、選択はRailsではないし、小中規模開発ではあまりに多すぎる層分けも考えものだと個人的には思う。)

なので、自分は1つにまとめるのだが、具体的には以下のようにしている。

  • app/repositories ディレクトリを作り、配下に置く
  • Hashのまま返さず、値やモデルにして返す
  • ファイルの区切りは、"リポジトリ"として意味のある単位で分ける。
    • 外部サービスの種類の単位ではない。一般的にはそれより細かくする。

Hashがメソッドの戻り値などとして漏れ出すのは、バグの原因になりやすいので、極力使わず、かならずクラスを定義して、そのインスタンスやインスタンスの配列で返す。

HTTPリクエストには Typhoeus を利用する

マイクロサービスでは複数のサービスにリクエストを送ったりすることが多い。Rubyはそれに向いている言語ではないが、 Typhoeus (GitHub) というHTTPクライアントは並列にリクエストを送れるので、それを利用しておく。

JSONへのシリアライザ

active_model_serializers を使う

もともとrablを使っていたが、よりかっちりしたものを目指すため、active_model_serializers (GitHub) に移行した。かつ、下記の関連記事のように、厳密にリソースを定義しながらやっている。

関連(自分の記事): RESTful Web API を厳密なリソース指向にする

active_model_serializersを使うときのコツの一つは、 instance_option オプションを極力使わないことだ。

例外処理

基本の流れ

  • model層でエラーを送出、controllerでハンドル、が基本
  • カスタムのエラークラスは積極的に定義する
  • 積極的にassertionを書く
    • 簡単にいうと、起きないはずのことが起きてたらその場でfail
    • 例えば、case-when文のelseとか。

rescue_from

  • ActionController::BadRequestは、ApplicationControllerでrescue_fromして400にする
  • カスタムクラスもいくつかrescue_fromする(ex: 認証エラー)
  • それ以外は、500エラーにする。
  • (上述の通りだが、特に) ActiveRecord::RecordNotFound は、rescue_fromしない。
    • つまり、誰も拾わなければ500エラーになる。
    • デフォルトで拾って404にする流儀もあるのは知っている。が、 find_by! などを使った時に異常系として発生するケースのほうが多いので、それを500エラーにしたいため。

管理画面

できればSPAとして別に作る

工数との兼ね合いもあるが、エイッとクライアントアプリにしてしまいたい。理由は以下。

  • Rails5からの api modeでやりたい(そうするとViewが作れない)
  • Viewに書いてしまうと内部実装を変更しにくくなるので・・・。簡単な物でも外出ししたい。

自分のスキルセットとして、RailsのViewを書きなれていなく、JavaScriptはそこそこ得意、という背景もある。正直にいうと、クライアントの世界にちょっとでも追いついてたい、というモチベーションの問題も。

関連(自分の記事): 「管理画面」のマイクロサービスを立ち上げる前に考えるべきこと

まとめ

全般的に、堅めの設計にしつつ、Railsの恩恵を受けながらうまくハンドリングしていく方針にしています。active_model_serializers や JSON Schema など、最初はやや面倒くさいなと思うものもありますが、それで行くと決めてしまえばそうコストにも感じません。

Railsを始めて、他の人がどんな構成でやっているのかよく気になっているのですが、それほど多く記事がない気がしたので、まずは自分からぼくがかんがえたさいきょうの・・を公開してみました。参考になるところがあれば幸いです!