【Google Cloud / Node.js】Cloud SchedulerをAPI経由で使ってみる

## はじめに
[Firebase](https://firebase.google.com/?hl=ja) をバックエンドに使ったWebアプリの開発にあたり
「定期実行されるcronサービスを使いたい」
「クライアントアプリからジョブを設定したい(API使いたい)」
と思い、調べたところ [Google Cloud Platform](https://cloud.google.com/docs/overview/?hl=ja) の [Cloud Scheduler](https://cloud.google.com/scheduler/?hl=ja) が良さげさったので使ってみました。

cronサービスは他にもいくつかありますが、Cloud Schedulerの特徴としては

- 無料枠がある（1アカウントあたり3タスク）
- APIが提供されており、HTTP経由でジョブの設定、変更が可能である
- GCPの他のサービスとの親和性が高い※

などかなと思います。

※ Cloud Functions for Firebaseであれば、Cloud Schedulerの機能をラップしたメソッドを使って定期実行ができるそうです。参考: [Firebase Cloud Functionsの定期実行が、それ単体で簡単にできるようになった！ - Qiita](https://qiita.com/RyotaMurohoshi/items/607258c35c935c581d26)

本記事は、Node.jsでCloud Schedulerジョブを**APIを使って**設定し、 [Cloud Functions for Firebase](https://firebase.google.com/docs/functions/?hl=ja) を実行する方法の備忘録です。

## 事前準備

### GCPに登録
Cloud Schedulerは Google Cloud Platform が提供するサービスです。無料使用枠はありますが使用には登録が必要なため、予め登録しておきましょう。
登録完了後、APIを使うためのプロジェクトを作成してください。

### APIを有効化
APIを使用するため、 コンソールのAPIライブラリからCloud Schedulerを検索しAPIを有効化しておきます。
今回はCloud FunctionsもAPI経由で実行するため、Cloud Functions APIも有効化しましょう。

### Google Cloud API認証用の環境設定
Google Cloud APIの使用にはOAuth認証が必須です。
事前にGCPのサービスアカウントと、秘密鍵を含むJSONファイルを作成し、それを用いて認証を行います。
[こちら](https://cloud.google.com/docs/authentication/getting-started?hl=ja)を参考に、サービスアカウントの作成〜環境変数の設定までを実施します。

### Cloud Pub/Sub の設定
Cloud Schedulerでジョブを作成する際、定期実行するジョブの種類（ターゲット）を以下の選択肢から設定する必要があります。

- [Cloud Pub/Sub](https://cloud.google.com/pubsub/?hl=ja)
- HTTP
- App Engine HTTP

今回はCloud Scheduler → Cloud Pub/Sub → Cloud Functions の順で実行するので、予めPub/Subの設定と実行される関数を準備しておきます。
まず、最終的に実行されるCloud Functionsの関数を [functions.pubsub](https://firebase.google.com/docs/reference/functions/functions.pubsub?hl=ja) で作成します。トピック名は後ほど設定します。

```js
const functions = require('firebase-functions');

exports.handlePubSubTrigger = functions
  // トリガーとなるPub/Subのトピックを指定する
  .pubsub.topic('[トピック名]')
  // Pub/SubからPUSHがあったときの処理
  .onPublish(async (event, context) => {
    const pubsubMessage = event.data;
    console.log(Buffer.from(pubsubMessage, 'base64').toString());
  });
```

次に、前段で作成した関数をトリガーするPub/Subのトピックおよびサブスクリプションを作成します。作成は [Google Cloud SDK](https://cloud.google.com/sdk/docs/?hl=ja) からCLIで作成できるほか、GCPのコンソール画面からも作成可能です。[こちら](https://cloud.google.com/scheduler/docs/tut-pub-sub?hl=ja)を参照しつつ

1. トピック作成後、トピック名を実行する関数に設定
2. サブスクリプションのエンドポイントURLに実行する関数のURLを指定

を行います。

## APIを使ってみる

事前準備が完了したら、実際にCloud Scheduler APIを使ってジョブを作成してみます。
なお、Google Cloud API リクエストの実装方法として
1. 単純にHTTPリクエストを発行する
2. クライアントライブラリ( [Google APIs Node.js Client](https://github.com/googleapis/google-api-nodejs-client) )を使う

の2パターンあり、今回は使い勝手のいい後者を採用します。
ジョブ作成するCloud Functionsの関数は以下になります。プロセスとしては、

1. GCPのOAuth認証
2. Cloud Schedulerジョブの新規作成(APIのリクエスト)

の2つです。

```js
const functions = require('firebase-functions');
const { google } = require('googleapis');

exports.createSchedulerJob = functions.https.onRequest(
  async (req, res) => {
    // ①GCPのOAuth認証
    const client = await google.auth.getClient({
      scopes: ['https://www.googleapis.com/auth/cloud-platform'],
    });
    
    // ②ジョブの新規作成
    const projectId = '[PROJECT_ID]'; // プロジェクトID
    const locationId = '[LOCATION_ID]'; // ロケーション名(Cloud Functionsのデフォルトはus-central1)
    const topicName = '[TOPIC_NAME]'; // トリガーするPub/Subのトピック名
    const jobName = '[JOB_NAME]'; //作成するCronジョブ名

    const request = {
      parent: `projects/${projectId}/locations/${locationId}`,
      resource: {
        name: `projects/${projectId}/locations/${locationId}/jobs/${jobName}`,
        schedule: '0 7 * * 1-5', // 定期実行する時刻 今回は月~金のAM7:00に設定
        pubsubTarget: {
          topicName: `projects/${projectId}/topics/${topicName}`,
          data:  Buffer.from('Cron triggered.').toString('base64'),
        },
      },
      auth: client,
    };
    // APIリクエスト
    cloudScheduler.projects.locations.jobs.create(request, (err, response) => {
      if (err) {
        console.error(err);
        return;
      }
      return response.data;
    });
  },
);

```

### ① GCPのOAuth認証
まず、クライアントライブラリの`auth.getClient()`メソッドを使ってOAuth認証を行います。

このとき、`scopes`の設定が必要になります。
スコープとはアプリケーションに許可するAPI操作の権限の範囲で、これを認証時に明記する必要があります。
各APIに必要なスコープはAPIのドキュメントにAuthorization Scopesとして記載されています。今回はCloud Schedulerに必要な`https://www.googleapis.com/auth/cloud-platform`を指定します。

```js
const client = await google.auth.getClient({
  scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
```

補足ですが、`auth.getClient()`は開発環境において前段で準備した秘密鍵のJSONファイルをよしなに参照してくれるので、コード内で認証ファイルをインポートするなどの必要はありません。クライアントライブラリを使うのにはこうした利点があります。

### ② Cloud Schedulerジョブの新規作成

OAuth認証後、Cloud Schedulerのジョブを作成します。ジョブの作成には [projects.locations.jobs.create](https://cloud.google.com/scheduler/docs/reference/rest/v1beta1/projects.locations.jobs/create?hl=ja) を使います。

リクエストとして以下の項目を設定します。

- `parent` 
…ジョブを作成するプロジェクト。
- `resource.name` 
…ジョブの名前。`projects/~`で始まるパスの形で設定。
- `resource.schedule`
… ジョブの設定時刻。 [unix-cron 文字列形式](https://cloud.google.com/scheduler/docs/configuring/cron-job-schedules)で設定。
- `resource.pubsubTarget`
… トリガーするPub/Subトピックの設定。`topicName`と`data`を必須で設定する。`data`はBase64エンコードする。
- `auth` … 前段で取得した認証情報

なお、`resource.pubsubTarget` の箇所は[トリガーするターゲット](https://cloud.google.com/scheduler/docs/reference/rest/v1beta1/projects.locations.jobs?hl=ja#pubsubtarget)に応じてプロパティが変わるので注意してください。

```js
const projectId = '[PROJECT_ID]'; // プロジェクトID
const locationId = '[LOCATION_ID]'; // ロケーション名(Cloud Functionsのデフォルトはus-central1)
const topicName = '[TOPIC_NAME]'; // トリガーするPub/Subのトピック名
const jobName = '[JOB_NAME]'; //作成するCronジョブ名

const request = {
  parent: `projects/${projectId}/locations/${locationId}`,
  resource: {
    name: `projects/${projectId}/locations/${locationId}/jobs/${jobName}`,
    schedule: '0 7 * * 1-5', // 定期実行する時刻 今回は月~金のAM7:00に設定
    pubsubTarget: {
      topicName: `projects/${projectId}/topics/${topicName}`,
      data:  Buffer.from('Cron triggered.').toString('base64'),
    },
  },
  auth: client,
};
// APIリクエスト
cloudScheduler.projects.locations.jobs.create(request, (err, response) => {
  if (err) {
    console.error(err);
    return;
  }
  return response.data;
});
},
```

無事ジョブが作成されると、レスポンスで作成されたジョブの情報が返却されます。

### ③ ジョブの変更
一度作成したジョブを変更する際は [projects.locations.jobs.patch](https://cloud.google.com/scheduler/docs/reference/rest/v1beta1/projects.locations.jobs/patch?hl=ja) を使います。
以下の例では、先程作成したジョブの設定時刻を7時から8時に変更します。

```js
const client = await google.auth.getClient({
  scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
const projectId = '[PROJECT_ID]'; // プロジェクトID
const locationId = '[LOCATION_ID]'; // ロケーション名(Cloud Functionsのデフォルトはus-central1)
const jobName = '[JOB_NAME]'; // 更新するCronジョブ名

const request = {
  name: `projects/${projectId}/locations/${locationId}/jobs/${jobName}`,
  updateMask: 'schedule',
  resource: {
    schedule: '0 8 * * 1-5', // AM8:00に設定変更
  },
  auth: client,
};

cloudScheduler.projects.locations.jobs.patch(request, (err, response) => {
  if (err) {
    console.error(err);
    return;
  }
  return response.data;
});
```

作成時と異なるのは`pubsubTarget`が不要となる点と、リクエストに`updateMask`を指定する点です。
`updateMask`はジョブのどのフィールドを更新するかを指定するパラメータです。設定しないと更新されないので必ず設定します。

## おわりに
API経由でcronジョブをスケジューリングするような記事があまりなかったので、参考になれば幸いです。
内容に誤りや改善点があればコメントいただけるとありがたいです！

## 参考記事

- [Firebase Cloud Functionsで定期実行してみる - Qiita](https://qiita.com/jun1009/items/4145072dbb243c3edf9f)
- [Cloud Functions for Firebase の関数をスケジューラから定期的に呼び出す - Qiita](https://qiita.com/masatomix/items/b40a2c604319e52df4bf#%E3%82%B9%E3%82%B1%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%A9%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)