マチマチ技術ブログ

ご近所SNSマチマチの開発チームよりお届けします

マチマチをGAEで動かす話

ご近所SNS「マチマチ」を作っている武者(@knu)です。夏生まれだけど暑い夏は大の苦手です。

マチマチとHeroku

マチマチでは、きめ細かい地域の情報を扱うそのサービス特性から、PostGIS(PostgreSQLの地理空間情報拡張)をかなりヘヴィーに使い倒しています。また、サーバサイドアプリケーションの実装にはRuby on Railsを採用しています。

これらを載せて動かすプラットフォームとして、マチマチでは当初Herokuを利用していました。Herokuはご存じの通りRails専用PaaSとしてスタートし、DBMSとしてPostgreSQLのみをネイティブ提供してきました。この組合せのPaaSとしては第一人者であり、今なお最適解の一つと言えます。様々なSaaSをプラグインの形で簡単に追加でき、デプロイも手軽かつ高速、といったあたりは、スタートアップにとって非常にありがたいサービスです。

ただ、マチマチなどの主に日本国内を対象にしたサービス提供者にとって大きな問題が、(「プライベートスペース」を除き)提供地域がアメリカとヨーロッパに限られることです。つまり日本国内にデータセンターがなく、すべての通信が太平洋を往復するため、大きなレイテンシーが生じてしまうのです。メインとなるログイン後のフィード画面の表示が完了するのに2秒近く掛かるようになった段階で、アプリケーションの最適化も必要だがインフラの移行も避けられないという認識になりました。

乗り換え先としてまずはAWSを考えましたが、スパイク対策に苦労が多い経験から、メディア露出が増えていくフェーズでの採用はためらわれました。マチマチはまだまだ小さなチームで開発・運用を行っているため、あまりインフラに手間をかけたくないのです。

GCPの誘惑

折しも昨年(2017年)はそんな我々にとって、Google Cloud Platform (GCP)が大きく存在感を示した年でした。3月、RubyをサポートするGAE Flexible Environmentが一般提供(GA)リリースされた上、Cloud SQL for PostgreSQLベータ版がリリース(その後2018年4月にGA)されました。突如、マチマチを稼働できるPaaS環境がGCP東京リージョンに誕生したことになります。Googleのインフラのスパイク対応の実績は折り紙付きですし、何より新しいものは試してみたい!

というわけで、機能開発が一段落した同年9月下旬から本格的に調査を開始し、さまざまな苦難を乗り越えて、最終的に11月下旬に移行を完了しました。本番稼働環境だけでなく、プレビュー・ステージング環境や開発プロセスの移行も行ったため、足かけ2ヶ月も要してしまいましたが、そのプロセスは、Herokuで完璧に構築された仕組みを自前の劣化コピーで置き換えていくような趣きがあり、ただただHerokuの偉大さが骨身に沁みる道のりでした。

Gitレポジトリへのpushをトリガーにして自動リリース、というのはどこでもやっていることですが、Herokuのそれはものすごく速くかつ安定しています。データベースのマイグレーションを挟んでのアプリケーションサーバの再起動・切替えは全自動ですし、ロールバックや他のバージョンへの切り替えも一瞬、環境変数の変更もheroku configで瞬時に反映できます。新しいブランチのプレビュー用アプリもコマンド一発で好きなホスト名でリリースできるし、データベースのコピーも簡単で、デモサイトのデータベースを定期的にロールバックするなんてのも高速に行えるので、多くのバージョンを効率よく同時運用する用途には持ってこいです。マチマチでは、メインサービスをGAEに移行した今でも、サブではHerokuを愛用しています。

デプロイという難問

他のPaaS同様、Google App Engine (GAE)にも良さと難しさが色々同居していますが、本記事ではデプロイについて軽く紹介します。

いきなり残念なお知らせなのですが、GAE/FEのデプロイは絶望的に遅いです。最小のRailsアプリでも、デプロイ完了までに7-8分は掛かります。Googleの強力な全自動インフラに載せるコストと思えばある程度しょうがないかと思いますが、それでもなお、マチマチの規模のアプリだと15分以上掛かっており、些細な変更のリリースでさえもCIと合わせると30分近く掛かってしまうのは辛いものがあります。

