Railsで作られた社内システムをGAE/Goでリプレースした所感

TABI LABOのinottiです。

昨年から年初にかけて、メディアのバックエンドの肝であるCMS(コンテンツ管理システム)をAWSでRailsアプリを動かしていた構成からGAE/Goにリプレースしたときの所感をまとめました。

経緯

TABI LABOでは直近3年ほど内製で作ったRailsアプリをCMSとして運用していましたが、運用開始当初から比較すると、社員は10倍近くに増加、単一メディアから複数メディア展開、テキスト記事以外にも動画記事など多様なコンテンツ提供をおこなうなど、事業規模の拡大やビジネスドメインの複雑化が加速度的に進んでおりました。

そのため、このまま既存のシステムを機能拡張しつつ運用していくには、単一メディア前提のシステム設計であったり、TABI LABO設立当初のWordPress時代の名残を引きずったイマイチなテーブル構成であったり、フロントエンドおよび外部メディアへの記事配信バッチや画像管理システムを内包したモノリシックなアプリケーションであったりと、限界に近づいており昨年6月にCMS(および周辺社内システム)を全面リプレースする意思決定をおこないました。

新システムを開発するにあたって、既存システムの技術スタックを流用することによってリスクや工数を減らす考え方をとる気は全くなく、今後の開発・運用時に恩恵を受けるべく新規技術の導入に積極的にチャレンジしていくことにしました。

技術選定

技術選定をするにあたって念頭においたことは、やみくもにWeb界隈で流行っている技術を採り入れるのではなく、既存システムの抱える問題点を解決するための手段として最適な技術を採用することでした。

今回のリプレースで達成したいことをまとめると次のようになります。

  • REST API、Viewテンプレート(HTML, JS, CSS)、配信系バッチが1つのAPサーバーに同居しており、積年の仕様変更、機能追加によりコードの見通しが非常に悪く品質面で問題を抱えるようになっていたため、これらをスマートに分離し、シンプルで変更が容易なアーキテクチャを実現する。
  • アクセス頻度やピークタイムの異なる複数のサービスが1つのAPサーバーに同居しており、サーバーリソースやスケーリングに無駄や非効率があったので、これを解消しインフラコストを削減する。
  • 複数メンバーによって頻繁な機能追加を続けてもコード品質を一定水準に保てるようにする。また、本番環境へのデプロイの作業負荷の高さや作業ミスによる障害が気になっていたので、なるべく工数をかけずに迅速にデプロイをおこなう仕組みを用意する。
  • エンジニアチームの規模が小さく、インフラ専任のメンバーを置ける状況ではないので、できるかぎりフルマネージドなプラットフォームを選択する。

これらを実現すべく、情報収集や他社事例のヒアリング、1週間ほど時間をかけてフィージビリティスタディなどをおこない、最終的にGAE/Goを採用しました。

以下が新旧システムの技術スタック一覧になります。

   既存システム  新システム
 インフラ  AWS  GCP
 言語  Ruby 2.4  Golang 1.8
 プラットフォーム  AWS Beanstalk  Google App Engine
 Webフレームワーク  Rails  Echo
 View  Slim
 (HTMLテンプレートエンジン)
 React.js
 (シングルページアプリケーション)
 DB  RDS(MySQL)  Cloud Datastore
 検索エンジン  AWS CloudSearch
 CDN  AWS CloudFront

所感

それぞれの技術要素ごとに開発中に苦心した点、実際運用してみた所感などをまとめていきます。

インフラ: AWS -> GCP

GAE/Goを採用したため、必然的にGCPとなります。AWSで構築していたLAMPベースの環境からGoggle独自のプラットフォームへの移行となりましたが、主要なサービスはAWSの各種サービスとだいたい1対1で対応しているかと思います。(後述しますが、CloudSearch、ElastiCache相当のサービスが存在しないのは痛かった)

かなり癖があり学習コストはそれなりにあったものの、慣れればコンソールのダッシュボードやモニタリングが充実していたり、各種リソースのプロビジョニングがAWSと比較して極端に早かったりと、使い勝手が非常によいです。

言語: Ruby 2.4 -> Golang 1.8

GolangはCやJavaの後継言語の印象です。Robert GriesemerやRob Pikeが開発に関わっているので当然といえば当然なのですが。静的型付け、明示的キャスト、実行時にビルドが必要、シュガーシンタックスが少ないなど、Rubyに慣れていると最初はかなりイライラします。ArrayやHashの操作など、Rubyでワンライナーで書けるものに何ステップのコードを書かないといけないのかと。。。そのかわり言語標準でコードフォーマッターがついていたり、ビルド時のコンパイルチェックがかなり厳しかったりと、コードから属人性を排除し、可読性や品質水準を維持できるのでスキルレベルにばらつきのある大人数でのチーム開発に向くのかなと思います。

弊社ではRubyとの住み分けを以下のイメージで考えています。

  • Ruby: フロントエンド周りなど、頻繁な仕様変更があり、作っては壊すをアジャイルに回していくサービス
  • Golang: バックエンドのAPIサーバーなど堅牢性が求められ、長期間に渡って複数メンバーで運用/機能追加を続けていくサービス

プラットフォーム: Beanstalk -> Google App Engine

Google App Engine(GAE)はGCP採用の決め手になるキラープロダクトであると思います。Standard EnvironmentとFlexible Environmentがありますが、Flexibleの方はBeanstalkでEC2インスタンスを立てるのとさほど変わらず、巷で騒がれているGAEはStandardの方になります。

