みんなの「作ってみた」

レガシーでオタク丸出しな公共交通Webアプリを今どきの技術でリニューアルした話

2019/06/15

seapolis
seapolis
ITと交通の融合を考えるしがない開発者。 全日本公共交通機関データベースを作ろうとしている人。

(2019/06/15 執筆途中です。図とか追加したい)
(2019/06/19 アプリケーション構成図を追加)

Sotetsu Lab.」というWebサービスを2014年ごろから公開しています。
ある鉄道会社の一つの編成の動きを、ユーザーが投稿する情報をもとに予測する、というのが主な機能で、そのほかに時刻表が見れたり、「何時何分に何駅を出る列車には何系が充てられる」みたいな情報がわかる、鉄道オタク丸出しなサービスです。

このWebアプリ、もはやいつ作ったのかも忘れるほど前に作りまして、ほとんど技術的な更新をしないまま5年も経ってしまったため、レガシーな技術セットとなってしまっています。
言語はPHP(CodeIgniter)、さくらのVPSにPHPサーバーとMySQLサーバーを建て、SSLはLet's Encryptで自動取得、CI/CDなんてものはもちろんなく、ソースコードをダイレクトにサーバーに上げる手法をとっていました。

今回、そんなレガシーなWebアプリをインフラからフロントエンドまでまるっとリニューアルしたので、その顛末をまとめておきます。

量が多いので、後々やったことについて詳細に記した記事を別途書こうと思っています。

リニューアル後のサイトはこちら。
https://v3.sotetsu-lab.com/

調査・設計

1. GTFSというフォーマットの存在を知る

2年ほど前に、Googleが提唱している公共交通のオープンデータフォーマット「General Transit Feed Specification」というものがあるのを知りました。

路線・停留所や駅・時刻といった情報をジャンル別にCSV形式でまとめ、zipに梱包したもので、現在では国土交通省が定める「標準的なバス情報フォーマット」にこのGTFSを拡張したものが用いられているなど、着実に日本でも広まっています。

しかしながら、日本の交通事情はGTFSが生まれたアメリカとは当たり前ですが大きく異なり、先述のようにバスであればまだシンプルなのでほぼそのまま適用できますが、こと鉄道となると、相互直通運転や種別変更といった情報が表せず、そのままでの適用は難しいな、という結論に至ります。

また、もともとがCSVファイルをzipで固めたものなので、Web APIとして使うには不便すぎるということで、「GTFSをもとにして全国の公共交通機関のオープンデータをWeb APIに出来ないか?」と考えるようになりました。

2. GTFSを拡張し、SQLで扱えるようにしたい

そこでまず、GTFSをSQLで扱えるようにパースしてみることにしました。

幸いにも、時刻表データは旧版のサイトに蓄積されたものがあるので、これをテストデータとして利用しつつ、日本の鉄道事情に適合するように拡張し、また他の時刻表・ダイヤグラム表示アプリ開発者と協議の上、「Japan Public Transport Information」フォーマットという名前で策定を進めていきました。

その成果が以下のスプレッドシートになります。(一部古いところがあります)
https://docs.google.com/spreadsheets/d/1XSroQhguG03YM2QGQ0fXlMt4RhGypVx5d2N4S_s_O_o/edit?usp=sharing

今回のリニューアルでは、基本的にこのフォーマットを下地にして実装を行っています。

3. APIを公開したい

将来的に、蓄積した公共交通機関のデータをオープンデータとして公開できるような設計にするため、アプリとAPIの間に別途認証用サーバーを作り、Open ID ConnectのClient Credential Grantで認可されたらデータソースにアクセスできる、という形を取ることにしました。

今回はひとつの鉄道会社に関する情報にとどめて開発を行いましたが、将来的には全国の公共交通機関の情報を集約できたらいいな、という願いを込めています。

インフラ

1. 大まかな構成

Sotetsu Lab.の構成は、大きく分けて、

  • フロントエンド - Angular
  • バックエンド - Node.js express
  • API - Node.js express
  • 認証サーバー
  • データベース - PostgreSQL

に分かれます。

2. AWSを採用する

今回リニューアルにあたっては、AWSを採用することにしました。

フロントエンド側はSPAということもあり、S3バケットにアップロードしてCloud Front経由で配信するシンプルな形です。

バックエンド側は、フロントエンドからAPIに向けた通信をインターセプトし、認可時に使用するクライアントIDとクライアントシークレットを付与して認証サーバーへ認可処理をリクエストするだけのものなので、Node.jsで実装したロジックをServerless Frameworkを用いてLambdaにアップロードし、API Gatewayで公開しています。

今後最も負荷がかかると思われるAPIでは、DockerコンテナをECR経由でデプロイし、AWS ECSでオートスケーリングするようにしています。本当はFargateを使いたかったのですが、お金の事情でEC2です。