デプロイ方法自体は簡単で、Dockerfileapp.yamlを用意してgcloud app deployを実行するだけです。しかし、DockerイメージのベースとするRuby用の公式イメージに入っているRubyやNode.jsは常に少し古く、Yarnは入っていません(執筆時点)。そこで、rbenvで新しいRubyを入れる、aptレポジトリを足して新しいnode/npm/yarnを入れるなど、「ふつうのRailsアプリ」を動かすにもいろいろ追加が必要です。そのイメージのビルドは手元で行い、Google Container Registry (GCR - バックエンドはGCS)にdocker pushしてからgcloud app deployする、というのが通常の手順で、つまり2ステップ必要です。

なお、毎回Dockerイメージをビルドして撒くというところから分かると思いますが、稼働中のインスタンスのアプリケーションを「アップグレード」することはできません。新しいバージョンのリリースは、新しいインスタンス(コンテナ)群を起動して、そちらに切り替えるというプロセスになります。(これはHerokuなども同じです)

また、データベースのマイグレーションは誰も自動でやってくれないので、自分で行う必要があります。しかし、gcloud app deployはモノリシックなプロセスで、途中の最適なステージにrails db:migrateを差し挟むということはできません。それではマイグレーションを行った後でデプロイを開始しよう、となりますが、それだと、データベースのスキーマ更新後に長いデプロイプロセスを待つことになります。つまり、少なくとも7, 8分の間、新しいデータベーススキーマの上で古いアプリケーションが動き続けることになり、データベースに不整合が起きやすい状況が生じてしまいます。さて、どうするか…。

マチマチで採用したのは、次の方法です。

  1. 新しいバージョンを、古いバージョンは動かしたまま、プロモーション(トラフィックの切り替え)なしでデプロイする。これはgcloud app deploy--no-promote --no-stop-previous-versionを与えることで可能です。

  2. 新しいバージョンのデプロイを待つ。7, 8分待つと新しいバージョンのインスタンスが起動されますが、データベースのスキーマは古いままなので、「動かない」状態("liveness"チェックは通るが"readiness"チェックは通らない)も想定されます。

  3. データベースのマイグレーションを実行する。

  4. 新しいバージョンの各インスタンスに対し、sudo docker restart gaeappをリモート実行してGAEアプリ用コンテナを再起動する。

  5. 新しいバージョンの各インスタンスが稼働状態になるのを待つ。

  6. トラフィックを古いバージョンから新しいバージョンに切り替える。

  7. 古いバージョンを停止する。

この一連の手順を何とか実装したのですが、まあ面倒でした。4や5は並列処理・待ち合わせが必要ですし、このデプロイ処理が複数同時に走った場合を考え、自分より新しいバージョンが稼働していたら切り替えない、などのチェックも必要です。エラー処理・ロールバック処理をきちんと実装するのも難しくて泣きそうです。

ともあれ、これで古いスキーマ+新しいコードでの稼働状態をだいぶ短くできました。お疲れ様でした。

移行を終えて

なかなか大変な作業でしたが、移行後はほぼ手が掛からず安定稼働しています。ただ、もう少しリソースを自前でコントロールしたい、デプロイを高速化したい、となるとGAEでは限界があるので、そうなる前に、次はGKE (Google Kubernetes Engine)で構築してみたいですね。

なお、コンテンツの大幅な拡充を進めていた今年の6月、突然の負荷上昇と障害に悩まされることになったのですが、そのときの話はまた改めて。

立ち上げ期スタートアップのテスト方針(Ruby on Rails編)

こんにちは!マチマチというご近所SNSを開発している id:fujimuradaisuke です。

スタートアップで働くみなさん、テスト書いてますか?テストの書き方・方針ってチームによってかなり異なるんじゃないかなーと思っています。実際いままで僕が参加したチームもそれぞれに個性がありました。そこでマチマチのテスト方針やその背景をシェアしたら面白いのでは…?と思ったので(僕は他の人のテスト方針を読みたいです)、社内向けにまとめておいた文章を共有してみようと思います。

背景

ご近所SNSマチマチは「ひらかれた、つながりのある地域社会をつくる」というミッションのもと2016年に開発がスタートしました。現在外部のお手伝い頂いている方を含めて5人のエンジニアで開発しています。構成としてはプレーンでモノリシックなRuby on Railsのアプリケーションです。現在は気がつけばモデルが200個とそこそこの規模になっています。フロントエンドはReact.jsを使っていて、コンポーネントは250個ほどという状態です。

目的

  • がんがんリリースできるようにする
  • 「どう動いているべきか」をテストコードとして残す

の2つを目的にテストを書いています。

