クックパッドとマイクロサービス

技術部の高井です。

最近、日本でもマイクロサービスという言葉が流行しつつあります。

今回は、なぜクックパッドがマイクロサービスを選択したのか、また実際にどのようなやり方をしているのかということを紹介します。

Conwayの法則

ここ数年の間、クックパッドはレシピの投稿・検索サービスから「食を中心とした生活のインフラ」として事業領域を拡大しつつあります。海外レシピサービスの買収による海外展開は、単なる金銭的な関係にとどまらず、人的・技術的な交流も含めて本格化しつつあります。また、「モバイルファースト」を標語とするモバイルアプリケーションへの取り組みも加速してきました。

事業領域の拡大やグローバル展開、モバイルファーストといったビジネス要求の変化に応じて、会社の組織構造も変化しています。そして、Conwayの法則 として知られているように、組織構造とソフトウェアアーキテクチャには密接な関係があります。

Any organization that designs a system ... will inevitably produce a design whose structure is a copy of the organization's communication structure. (Conway 1968)

組織構造が変わるのであれば、クックパッドのソフトウェアアーキテクチャも変化しなければなりません。「組織とアーキテクチャは同じ形であるが、アーキテクチャはマーケットに従わなければならない」(Coplien and Harrison 2004)のです。

クックパッドはソフトウェアアーキテクチャの転換を進めてきました。大きなひとつのアプリケーションを一丸となって育てるというモデルから、自律的なアプリケーション群を事業領域や国を越えて連携させていくモデルへの転換です。

モノリシックアーキテクチャ

クックパッドは、巨大なコードベースを持つシステムです。リポジトリには、クックパッドのウェブサイトのためのコードだけではなく、APIや管理機能、バッチといった複数のサブシステムのためのコードが含まれており、メンテナンスされています。この仕組みは、クックパッドというサービスを会社に所属する開発者のほぼ全員が開発し、維持していくというモデルのもとでは、うまく機能していました。

とはいえ、まったく問題がなかったかというと、そうではありません。

コードを複数サブシステム間で共有しているため、共有されたコードには、特定のサブシステムからしか使わないモデルが存在したり、特定のサブシステムからしか呼ばれないメソッドがあったりします。モデルが複数のコンテクストに所属しているため、全体像の理解が難しく、意図しないところの変更の影響をうけたり、処理の流れがつかみにくいコードになってしまっていました。

また、自動テストの実行時間が非常に長くなってしまうという問題もあります。クックパッドでは、現在17,000項目を超える自動テストが存在します。RRRspec という分散テスト実行システムによってテストを並列化することにより実行時間を10分前後にとどめていますが、これも少なくないコストをかけたうえで、ようやく実現できていることです。

その他、デプロイメントにも影響がでてきます。クックパッドでは、デプロイメントは継続的インテグレーションによって自動テストを通過したリビジョンのコードだけというルールがあります。そのため、デプロイしたいサブシステムのコードに問題がなくても、別のサブシステムが利用しているコードによってテストが失敗し、デプロイができないということが発生しうるのです。また、リリースした機能は問題がなくても、他の部分に問題があったため、ロールバックをしなくてはならなくなったということもありました。

開発者個人が全体を把握できる規模のプロジェクトであれば、密結合なコードには十分なメリットがあり、効率良く機能します。しかし、一定の規模を越えるとそれが苦痛になり、耐えがたいものになってしまいます。

RESTful Hypermedia API

このような巨大なアプリケーションのコードベースに、これ以上システムを組み込み、同居させていくということは現実的な選択ではありません。かつてのクックパッドでは、データの共有をするためにコードの共有をしていました。かわりに、データの共有のためにRESTful JSON APIを構築することで、コードの共有なしにデータの共有をするやり方へと大きく方針を変えたのです。

RESTful JSON APIと一言で表現しても、厳密な規格があるわけではありません。ですから、アプリケーションごとにそれぞれAPIを実装すると、まったく異なった設計思想にもとづくバラバラなAPIができあがってしまいます。

そこでキーポイントになるのが、Garageとよばれる社内ライブラリです。Garageは「RESTfulであり、Hypermediaに現実的なレベルで対応する」(Miyagawa 2013)という思想で設計された、RESTful APIをRails上で実装するためのライブラリです。