また、フロントエンド/APIのデプロイにあたってはCode Pipelineを使用し、Githubのmasterブランチにプッシュしたら、

  • フロントエンド側:Angular CLIでビルド→S3にアップロード→Cloud FrontでInvalidate
  • API側:Dockerコンテナのビルド→ECRへの登録→デプロイ

を自動でやってくれるようにしています。

おまけにECSはBlue/Greenデプロイなので、ミスった時のロールバックもボタン一つでお手の物です。

認証サーバーに関しては、AWS Cognitoをそのまま利用しています。ほんとクラウドって便利。

フロントエンド

1. Angularは重い

フロントエンドにはAngularを使用しました。
開発当時はAngular 7でしたが、先日Angular 8が登場したのでアップデートしています。

しかし、フルスタックなフロントエンドフレームワークであるところのAngularは、ReactやVueといったフレームワークに比べるとどうしても重くなってしまうというのが難点です。

また、Angularは内部でChangeDetectionという変更検知機構が存在し、通常状態では値が変更されたかどうかにかかわらずこれが走っているので、特にIE11のようなレガシーブラウザや。スマホのようなモバイル端末では重くなりがちです。

そこで、ページを構成するそれぞれのAngular ComponentにChangeDetectionStrategy.OnPushという設定を追加しました。

従来の変更検知機構を切り、データが変更されるときにだけ変更検知を走らせることで動作の軽量化を行っています。

2. Material Designの採用

旧版Sotetsu Lab.では、オレオレデザインを採用していました。

元来私はデザインがへたくそなので、一貫性のない微妙なデザインになりがちでしたが、コンポーネントライブラリとして@angular/materialを採用することで、一貫性があってなおかつ見栄えするデザインに統一することができました。

3. socket.ioでリアルタイム性を追求

Sotetsu Lab.で一番見られている機能として、「リアルタイム運用」というものがあるのですが、旧版ではリアルタイムと言いながらページを更新しないとデータも更新されない仕様でした。

これを、socket.ioを利用して「真にリアルタイム」にしました。

やっていることは、フロントエンドから「情報を投稿」フラグをsocketサーバーに送信したら、送信者以外に「情報が投稿された」フラグを返し、それを受け取ったらAPIからデータを読み直すだけの単純なものですが、UXは向上したのではないかと思います。

バックエンド

フロントエンド側で認証情報をセキュリティ上保持できない関係上、APIとの通信に必ずバックエンドをかまし、バックエンド側で認証情報を付与してAPIに再送信する処理を行っています。

凝ったことはやっていないので割愛します。

API

1. Clusterモジュールによるマルチスレッド化

Node.jsは単一スレッドで処理を行いますが、Clusterモジュールを使うことでマルチスレッド化することができます。

正直効果のほどは不明ですが、今後のためのおまじないです。

2. node-cronでバッチ処理

毎日午前3時に、前日の運用情報をコピーして番号を一つ進めたものをDBに登録する処理を、node-cronというモジュールで行っています。従来のcrontabの書き方で設定できるので便利です。

しかし、Clusterモジュールを使っていると、同一のcron設定が複数スレッドで走ることになるので、それぞれのスレッドで乱数を使用してバッチ処理をn秒遅延実行し、一番最初に実行されたcronだけDB操作を行うような実装にしています。

これは正直頭が悪いので、今後Lambda関数としてバッチ処理だけを切り出したほうがよさそうです。

3. JPTIフォーマットの実装

基盤となるフォーマットをもとに、各テーブルに対してCRUD処理を実装するシンプルなものです。ORMにSequelizeを使用しています。

リレーションがとても複雑で、単純に時刻表を表示するだけでもかなりのJOINが必要なので、パフォーマンス問題が今後の課題となりそうです。

ORMを無理に使わず、SQL直書きも視野に入れていきます。

まとめ

  • GTFSは頑張ればWeb APIにできる
  • ただしそのままでは使えないので、拡張は必要
  • Angular、やっぱ重い
  • しかし軽量化の余地はある
  • AWSが便利すぎる

今後の展望

  • 時刻表オープンデータ化の波を、バス業界だけでなく鉄道業界にも広げていきたい
  • 今回作成したAPIをもとに、ディープで全国規模のオープンデータソースを作りたい
  • APIのTypescript化
  • ダイヤグラム描画(オタクしか使わないけど)

あとがき

5/31、公共交通オープンデータ協議会により、「公共交通オープンデータセンター」の運用開始が発表されました。

GTFSではなく、独自策定フォーマットのJSON-RD形式によるWeb APIですが、現時点では唯一の、信頼できる筋が提供する日本の公共交通オープンデータベースとなります。

これにより、今後公共交通とITのかかわりがさらに広がっていくことが予想されます。

鉄道とプログラミングの両方を趣味とするものとしては、この流れを歓迎しつつ、今後の行く末を注意深く見守っていき、あわよくば乗っかっていきたいと思っています。

広がれ、公共交通オープンデータ。