Cloud Tasks を使って、Cloud Functions で作成した認証が必要なエンドポイントを叩いてみた

## 目次
- Cloud Tasks から叩くエンドポイントの作成
- エンドポイントのデプロイ
- エンドポイントの権限設定
- キューの作成
- キューの設定
- サービスアカウントの作成
- タスク作成ロジックの作成
- タスク作成ロジックの呼び出し

## Cloud Tasks から叩くエンドポイントの作成

Cloud Functions を用いてエンドポイントを作成します。

```ts
export const helloWorld = functions
  .region("asia-northeast1")
  .https.onRequest(
    async (req, res) => {
      console.log("hello world");
      res.status(200).send("success");
      return;
    }
  );
```

参考 ↓

https://firebase.google.com/docs/functions/http-events?hl=ja

## エンドポイントのデプロイ

プロジェクトの functions ディレクトリ配下で以下を実行します。

```bash
$ firebase deploy --only functions
```

もし特定の関数（今回の例だと helloWorld）だけデプロイする場合は以下のようにしてください。

```bash
$ firebase deploy --only functions:helloWorld
```

参考 ↓

https://firebase.google.com/docs/functions/manage-functions?hl=ja

## エンドポイントの権限設定

デプロイをすると、GCP コンソールから遷移する Cloud Functions のページに `helloWorld` という関数が選択できるようになっているかと思います。

選択をすると「指標」「詳細」「ソース」「変数」「トリガー」「権限」「ログ」「テスト中」というタブが表示されます。そこで、「**権限**」を押してください。

するとそこにはその関数を呼び出せるアカウントが一覧表示されているのですが、以下のような行が存在することがあります。

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462620/969e15ff-20d6-5799-06a2-df2b730614a6.png)


この行が存在する限り、全てのユーザーが構わずエンドポイントを叩くことができてしまいます。なので、この allUsers を右の鉛筆アイコンから削除します。

こうすることで無駄にエンドポイントが叩かれることがなくなります。

また、呼び出しには認証が必ず必要になります。

## キューの作成

### 1. Cloud Shell を開く

GCP コンソール画面でターミナル（Cloud Shell）を開きます。

参考 ↓

https://medium.com/google-cloud-jp/start-cloud-shell-bb0905e9593d

https://cloud.google.com/shell?hl=ja

### 2. キュー作成コマンドを実行

Cloud Shell で以下のコマンドを実行します。こうすることでキューが作成されます。

```bash
$ gcloud tasks queues create my-queue
```

`my-queue` に関しては、任意の名前で大丈夫です。後ほど使います。

### 3. 正常に作成されたことを確認

Cloud Shell で以下のコマンドを実行して、正常にキューが作成されたことを確認しましょう。

```bash
$ gcloud tasks queues describe my-queue
```

参考 ↓

https://cloud.google.com/tasks/docs/tutorial-gcf?hl=ja#creating_a_cloud_tasks_queue

## キューの設定

GCP コンソールから遷移する Cloud Tasks のページに `my-queue` というキューが選択できるようになっているかと思います。

構成タブを開いて、要件に合わせて、設定を行なってください。（同時に実行できるタスクが多いと、リクエストを捌けない可能性も考慮して、筆者は最大同時タスクを 1 としました。また、ただ試したいだけなので、最大施行回数を 15 としました。）

## サービスアカウントの作成

先ほど Cloud Functions を呼び出す際に、認証を必要とする設定を行いました。ここでは、作成したタスクが実行され、Cloud Functions を用いて作成したエンドポイントを叩く時の認証用にサービスアカウントを作成します。

サービスアカウントの作成手順は以下の通りです