Garageの特徴は、次の2点です。

  • リソース概念の導入
  • OAuth 2.0と統合されたアクセス制御

Garageを採用することで、RESTfulやHATEOASといった設計思想と、JSONやOAuth 2.0といった標準を取り入れたAPIを容易に実装することができるようになります。

Garage

では、実際にGarageを利用したコードを見てみましょう。次のコードは、GarageをつかったRailsのモデルになります。Garage::Representer はヘルパーモジュールで、ActiveRecordオブジェクトをリソースとして表現するための手助けをしてくれます。

class Employee < ActiveRecord::Base
  include Garage::Representer

  belongs_to :division

  property :id
  property :title
  property :first_name
  property :last_name

  property :division, selectable: true

  link(:division) { division_path(division) }

  def self.build_permissions(perms, other, target)
    perms.permits! :read
  end
end

リソースは、ちょうどモデルとビューとの中間にあたるような概念で、RESTfulな操作の単位でもあり、JSON形式へのシリアライゼーションも担当します。また、リソースは必ずしもActiveRecordオブジェクトでなくてもかまいません。

property や link といった見慣れない宣言が目につくかもしれません。これらは、Garageによって追加されたシリアライゼーションのための宣言です。property宣言は、シリアライゼーションするモデルの属性を指定します。link 宣言は、関連するリソースをハイパーメディアで表現するための宣言です。

build_permissionsクラスメソッドは、アクセス制御のためのメソッドです。あるリソースを、リソースオーナーによってのみ更新できるようにしたいケースなど、リソース単位でのアクセス制御に利用します。Garageは、OAuth 2.0 スコープによるアクセス制御を基本としていますが、それと組み合わせることによって細かなアクセス制御を実現します。

コントローラでは、 Garage::RestfulActions モジュールをインクルードします。これによって、CRUDの生成やアクセス制御の機能が実現されます。

class EmployeesController < ApplicationController
  include Garage::RestfulActions

  def require_resources
    @resources = Employee.all
  end
end

このように定義したリソースに対し、実際にcURLでリクエストを送ってみると、次のような結果を得ることができます。

% curl -s -H 'Authorization: Bearer xxxxxxxx' 'http://localhost:3000/employees' | json_pp
[
   {
      "_links" : {
         "division" : {
            "href" : "/divisions/1"
         }
      },
      "title" : "Manager",
      "id" : 1,
      "last_name" : "Cooper",
      "first_name" : "Alice"
   },
   {
      "_links" : {
         "division" : {
            "href" : "/divisions/1"
         }
      },
      "title" : null,
      "id" : 2,
      "last_name" : "Dylan",
      "first_name" : "Bob"
   }
]

複数のリソースがJSONの配列として取得できました。個々のリソースには、property宣言で指定したものが含まれています。また、link宣言で指定したハイパーリンクが、_linksというキーの値として含まれています。

クエリパラメータに fields という項目を追加し、リソースから取得するフィールドを指定することもできます。これは、Garageの機能のひとつです。

% curl -s -H 'Authorization: Bearer xxxxxxxx' 'http://localhost:3000/employees?fields=id,division' | json_pp
[
   {
      "division" : {
         "name" : "Music",
         "id" : 1,
         "phone" : "008"
      },
      "id" : 1
   },
   {
      "division" : {
         "name" : "Music",
         "id" : 1,
         "phone" : "008"
      },
      "id" : 2
   }
]

明示的に取得するフィールドを指定することで、property 宣言で selectable としていたフィールドも取得することができました。関連するリソースがネストされたリソースとして埋め込まれていることにも注目してください。

Pantryman

Garageによって、RESTful JSON APIの設計を統一することのメリットのひとつに、汎用的なクライアントライブラリが利用できるというものがあります。Garageを採用して実装されたAPIであれば、アプリケーション毎にクライアントライブラリを実装する必要がありません。

Pantrymanと呼ばれる社内ライブラリを利用すると、さきほどのAPIにアクセスするコードを次のように書くことができます。

client = Pantryman::Client.new(access_token: 'xxxxxxxx')

