テニスコートの空き状況を Slack に通知するアプリケーションの設計

新宿区や横浜市などのテニスコート予約サイトをスクレイピングして、空き状況を Slack に通知するアプリケーションの設計を公開します。

# 背景

実は既に、新宿区のテニスコートの空き情報は Slack に通知できています。毎日21時になったら投稿されます。

<img width="643" alt="スクリーンショット 2019-02-26 23.19.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/102964/d0195ce4-f058-4f10-a2b9-b6dfc27d35e0.png">

Node.js + Puppeteer + Google Cloud Functions で動いています。定期実行には Cloud Scheduler を使用しています。当初は AWS Lambda を使っていましたが、Cloud Functions に移行しました。[^1]

ただ、横浜市のテニスコート予約サイトに着手し始めると、Cloud Functions だけでは限界を感じました。というのも、空き時間まで取得しようとするとページの階層が深すぎるのです。特に、その月の空きコートが存在する日付を特定した後に、それぞれの日付のリンクをたどって時間を取得する必要があるのですが、Promise を使って非同期処理をしても苦しかったです。

そこで、Pub/Sub 的な仕組みや、キューなどの仕組みを利用して、分散して空き状況を取得していくものをイメージしました。仕事で最近 GCP を使っていて、Google App Engine, Cloud Firestore, Cloud Tasks などを使った構成は結構良いなあ、と思っているので、この組み合わせで考えてみます。

出来上がってから記事を書いても良かったのですが、仕事でやるように、個人開発でも設計を公開したら面白いかもしれない、と思ったのでやってみます。

# 技術スタック

- Node.js
- TypeScript
- Puppeteer
- GCP
    - App Engine
    - Cloud Firestore
    - Cloud Tasks

# 設計

以下、アーキテクチャ図・API設計・DB定義に分けて書きます。

## アーキテクチャ図

当たり前かもしれないですが、新宿区と横浜市で大きくサイトの構造が変わるので、それぞれ設計を考えます。

### 図の前提

- Google App Engine[^2]
    - `App Engine` のアイコンが複数登場しますが、同じアプリケーションを表しています。分散処理のイメージを図にしてみました。
    - GAE は同じ GCP プロジェクト内で複数のアプリケーションを動かすことができますが、今回は同じアプリケーションで、複数のエンドポイントを用意するようにします。
- オレンジ色の矢印
    - Cloud Tasks[^3] を経由して各エンドポイントにリクエストを送ります。
- Cloud Firestore
    - NoSQL データベース。自動でスケーリングする。アイコン探しても見つからなかったので、手書きです。

### 新宿区

![アーキテクチャ図（新宿区）.png](https://qiita-image-store.s3.amazonaws.com/0/102964/ae9f99e4-624a-5c87-9e98-e1e9f311bfd0.png)

### 横浜市

![アーキテクチャ図（横浜市）.png](https://qiita-image-store.s3.amazonaws.com/0/102964/2468fa00-9970-3374-6954-4e329560ccfd.png)

### Slack 通知

![アーキテクチャ図（Slack通知）.png](https://qiita-image-store.s3.amazonaws.com/0/102964/b157778e-ac16-932b-b01b-5e652ca43494.png)

## API定義

API は以下の４つを定義しました。

1. 各テニスコートの情報をスクレイピングする
2. 日付ごとに利用可能な時間をスクレイピングする（横浜市用）
3. `1.` のAPIに対してリクエストを送る Cloud Tasks のタスクを作成する
4. Cloud Firestore に集まったスクレイピング結果を Slack に通知する

[mokuo/tennis\-court\-watchman/swagger\.yaml at master](https://github.com/mokuo/tennis-court-watchman/blob/master/docs/swagger.yaml) で公開してます。 [Swagger Editor](https://editor.swagger.io/) にコピペするなりして見てみてください。

## DB定義

スプレッドシートで簡単に作成しました↓↓

https://docs.google.com/spreadsheets/d/1sSdckkX1byNPYd4wMbyUyvgD_PP3RIqQATjhbK3_Y70/edit?usp=sharing

# おわりに

こういう設計の方が良いのではないか、などありましたら、ぜひコメントいただきたいです。

自分の中で今回の記事の想定読者も定めきれていないところもあり、ここがわかりにくい、などのコメントもいただければ、説明を追加するなど考えたいと思っています。

正直言って、AWS Lambda => Cloud Functions => Google App Engine と作り直していてどんだけ時間かけてんだよと思うし、Puppeteer はデバッグしづらくて面倒くさいし、でもせっかくだからやりきりたいなあという感じです。

最近運動不足だし、ちゃんと通知来るようになったら、定期的にテニスやるようになると良いな・・・。

[^1]: 簡単に言うと、次のような理由です。AWS Lambda はデプロイする際にソースコードを依存関係も含めて Zip 化しますが、そうすると npm モジュールをインストールした環境（Mac）と Lambda の実行環境（Amazon Linux）で OS が異なってしまいます。これにより、Puppeteer がうまく動作しませんでした。Google Cloud Functions はデプロイ後に `yarn install` を行ってくれるため、このような問題は起こりません。

[^2]: `Google App Engine` : GCP の PaaS 。基本的にデプロイするだけで動いてくれて、自動でスケーリングします。Standard Environment はリクエストがない時はインスタンス数が 0 になります。Flexible Environment は最低 1 インスタンスは常駐するが、Docker を使ってカスタマイズ可能。今回は Standard Environment を使用します。

[^3]: `Cloud Tasks` : GCP のフルマネージドな分散タスクキュー。エンドポイントやパラメータを指定して、非同期に処理を実行できます。リトライ処理なども行ってくれて、最大同時実行数や最大リトライ回数なども指定できます。AWS でいう SQS 的なやつかなと思います。SQS 使ったことないけど。