Firebase Cloud Functions から Google Cloud Run functions への移行

## はじめに

[GitHub Copilot Extensions](https://docs.github.com/ja/copilot/using-github-copilot/using-extensions-to-integrate-external-tools-with-copilot-chat#github-copilot-extensions-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6) というものが最近パブリックベータになったようで、コード開発支援ツールである GitHub Copilot の VSCode 拡張から、外部サービスとシームレスに連携できるような機能が利用可能となった。自分も Copilot のライトユーザーとして、何か試してみたいと思い、GitHub Marketplace で利用可能な拡張を物色していたところ、Sentry Copilot Extension を見つけた。「今稼働している Firebase Cloud Functions を Sentry で監視するようにしてエラー発生させてそれを VSCode から可視化できたら面白いかも」と思ったものの、本拡張が Firebase Cloud Functions に対応していないらしく、はたまた困ってしまったが、Google Cloud Run functions はサポートしているようで、いっちょ変えたれ、と思って本件に至る。

## ユースケース

Pubsub からメッセージを受け取って発動する Functions を Firebase Cloud Functions(第 1 世代) から Google Cloud Run functions(第 2 世代) に書き換える。

## 前提

### 環境

```sh
node -v
# v18.18.0

gcloud -v
#Google Cloud SDK 494.0.0
#bq 2.1.8
#core 2024.09.20
#gcloud-crc32c 1.0.0
#gsutil 5.30
```

### 条件

- Pub/Sub トピックはあらかじめ作成されているとする（`my-topic`）

## やったこと

### 環境準備

- [`gcloud` をインストール](https://cloud.google.com/sdk/docs/install?hl=ja) する。
- gcloud の環境を初期化する。
	```sh
	gcloud init
	```

詳しくは本家を参照

https://cloud.google.com/functions/docs/tutorials/pubsub?hl=ja

### アプリ改修

functions のメイン？コンポーネントを変更する

```diff_typescript
- import * as functions from 'firebase-functions';
+ import * as functions from '@google-cloud/functions-framework';
```

もちろん、その前に install は必要だ

```sh
npm i --save @google-cloud/functions-framework
```

次に、エントリーポイント関数を書き換える。

Firebase Cloud Functions(第 1 世代) の場合、以下のようなコードであった:

```ts
import * as functions from 'firebase-functions';

exports.myFunction = functions
  .runWith({ timeoutSeconds: 540, failurePolicy: true, memory: '1GB' })
  .region('asia-northeast1')
  .pubsub.topic("my-topic")
  .onPublish(async (message, context) => {
    // decode message
    const decoded = Buffer.from(message.data, 'base64');
    ...
  })
```

特徴として、**後段の関数デプロイ時の設定値をプログラム内に書き込む**ことである。
タイムアウト (`timeoutSeconds`)、関数に割り当てるメモリ (`memory`)、リージョン (`region()`)、対象 pubsub トピック (`pubsub.topic()`) などを指定できる。

これを、新しくしたのがこちら:

```ts
import * as functions from '@google-cloud/functions-framework';

interface PubSubData {
  subscription: string;
  message: {
    messageId: string;
    publishTime: string;
    data: string;
    attributes?: { [key: string]: string };
  };
}

functions
  .cloudEvent<PubSubData>('myFunction', async (cloudEvent) => {
    // access encoded event data
    const data = cloudEvent.data?.message.data;
    // decode event data
    const decoded = Buffer.from(data, 'base64');
    // ...
  })
```

Firebase の場合のような指定項目はなく、シンプルになった。
が、これは後続のデプロイコマンドで指定することになる。

参考:

https://cloud.google.com/functions/docs/writing/write-event-driven-functions?hl=ja

### deploy

- Firebase Cloud Functions(第 1 世代) の場合

```sh
firebase deploy --only functions
```


- Google Cloud Run functions(第 2 世代) の場合

```sh
gcloud functions deploy <FUNCTION_NAME> --runtime <RUNTIME> --trigger-topic=<TOPIC_NAME> --region=<REGION> --memory=<memory> --timeout=<timeout_in_second>

#e.g.
# gcloud functions deploy myFunction --runtime nodejs18 --trigger-topic=my-topic --region=asia-northeast1 --memory=1GB --timeout=540
```

エントリーポイント関数が簡潔だった分、そっくりそのままデプロイコマンドに跳ね返ってきた形となる。

参考:

https://cloud.google.com/functions/docs/calling/pubsub?hl=ja


## 遭遇したエラー

### secret manager 周りでエラー

本アプリでは、Secret Manager を 利用しており、API 経由で情報を取得してきているのだが、API アクセス時に以下のようなエラーが発生した:

```
Error: 7 PERMISSION_DENIED: Permission 'secretmanager.versions.access' denied for resource ...
```

対応は、[こちら](https://stackoverflow.com/questions/62444867/secret-manager-access-denied-despite-correct-roles-for-service-account) を参照した。

やるべきことは、以下のデプロイコマンド実行時のメッセージに対し：

```sh
gcloud functions deploy slackNotification-g2 --runtime nodejs18 --trigger-topic=notification-pubsub-topic2 --region=asia-northeast1 --memory=1GB
Preparing function...done.                                                                    ...     
  service: projects/xxxxx/locations/asia-northeast1/services/myFunction
  serviceAccountEmail: xxxxxxxxxxx@developer.gserviceaccount.com
  ...
```

`serviceAccountEmail` のアカウント `xxxxxxxxxxx@developer.gserviceaccount.com` のサービスアカウントに対して、IAM ページでロール（`Secret Manager Secret Accessor`）を追加する。

- **IAM と管理** > **IAM** に遷移し、編集アイコンクリック

![スクリーンショット 2024-10-03 0.07.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/1b5cfb06-2751-50e0-7a86-fa4df8f0af2c.png)

- 「別のロールを追加」クリック

![スクリーンショット 2024-10-03 0.10.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/3b9e69ec-3b48-b99c-a9c9-19103d9a68f5.png)

- `Secret Manager Secret Accessor` のロールを付与

![スクリーンショット 2024-10-03 0.12.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/1ec8caa8-527f-90a9-ba5f-2d49140d32fb.png)


### Google Sheets API でエラー

本アプリでは、Google Sheets への書き込みを API 経由で行っており、API コール時に以下のようなエラーが発生した:
```sh
The caller does not have permission
```

対応は、[こちら](https://stackoverflow.com/questions/38949318/google-sheets-api-returns-the-caller-does-not-have-permission-when-using-serve) を参照した。

書き込みたい Sheets ページで、**共有** クリック > サービスアカウントを追加する必要がある。このサービスアカウントは前述の Secret Manager Secret Accessor ロールを追加したアカウントと同じものである。

- **共有**クリック
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/d811c81f-e9d3-0d56-5c72-60e4addcdb67.png width=300>

- サービスアカウントを追加
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/3712c0ef-2da0-d67b-e1d6-a192b1c3e9bf.png width=500>


:::note info
Secret Manager おより Google Sheets での両エラーは、思い返してみると最初本アプリを Firebase で作った時のも同じエラーが出た記憶がある。つまり、Google Cloud Run functions だから起こったのではなく、サービスアカウントが変更になったから、その分同じ権限を付与する必要があったということと考えられる。
:::

## 移行完了

サービスアカウントのニアミスさえ気をつければ、移行作業は小一時間で終わりそうなものであった。
一応、両方の Functions を触ってみての感想としては、Firebase の方が好みであることもわかった。
理由は、デプロイ設定値をプログラム中に宣言的に記述できるからである。
デプロイにまつわる設定値の管理上、ソースコードに入れられた方が取り扱いがしやすいだろう（デプロイコマンドをシェルスクリプトに書いて、それを管理するという方法もあるが、取り回しが一段下がる気がする）

こうして、当初の目的を達成すべくようやくスタート地点につけた。
Sentry Copilot Extension については引き続き作業を継続し、成果が出たら共有したい。