employees = client.get('/employees')
employees.each do |employee|
  puts <<-EOF
    ID:    #{employee.id}
    Title: #{employee.title || '-'}
    Name:  #{employee.first_name} #{employee.last_name}
  EOF
end

公表された言語(Published Language)

Garageを採用することによって、RESTful JSON APIにおけるJSONデータ形式表現の統一や、OAuth 2.0にもとづくアクセス制御を統一的な実装で実現できるようになりました。

マイクロサービスの文脈でしばしば取り上げられる概念のひとつに、『ドメイン駆動設計』で紹介されているパターンである「境界づけられたコンテキスト(Bounded Context)」があります。たとえば、SoundCloudでは、境界づけられたコンテキストをサービス抽出の単位としていることが紹介されています。

境界づけられたコンテキストという概念を採用するならば、それらの間でどのように情報をコミュニケーションさせるかということも、同様に重要となります。そのためのパターンが、「公表された言語(Published Language)」です。

必要なドメインの情報をコミュニケーションにおける共通の媒体として表現できる、明確にドキュメント化された共有言語を使用し、必要に応じてその言語への変換と、その言語からの変換を行なうこと(Evans 2003)

クックパッドにおけるGarageは、ちょうどこの公表された言語の機能を持っています。どのようなアプリケーションであれ、Garageという公表された言語を備えることで、他のアプリケーションと統一的で一貫した情報のやりとりができるようになっているのです。

マイクロサービス(Microservices)

クックパッドが、モノリシックアーキテクチャからGarageを基盤としたアーキテクチャへの転換を始めてから1年以上が経ちました。現在のクックパッドのトップページは、実のところ色々なサービスから情報を取得し、ひとつのページとして表示するようになっています。

たとえば、毎日のおすすめレシピの動画もそのひとつです。これは、料理動画の情報をAPI経由で取得し、表示しています。その他に、掲示板や料理教室についても同様です。これらのサービスは、それぞれ独立したチームが開発する独立したアプリケーションとして構築されています。

このようにクックパッドでは、新規のアプリケーションを独立したチームによる独立したサービスとして開発し、それらを連携させていくというアプローチをとるようになりました。私たちは、このアーキテクチャをマイクロサービス(Microservices)として位置付け、そう呼んでいます。

"Microservices: Decomposing Applications for Deployability and Scalability"(Richardson 2014 )という記事の中で、マイクロサービスのメリットについて次のようなものが挙げられています。

  • サービスを相対的に小さくすることができる
  • 他のサービスと独立してデプロイできる
  • スケール戦略を他のサービスから独立して行なえる
  • 他のサービスによる障害の影響を分離することができる
  • 特定の技術へのロックインを避けることができる

クックパッドの経験でも、マイクロサービス化によって、おおむねこのようなメリットを受けることができていると感じています。クックパッド自体のコードベースは巨大なままですが、それぞれのサービスはそこから独立し、巨大なコードベースの重さにひきずられることなくスピード感をもってサービス開発ができています。

一方で、特定の技術へのロックインを避けることができるという点では、まだまだRuby on Railsという選択が主流となっています。この点については、もっとチャレンジする余地があるのではないかと感じています。特に、ユーザーインタフェースがないバックエンドのサービスについては、別の言語やフレームワークを採用していきたいと考えています。

まとめ

クックパッドは、ビジネス環境の変化にともなう組織構造の変化にあわせて、ソフトウェアアーキテクチャをモノリシックアーキテクチャからマイクロサービスアーキテクチャへと進化させてきました。そこでのキーポイントは、RESTful JSON APIやOAuth 2.0といったウェブで培われてきた技術を採用している点です。これにより、軽量な疎結合アーキテクチャを実現しています。

その結果として、独立性の高いチームが比較的小さなサービスを素早く開発し、ユーザーさんにスピード感をもって価値を届けるという体制ができました。これは、今まで通りにクックパッドに全てを詰め込むというやり方ではできなかったことです。今後は、こうしたやり方をもっと推し進め、さらなる効果を狙っていきたいとおもいます。

なお、クックパッドではマイクロサービス化を強力に推進していく仲間を募集しています。このエントリを読んでご興味をお持ちいただけた方は、ぜひともご応募ください。