1. [[IAM と管理] の [サービス アカウント]](https://console.cloud.google.com/iam-admin/serviceaccounts?hl=ja)  で、[**+ サービス アカウントを作成**] をクリック
2. [サービス アカウント名](https://cloud.google.com/iam/docs/creating-managing-service-accounts?hl=ja)（わかりやすい表示名）を追加し、[**作成**] を選択
3. [**Cloud Functions 起動元**] の役割を選択し、[**続行**] をクリック
4. [**完了**] を選択

参考 ↓

https://cloud.google.com/tasks/docs/tutorial-gcf?hl=ja#creating_a_service_account

## タスク作成ロジックの作成

タスク作成ロジックを作ります。最短で実装するのであれば以下をコピーすれば良いかなと思います。

```typescript
import { v2beta3 } from "@google-cloud/tasks";

export const createHttpTaskWithToken = async <T,>({
  project,
  location,
  queue,
  payload,
  url,
  serviceAccountEmail,
}: {
  project: string;
  location: string;
  queue: string;
  payload: T;
  url: string;
  serviceAccountEmail: string;
}): Promise<string | null | undefined> => {
  const client = new v2beta3.CloudTasksClient();

  const parent = client.queuePath(project, location, queue);

  const convertedPayload = JSON.stringify(payload);
  const body = Buffer.from(convertedPayload).toString("base64");

  try {
    const [response] = await client.createTask({
      parent,
      task: {
        httpRequest: {
          httpMethod: "POST",
          url,
          oidcToken: {
            serviceAccountEmail,
            audience: url,
          },
          headers: {
            "Content-Type": "application/json",
          },
          body,
        },
      },
    });
    console.log({ response });
    return response.name;
  } catch (error) {
    console.error(error);
    throw new Error("failed to create task.");
  }
};
```

scheduleTime を設定しないと、即時で実行されるようスケジューリングされます。

あとは、呼び出し側で project, location, queue, payload, url, serviceAccountEmail を詰めてあげれば良いです。

たとえばこんな感じです。

```tsx
await createHttpTaskWithToken({
  project: "demo-project", // ここはGCPのproject名
  location: "asia-northeast1", // queueが存在するリージョン名
  queue: "my-queue", // queue名
  payload: { message: "hogehoge" }, // ここはリクエスト送信時に詰めるパラメータ
  url: "https://asia-northeast1-demo-project.cloudfunctions.net/helloWorld", // エンドポイントのURL
  serviceAccountEmail: "hogehoge@demo-project.iam.gserviceaccount.com", // 作成したサービスアカウントのEmail
});
```

参考 ↓

https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/HEAD/cloud-tasks/app/createTask.js

https://cloud.google.com/tasks/docs/tutorial-gcf?hl=ja#creating_the_task

## タスク作成ロジックの呼び出し

要件に合わせて先ほどの呼び出しロジックを記述してあげれば良いです。

たとえば毎日 9 時に実行するとかであれば、以下のようになるかと思います。

```ts
export const helloWorldScheduler = functions.pubsub
  .schedule('0 9 * * *')
  .timeZone('Asia/Tokyo')
  .onRun(async (context) => {
    try {
        await createHttpTaskWithToken({
          project: 'demo-project', // ここはGCPのproject名
          location: 'asia-northeast1', // queueが存在するリージョン名
          queue: 'my-queue', // queue名
          payload: {message: 'hogehoge'}, // ここはリクエスト送信時に詰めるパラメータ
          url: 'https://asia-northeast1-demo-project.cloudfunctions.net/helloWorld', // エンドポイントのURL
          serviceAccountEmail:　'hogehoge@demo-project.iam.gserviceaccount.com', // 作成したサービスアカウントのEmail
        });
      }
      return;
    } catch (error) {
      console.error(error);
      throw new Error('failed to helloWorldScheduler');
    }
  });
```

たとえば、毎日決まった時間（朝 9 時とか）に登録ユーザー全員に対して一人ずつ重たい処理を行わなければいけない、という状況があったとしても、ユーザー一人一人の処理用にタスクを作成して、そのタスクを経由して処理を行えば、Cloud Functions がタイムアウトせずに、全てのユーザーに対して処理が行えます。

こういった負荷を分散させる役割も期待できるので、Cloud Tasks を使った実装を手段として持っていると便利です。

## 引っかかったポイント

- チュートリアルで audience が `new URL(url).origin` だったけど、フルパスが必要だった

https://dailydevsblog.com/troubleshoot/resolved-cloud-functions-cloud-tasks-unauthenticated-error-126840/