このGAEを導入することでとくに運用面で多くの恩恵を受けることができました。とくに大きかったのがスケーリングと本番デプロイです。スケーリングについては、Webメディアの特性上ピークタイムとそうでない期間のトラフィックの差が大きく、スマニュー砲やYahoo砲でバズると極端なバーストトラフィックが生まれますが、GAEの場合インスタンス起動が瞬時におこなわれるため、AWSでEC2インスタンスの起動待ちでヤキモキしていた時間が完全になくなりました。

本番デプロイ作業については、今までBeanstalkでおこなっていた、Staging環境に最新アプリをデプロイしてProduction環境とURLスワップしてトラフィックが完全に移行したらProduction環境を更新してもう一度スワップして、といった作業が完全になくなり、本当にワンクリックでBlue-Green Deploymentを実現できました。

ログビューワーも充実したフィルタリング機能だったりRuntime Errorが発生した位置のコードをデバッガーで確認できたりと、十分すぎる機能が揃っています。

また、コンフィグレーションはインスタンスクラスB2でBasic Scalingにしていますが、Beanstalkを利用していた既存システムと比較してインスタンス料金は1/2以下に落ち着いておりコストパフォーマンスも良好です。

逆にGAEの気になるところといえば、Golangに対する対応が遅かったり、説明しづらいのですがどうも世界観的に相性が悪いように感じます。Golangの最新バージョンは1.10ですが、GAEでは昨年ようやく1.8に対応しています(ただし現状ではgcloudツールでデプロイがおこなえずgoappコマンドを使わないといけなかったりと制約がまだ残る)。Pythonのみ対応の状態でのリリースからJava、PHP、そしてGolangと後付けで対応してきた経緯なのでしょうか。個人的には(そしておそらく国内のWeb企業的にも)GAEのRuby対応が待ち望まれます。

Webフレームワーク: Rails -> Echo

Golangは現状ではWebフレームワークが乱立しており、よく言えば選択肢が多い、悪く言うとデファクトスタンダードが存在しない状況です。今回はなるべく薄くシンプルなWebフレームワークを入れたかったのでEchoを採用することにしました。Railsのようなオールインワンフレームワークではないものの、とくに開発で困ることはありませんでした。

View: Slim -> React.js

この記事の主題とは少しずれますが、ViewはJSフレームワークでシングルページアプリケーション(SPA)にし、GAE/Goで作成したAPIサーバを呼び出す形にしました。JSフレームワークには、2017年の時点でエコシステムがいい感じになっておりデファクトスタンダードとなった感のあるReact(+Redux)を採用。開発時は既存システムで使っていたJQueryコンポーネントの中にReactと相性が悪いものがいくつかあり、同等のUIを実現するのに多少苦労がありました。RailsでWebページを実装するのに比べて肌感で2〜3倍の工数はかかったものの、ページ遷移でモタモタすることがなくなりユーザーから喜ばれました。

SPAの利点としてよく言われるUXの向上やUIのコンポーネント化云々は、開発者の設計スキルによるところが大きく、良いものを作ろうとすると相応の工数が必要となり難易度も高いです。どうしてもSPAでないと実現が難しいUI/UX要件がない場合は、開発工数がかさむだけになってしまい、あえてSPAを採用するメリットは少ないのではないかと思います。

ちなみに生成した静的アセットのホスティングにはFirebase Hostingを使いましたが、CLIツールを使ってコマンド一発でデプロイから公開、CDN配信まで自動でやってくれます。バージョニングにもしっかり対応しています。そしてほぼ無料。今後、静的サイトの公開にS3+CloudFrontを使うことはおそらくないと思います。素晴らしいサービスです。

DB: RDS(MySQL) -> Cloud Datastore

DatastoreはKVSに簡素なインデックスとトランザクション機能を付け足したGCPプロダクトで、AWSでいうDynamoDBに相当するものです。かなり癖があり、RDBでやる一般的なテーブル設計とは全く異なる設計思想が必要です。トランザクション要件が厳しいので、正規化崩しまくってフラットスキーマにする、1対n関係のエンティティもできるかぎりArray型にして1エンティティの中にぶち込む、といった感じで設計するのが正しいのではないかと思います。

インデックスが貧弱でor検索やlike検索ができない(前方一致検索なら工夫すれば可能)ため、複雑な検索が必要な部分は、別途検索エンジンを使うことで対応しました。

Read/Write回数による従量課金のため、ある程度トラフィックのあるアプリケーションの場合、Memcacheによるキャッシングは必須になります。

複数メディア対応のため、Namespace(マルチテナンシー)機能によるパーティショニングを採用しましたが、うまく要件にマッチしました。

検索エンジン: CloudSearch

GCPのプロダクトには検索エンジンが存在しないため(Search APIは全文検索機能のみ)、GCP上にElasticSearchを立てることも検討したものの、既存システムで採用していたAWSのCloudSearchをそのまま流用することにしました。
aws-sdk-goはドキュメントが簡易で説明不足感があり、GolangからのCloudSearchの呼び出しに若干苦労しました。

CDN: CloudFront

GCPにはCDNサービスとしてCloud CDNが存在しますが、AWSとCloudFrontのディスカウント契約を結んでおり他社CDNは使えないため、AWSのCloudFrontをそのまま流用しました。ドキュメントを読んだ限りでは、静的コンテンツのキャッシュ目的の場合どちらを選んでも大差ないかと思います。

まとめ

本記事は以上になります。

弊社では、AWSなのかGCPなのか、RailsなのかGolangなのか、二者択一で選ぶのではなく、それぞれの得意分野を組み合わせていくという技術戦略を採用しています。当然他の有力候補が出てくれば積極的にチャレンジしたいと考えています。

GAE/Goを検討している皆様の一助となれば幸いです。