テストコードがないと新しい変更が正しく動作しているか自信がもてず、リリースへの心理的ハードルがあがってテンポが悪くなってしまいます。また、都度動作確認する時間的コストもバカになりません。

すべての機能に厳密な仕様書を残しメンテナンスするのは現実的ではないので、どうしても動いているコードが正解、ということになってしまいます。しかし動いているコードの現在の挙動は意図しているものなのか?は、テストコードがないとわかりません。自分でも忘れてしまいます。

大方針

  • 複雑にしない
  • End to endテストをがんばる

をモットーにしています。

「複雑にしない」の理由は、複雑さを解決するコストは手を動かすコストより高い、という認識あっての判断です。愚直に手を動かせば直せるものは無理に共通化せず、読みやすい、変えやすい、という状態を目指しています。

具体的には、複数のテストケースで前提を共有しないようにするなど、DRYさより可読性を重視し冗長に書くようにしています。マチマチはRSpecを使っているので、shared_example / shared_contextは極力使わない、beforeやsubjectに無理に共通部分を押し込まないなど。

「End to endテストをがんばる」の理由は、バックエンド・フロントエンド含めたアプリケーション全体としての動作保証で最も重要であるという点が大きいです。マチマチだとfeature specがEnd to endテストにあたります。

feature spec: 重要な仕様の網羅を目指す

全ての表現をfeature specでも網羅することで得られる価値はコストに見合わないと判断していて、「重要な仕様」とは思えない些末なものはテストしていません。具体的にはユーザー体験上進行不能なレベルではないクラスの切り替えによる表示のハイライトなど。こういうところはJavaScriptのテストで書くと簡単な場合もあるので、それもアリです。

feature spec: 長いexampleを書くの推奨

登録プロセスすべて、などを100行あるexampleで一気に書いています。例えば「登録できる」という機能を、メールアドレスとパスワードの送信、プロフィールの入力など、とそれぞれ単体でテストしても組み合わせると失敗する場合もあるので、システムテストとしてはあまり意味がありません。実際に使うときの一連の流れをテストコードとして残す気持ちで書いています。

feature spec: 開発スピード重視

「コメントがn件増えた」ことを検証するときに直接モデルに触ってもOK、としています。End to endテストなので厳密には触ってはいけないはずですが、ここはスピードを重視しました。テストデータの投入もFactoryBotでやっています。

request spec: Web APIのドキュメントのつもりで書く

「期待される挙動を残す」のが目的なので。現状まだドキュメントを生成するまでには至っていません。

request spec: feature specでカバーできていれば書かなくてもよい

副作用、レスポンスともにごく簡単なAPIは書いていません。余談ですが、最近は更新系のAPIで204 No Content を返すことが多くなってきました。面倒だし、レスポンスを使わないんですよね….。

model: Rubyのクラスとしての振る舞いをテストとして残す

ActiveRecord::Baseを継承したモデルもひとつのRubyのクラスです。クラス単体で見てどう振る舞うか、をテストコードして残すようにしています。他のテストでカバーされている場合でも、なるべく単体としての振る舞いはテストケースとして書き残すことを推奨しています。重複に思えるかもしれませんが、拡張、再利用のとき挙動が残されていると楽です。また複数の役割を持ったメソッドってたまに生えてしまうものですが、メソッドの挙動がテストに残っていると不自然さがわかるので、そういったメソッドの発生も避けやすいです。

model: バリデーション、リレーションなど「書けば動く」系のものは込み入ったもの以外書かない

これは過去あまり書いて嬉しかったことがないので書いていません。

job, lib, tasksなどその他

他のテストでカバーできていれば書かなくてもよいが、なるべく書くようにしています。ただ、簡単なジョブ(メソッド呼び出しをバックグラウンド実行するだけのもの)は書かず、feature/request/model specでカバーしています。

まとめ

以上が私達マチマチ開発チームのテストのやり方です。みなさんのチームのテストの方針と比べてどうでしょうか?異論・反論・賛同の声などありましたら、ブコメでもツイートでも直接ご連絡でもよいので、ぜひとも教えてください。何しろ少人数のチームなのでどうしても考えが極化しがちなので、外部の方の意見は自分たちのやっていることを客観視できるのでとてもありがたいです。また、ここはもっと聞きたい、という所があればできる限り追記しますので、遠慮なくお声掛けください。

またマチマチではエンジニアを募集しています。副業も大歓迎です。 藤村武者にお気軽にお声掛けください!