こんにちは。技術部の吉川です。
最近ではMicroservicesという言葉もかなり浸透し、そのテクニックも体系化されつつあります。 一方でMicroservicesについての話は概論や抽象的な話が多く、具体像が見えないという方もいらっしゃるのではないでしょうか。
当ブログでは1年半ほど前にMicroservicesへのとりくみについてご紹介しました。 当時社内ライブラリだったGarageはその後オープンソースとして公開され、また社内のシステムも当時と比べ飛躍的な進化を遂げています。
そういったクックパッドにおける最近のMicroservices事例を先日Microservices Casual Talksで紹介しました。
Microservicesの抽象的な話は一切割愛し、具体的な事例に終始した内容となっています。 Microservicesの基本となる考え方はわかったものの、実践方法で悩んでいる方へ少しでも助けになればということで当ブログでもあらためてご紹介します。
サービスの粒度
単一の責務をもった小さなサービス、などとよく言われますが、単一の責務とはどこまでを指すのでしょうか? クックパッドでのサービスを分類すると主に3つのタイプに分類できます。
ユーザーサービス
プロダクトにフォーカスしたタイプのものです。Microservicesは事業ドメインで分割することが大原則なので、これが基本形となります。 おいしい健康、料理教室、料理動画、クックパッドブログ、みんなのカフェ などなど、多くのサービスを提供しています。
個々のサービスは普通のRailsアプリケーションが多い(モバイルアプリのみというケースもありますが)ものの、チームによって個性が出ています。 例えばES6でReact.jsなものもあればCoffeeScriptのものもありますし、Rubocopを入れているものとそうでないものがあります。
プロダクトが異なれば対象ユーザーも違うため、柔軟な技術選定ができるべきです。一方で必要に応じて他ドメインのモデルを簡単に利用できれば相乗効果が期待できます。 言い換えると、ユーザーに対してはサービスごとに千差万別の対応ができつつも、社内の他サービスに対しては1つのモデルとして同じように振る舞うことが必要です。
ビューサービス
ドメインモデルは同じで別のビューとして振る舞うタイプです。
形としてはいわゆるBFF(Backend for frontends)パターンに近いのですが、 クックパッドでは様々なデバイスに対応するためというよりは、同じドメインモデルを別バージョンで提供するようなケースで使われています。 OEM版がその例で、根本的には同じものを提供するが、UIが別バージョンだったりアカウント体系が異なるものを提供します。
共通基盤サービス
様々なサービスから共通して利用される基盤機能群です。例えば以下のようなものがあります。
- OAuthプロバイダ
- 決済
- Push通知/メール配信
- アクティビティ
- 各種ログ
- セキュアデータ(個人情報)ストレージ
- 動画
サービス間の連携
RESTful Hypermedia API
Microservicesにおいてサービス間の通信をどのように実現するかはよく議論されるテーマです。 HTTPでREST APIを利用するのが一般的だと思いますが、Protocol BuffersやThriftなどを利用したRPCを選択する場合もあるかと思います。
クックパッドではGarageを使ったRESTful Hypermedia APIによるサービス間通信を行っています。 Garageについては以前にも紹介しているため本記事では割愛します。 本記事では、Garageだけではカバーできないポイントについて説明します。
並行処理と耐障害性
サービス連携が増えると問題になるものの一つがオーバーヘッドです。モノリシックなアプリケーションだとDBに接続してとってくるだけですが、それを単純にREST APIに置き換えるとオーバーヘッドは大きくなります。 スループットを上げるための工夫が必要となるわけですが、Microservicesアーキテクチャの場合他サービスへのリクエストは呼び出し元からするとただのIO待ちと見なせるため、並行処理と相性が良いという特徴があります。 適切な粒度にサービスが分割されていれば、各サービスの責務は独立性が高くなるため、より並行処理しやすい形になっていきます。
二つ目の問題は耐障害性です。どこかのサービスが詰まるとそのタイムアウト待ちリクエストが滞留してクライアント側も詰まってしまうという障害の連鎖はよくあるパターンです。 せっかくサービスが分かれているのに、障害に引きずられたくはありません。
リクエストを並行処理したり、よしなにリトライしたり、あるサービスで一定以上のエラーが発生したら、一定期間そのサービスへのリクエストを停止する。 こういったことをやってくれるのがExpeditorです。これらの機能で察する方もいるかと思いますが、Netflix/HystrixのRuby版です。 現状はまだ本家ほどの機能はありませんが、依存関係のあるAsynchronous ExecutionやFallback, Retry, Circuit Breakerといった基本的な機能は備えています。
サービス間のテスト
REST API連携で問題になるものの一つが、APIの互換性です。
通常CIで担保するテストのレベルでは、別サービスへのリクエストはスタブしているため、そのサービスのエンドポイントに互換性の無い変更が入っても気付けません。 Protocol BuffersでRPCをしている場合などは、protoファイルを共有していれば問題にならないかもしれません。 もしREST API連携で似たようなことをするなら、JSON Schemaを利用する手もあります。しかしもともとRuby + HTTPという文化であるため、型で管理する開発スタイルよりも、動くテストで管理するスタイルの方が開発者に馴染んでいました。 それがRack::VCRができた背景です。しかしサービスごとのCIで担保する方式だと、サービス間でビルド頻度に差がある場合、クックパッドのように高頻度でリリースを行う組織だと非互換を検知する前に反映されてしまうことがあります。 そこでConsumer-Driven Contract testingに舵を切り、現在ではPactに移行しました。
これにより、クライアントが必要なAPIの互換性が壊れた場合、プロバイダ側のCIで検知できるようになり、非互換な変更がリリースされることを防ぐことができるようになっています。
サービス間のログを紐付ける
サービスがわかれていると難しくなることの一つがログの追跡です。特にエラーログは、あるサービスでエラーが発生した際に、その呼び出し元でのエラーを見たり逆にリクエスト先でどういうエラーが発生していたのかを追跡できないと、問題の特定が困難になります。 これはリクエストにIDを採番して、各サービスでどのリクエスト起因なのかを特定する手法が一般的です。特にRailsではデフォルトでX-Request-Idヘッダに対応しており、IDがなければ採番されるようになっています。 また互いにGarageClientを使って連携しているため、GarageClientがリクエスト時にX-Request-Idをセットすることでリクエスト先に伝播することができます。
社内ではエラーログの管理にSentryを使っており、このリクエストIDをタグとしてエラーにつけて管理するようにしています。
サービスの実行環境/構成管理
サービスの種類が増えてくると大変になるのはインフラです。クックパッドではもともとPuppetを用いて構成管理をしていましたが、構成するモジュールが増えて複雑になったり、 また同じアプリケーションであってもAPサーバー向けの構成と、バッチジョブワーカー用の構成を分けて管理したりと様々な面で複雑になっていました。
現在は、以前からある巨大なRailsアプリケーションなどといった一部を除き、ほとんどのアプリケーションがDockerで管理されています。もちろんテスト環境だけではなく本番環境もDockerです。
ビルドパイプライン
各サービスでの変更がマージされると、各サービスのCIが実行され、テストが通ればDockerイメージが作成されます。作成されたイメージはそのまま自動でstagingにデプロイされます。 もちろん本番も同じイメージが使われますし、またバッチジョブもサービス専用の実行環境を必要とせず、Dockerイメージをとってきて実行するだけです。 もちろんこのイメージは開発者の手元で動かすこともできます。スケールアウトする際や、ペネトレーションテストのために独立した環境を作りたい、といった場合にもすぐに対応できます。
デプロイ
DockerのバックエンドはECSを利用していますが、実際のデプロイにはELBとコンテナの紐付けを設定したり、環境変数の注入といった様々な作業が必要となります。 そういったデプロイに関連する様々なアクションをプラガブルに設定できるデプロイツールがHakoです。 Hakoのおかげで、初回デプロイ時にELBを作成したり、Route53を使って自動でドメインを設定したりといった、以前であればインフラエンジニアが個別に設定していたような作業まで自動化されています。 これを活用して、本番環境では1つのアプリケーションが1つのELBに紐づくが、テスト環境では1つのELBに複数のアプリケーションが紐づくといった柔軟な構成をとることができるようになっています。
設定管理
同じイメージといっても、DBの接続先情報や、APIのクレデンシャルなど、環境依存の部分もあります。 Hakoが環境変数を注入してくれるのである程度はそこで管理できるのですが、秘匿すべき値はetcenv、etcvaultを使って管理しています。 バックエンドはetcdですが、もともとACLが無かったため暗号化して管理するようにしたものです。etcenvにはetcwebというUIツールがあり、それを使ってWebUI上で管理しています。
組織的なサポート
Microservicesアーキテクチャを採用しているということは、当然ながらチームもサービスに応じて分かれています。 そのためチーム間の連携方法や、全体の意思決定などの仕方も徐々に変化してきました。
技術領域課題共有会
各チームの独立性が高まると、互いにどういうことをやっているのか把握しづらくなってきます。その事業固有の悩みも増えていきます。 そうなると、実はみんな困っているのに自分たちのところだけだと思って問題提起されなかったり、隣のチームで解決済みなのを知らずにずっと困っているということにもなりかねません。 そこで各チームの技術リーダーと、基盤/インフラのエンジニアが月に一度集まって課題を話し合う場を設けています。
この会では各チームから今困っていることをあげてもらい、既に解決策がある場合はその共有を、無い場合は適切なチームが対応に動きます。 また全体最適ということで全体に影響のある仕様変更を考えているときに共有したり、それ以前にヒアリングするような場にもなっています。 これによって、問題と思ってなかった点に実はみんな困っていたとか、逆にみんな使っていると思って頑張って運用していたら別に誰も使っていなかったということも見つけ出せるようになってきました。
技術基盤担当
チームが多様になると、中には基盤改善が得意なメンバーがいるチームといないチームがあったり、新規開発が多いチームは自然と最新の社内基盤を知っていますが、 そうでないチームは便利な社内基盤があるのを知らずに自前で頑張ったりするケースもあります。あるいは基盤改善にまわす余力が無いチームもあったりします。
そこで開発基盤のメンバーが各サービスに担当者としてつくようにしました。 一人で複数のサービスについており、常駐するわけではないのでチーム外基盤担当とでも言えるでしょうか。 担当メンバーは各チームのチャットにはいるようにしているので、雑に相談したり、時にはガッツリはいって開発環境の整備をしたりします。
まとめ
クックパッドにおける最近のMicroservices事例についてご紹介しました。 Dockerがもたらしたポータビリティは社内の開発スタイルを大きく変え、Microservices化を加速させました。 それに呼応してPactのような仕組みや組織的なサポートなどが発展し、クックパッドの開発スタイルはこの1年でも飛躍的な進歩を遂げています。
Microservices化によって各チームがそれぞれのプロダクトにフォーカスすることができるようになります。 プロダクトにフォーカスするということはそのプロダクトのユーザーにフォーカスするということです。 プロダクトごとに最適な技術を選択したり、自分たちで自分たちのプロダクトに責任と権限を持って開発することができます。
今回は概要をご紹介しましたが、いつかHakoやPactといった個々の仕組みについてもご紹介できればと思っています。