Firebase を駆使して作った Slack の slash command を改良してみた

#概要
こんにちは。Aidemyの刀根です。
以前、Firebaseを使ってslackにslash commandを追加しました。[その記事はこちら](https://qiita.com/Swordroot_M/items/ff337cde987fab6795ea)

しかし、同じようなコマンドをしばらく運用した結果timeoutエラーがちょいちょい出てきてしまうことがわかりました。

今回の記事は、その原因と解決法です。

#timeoutする原因
結論から言うと、 **Firebaseの仕様** でした。

Firebaseでは負荷に応じてvmをスケーリングするなどインフラ調整を自動で行なっているのですが、その中には

**あまりにもアクセスが少ないリソースはスリープ状態にする**

というものがありました。
スリープ状態になると、Cloud Functionを実行するとき一度リソースを起動してから関数を実行する必要があるので、httpリクエスト受信から応答までの時間が長くなってしまいtimeoutが発生していたと言うわけですね。

#対策
今回はhttpリクエストに対する応答さえ早く返してしまえばいいので、 `Google Cloud Pubsub` とAzureに立ち上げたVMを一つ利用して以下のような構成にしました。
![資料ベース2.jpg](https://qiita-image-store.s3.amazonaws.com/0/153926/7598153a-44a9-28e9-d7e0-ae498f4511c9.jpeg)

処理の順序としては、
1. vmにhttpリクエストを送る
2. vmがhttpレスポンスを返す
3. vmがtopicメッセージを発行する
4. topicメッセージの発行に呼応してfirebase上でslackへの投稿を実行

という流れになります。

#VM側で実行するコード
VM側ではExpress+Typescriptでhttpリクエストを受け付ける簡易APIサーバーを立てました。ソースはこちら。

```typescript:app.ts

import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as PubSub from '@google-cloud/pubsub';

const pubsubClient = new PubSub({
  projectId: 'your-project-id'
});

//トピックがなければ作る
pubsubClient.getTopics().then(async (results:any) => {
  try {
    const mappedTopics = results[0].map((element:any) => {
      return element.name;
    });
    if (mappedTopics.indexOf('topicName') < 0) {
      await pubsubClient.createTopic('start');
    }

  } catch (error) {
    console.error(error);
  }
}).catch((err:any) => {
  console.error('ERROR:', err);
});

const app: express.Application = express();
app.use(bodyParser.urlencoded({ extended: true }));

const publish = (topic: string, message: any) => {
  const messageBuffer = Buffer.from(JSON.stringify(message));
  pubsubClient.topic(topic)
    .publisher()
    .publish(messageBuffer)
    .then((messageId:any) => {
      console.log(`Message ${messageId} published.`);
    })
    .catch((err:any) => {
      console.error('ERROR:', err);
    });
};

app.post('/yourEndPoint', async (req, res) => {
  try {
    res.send('');
    // メッセージの内容はslackから飛んでくるbodyの内容と全く同じにする
    publish('topicName', req.body);
    
  } catch (error) {
    res.status(500).json(error);
    throw error;
  }
});



const server = app.listen(5000, async () => {
  try {
    console.log(`Example app listening on port ${5000}`);
  } catch (err) {
    console.error('Error: 起動に失敗しました');
  }
});

export { server };

```

#Cloud Function側で実行するコード
次に、Cloud Functionで実行するコードですが、こちらは改良前のソースコードとの差分を載せておきます。

```typescript:before.ts
// httpリクエストで発火する関数
export const setReminder = functions.https.onRequest(async (req,res) => {
  try {
    if(req.method === 'POST'){
      const min = parseInt(req.body.text || 30);
      const user_id = req.body.user_id;
      const remindTime = myMoment().add(min, 'minutes').format('X');
      await db.collection('reminders').doc(shortid.generate()).set({
        user_id,
        remindTime
      });
      res.send('reminder is set.');
    }else{
      throw new Error('only post method is accepted');
    }

  } catch (error) {
    console.error('error');
    res.status(500).send(error);
  }
})
```

```typescript:after.ts
// トピックメッセージ受信で発火する関数
export const setReminder = functions.pubsub.topic('topicName').onPublish(async (event) => {
  const min = parseInt(event.json.text || 30);
  const user_id = event.json.user_id;
  const remindTime = myMoment().add(min, 'minutes').format('X');
  await db.collection('reminders').doc(shortid.generate()).set({
    user_id,
    remindTime
  });
}
```

なんかスッキリしましたね。

##結果
この実装にしてからtimeoutするという問い合わせがなくなりました。快適。

##まとめ
今回はFirebase Cloud Functionを駆使して作ったSlash Commandの応答速度をVMの手を借りて改善しました。
今ではすっかり社内インフラの一つとして浸透しています。

今後も誰かに快適に使ってもらえるものを作っていきたいですね。