CloudFunctionsで一定時間経過後に処理を実行する方法

# はじめに

CloudFunctionsで何らかの処理を実行して、その何分後かに別の処理を呼び出したいことがあります。
だけどsetTimeoutを使いたくない。事前にジョブを登録しておくわけにもいかない。

そんな時に以下の方法で実装してみました。

+ cloudSchedulerに対して、実行させたい日時（例：○分後）にジョブを登録。

+ 実行時刻になると処理が呼び出される。

+ 処理が終わったら不要になったジョブを削除。

参考：[CloudSchedulerリファレンス](https://cloud.google.com/scheduler/docs/reference/rest/v1beta1/projects.locations.jobs?hl=ja)

# サンプルコード

```javascript:index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const { google } = require('googleapis');
const cloudScheduler = google.cloudscheduler('v1beta1');
const moment = require('moment');

process.env.GOOGLE_APPLICATION_CREDENTIALS = 'path/to/serviceAccountKey.json';

async function authorize() {
  const auth = new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/cloud-platform'],
  });
  return await auth.getClient();
}

const projectId = 'my-project-id';
const locationId = 'us-central1';
const topicName = 'my-topic-name';
const jobName = 'my-job-name';

// 10分後処理実行を登録する処理
exports.setTriggerIn10minutes = functions.https.onRequest(
  async (req, res) => {
    // 現在時刻の10分後を取得し、cronフォーマットを生成
    const cronStr = moment().add(10, 'minutes').format('m h D M *');
    // GCPのOAuth認証
    const authClient = await authorize();
    // ジョブ追加リクエストを作成
    const request = {
      parent: `projects/${projectId}/locations/${locationId}`,
      resource: {
        name: `projects/${projectId}/locations/${locationId}/jobs/${jobName}`,
        schedule: cronStr,
        pubsubTarget: {
          topicName: `projects/${projectId}/topics/${topicName}`,
          data: Buffer.from(`${topicName} ${cronStr}`).toString('base64'),
        },
      },
      auth: authClient,
    };
    // APIリクエスト
    try {
      const response = (await cloudScheduler.projects.locations.jobs.create(request)).data;
      console.log(JSON.stringify(response, null, 2));
    } catch (err) {
      console.error(err);
    }
    res.status(200).send('OK');
  }
);

// 10分後に呼び出される処理
exports.beCalledAfter10minutes = functions.pubsub.topic(topicName).onPublish(
  async (event, context) => {
    const pubsubMessage = event.data;
    console.log(Buffer.from(pubsubMessage, 'base64').toString());

    // TODO: 実際にやることを記述

    // GCPのOAuth認証
    const authClient = await authorize();
    // ジョブ削除リクエストを作成
    const request = {
      name: `projects/${projectId}/locations/${locationId}/jobs/${jobName}`,
      auth: authClient,
    };
    // APIリクエスト
    try {
      await cloudScheduler.projects.locations.jobs.delete(request);
    } catch (err) {
      console.error(err);
    }
  }
);
```

# 実行確認

## 最初の処理

ブラウザから以下のURLにアクセスし、Cloud Functionsを実行します。
`https://location-id-my-project-id.cloudfunctions.net/setTriggerIn10minutes`

Cloud Functionsのログを確認し、setTriggerIn10minutesが正常に実行されたことを確認します。
Cloud Schedulerのジョブ一覧を確認し、トピック：my-topic-nameのジョブが10分後の実行予定で追加されたことを確認します。

## 10分後に呼び出される処理

Cloud Functionsのログを確認し、beCalledAfter10minutesが正常に実行されたことを確認します。
Cloud Schedulerのジョブ一覧を確認し、トピック：my-topic-nameのジョブが削除されたことを確認します。


# 注意事項

+ FirebaseのSparkプラン（無料）では使えませんでした。Blazeプラン（従量制）にする必要あり。

+ Cloud Schedulerの管理者権限をIAMで割り当てる必要あり。

+ 実行時刻は秒単位での指定はできません。

といった制約があるので使える場面は少ないかもしれませんが、もしもの時に役立てばと思います。