マチマチ技術ブログ

ご近所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月、突然の負荷上昇と障害に悩まされることになったのですが、そのときの話はまた改めて。