関数の数が多くなってきたらどうする？Cloud Functions for Firebaseのソース構成

# はじめに

FirebaseのCloud Functionsはindex.ts(Javascriptの場合はindex.js)に関数を追加することで、複数の関数を作成していくことができるのですが、数が多くなってくるとどうしてもデプロイに時間がかかりますし、デプロイエラーもかなり発生しやすくなります。

そんな中、changelogを見ていると、最近のfirebase-toolsで変更された関数のみがデプロイされる仕組みが導入されて、デプロイ時間を短くできるようになりました。

がしかし、この機能、書き方によってはうまく動かないので、どうしたらいいか検証してみました。


## 今まで通りindex.tsに複数関数を書いたとき

まずは単純に２つの関数を追加してみます。

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

export const functionA = functions
    .region("asia-northeast1")
    .https.onRequest(() => {
      console.log("A");
    });

export const functionB = functions
    .region("asia-northeast1")
    .https.onRequest(() => {
      console.log("B");
    });

```

ではこのfunctionBを変えてデプロイしてみます。

```diff_typescript
-      console.log("B");
+      console.log("BB");      
```

```sh
i  functions: updating Node.js 16 function functionA(asia-northeast1)...
i  functions: updating Node.js 16 function functionB(asia-northeast1)...
```

うーん、だめでした。

そもそもどうやって更新されたかを判断しているかを考えないと正解には辿り着けなそうです。

## ソースを解析してみます。

`firebase-tools`がデプロイ処理の実体となるので、そこをソースで読んでどこで違いを判断しているかを把握してみます。

リンク先のところでhashの比較しているのがわかります。

https://github.com/firebase/firebase-tools/blob/master/src/deploy/functions/deploy.ts#L149

```ts
    return haveEndpoint.hash && wantEndpoint.hash && haveEndpoint.hash === wantEndpoint.hash;
```

ここで今のソースと以前のソースの違いをみていそうです。
さらにこのhashというものが何かをみていきます。

この先のソースはネストも深いので省略しますが以下のことをやっていました。

- ソースコード全体をファイルごとにハッシュ化し、それをまとめたハッシュを作成
- 環境変数のハッシュ化
- シークレット変数のハッシュ化
- それぞれのハッシュをくっつけてさらにハッシュ化
- それを上の比較分で比較

一応ハッシュについて説明しておくと、ハッシュ（関数）は不可逆変換の任意のデータを一定の長さのデータに変換するものですが、ちょっとした違いでも基本的には別のデータになる、同じデータであれば同じ結果になるという特性を生かして、こういったデータの差分を測るのによく利用されるものです。

この中の処理のソースコード全体は、デフォルトだとfunctionsフォルダ全体を指すので、srcにあるプログラムだけではなく、package.jsonからlib配下にあるjsにbuildしたものまで全部が対象です。

結果、基本的にはソースの変更で変更がない関数はskipさせるという処理は動かないことがわかりました・・・。

だったらいつskipされるんだよ！

## skipが実行される時

上のロジックからすると、何も変更がないときはskipされますよね。
あとは、環境変数とかシークレット関数のところの変更の仕方によってはskipが動きそうです。

ただそれだけだとソースの差分見ている価値がないですよね。

実は、Firebase CLI v10.7.1のときにcodebaseという仕組みが導入されています。
これは、codebaseというグルーピングを行うことでpackage.jsonから別のプログラムとしてfunctionsを構築できる仕組みで、これにより全然依存関係がなかった関数群を分けることができ、効率的な関数のインスタンスを作ることができます。

この仕組みを使うと、対象のソースコードという部分が分割できそうです！

### codebaseで分けてみる

codebaseを分けるために、フォルダ構成を変更します。

```
├── repoA
│   ├── lib
│   │   ├── functionA.js
│   │   ├── functionA.js.map
│   │   ├── index.js
│   │   └── index.js.map
│   ├── package-lock.json
│   ├── package.json
│   └── src
│       ├── functionA.ts
│       └── index.ts
└── repoB
    ├── lib
    │   ├── functionB.js
    │   ├── functionB.js.map
    │   ├── index.js
    │   └── index.js.map
    ├── package-lock.json
    ├── package.json
    ├── src
        ├── functionB.ts
        └── index.ts
```

フォルダ変更したときに　package.jsonが分かれるのでそれぞれでnode_modulesの作り直しは必要です。
これだけだとfirebaseに設定が伝われないので、firebase.jsonも変更します。

```json:firebase.json
  "functions": [
    {
      "source": "functions/repoA",
      "codebase": "repo-a",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ],
      "predeploy": [
        "npm --prefix \"$RESOURCE_DIR\" run build"
      ]
    },
    {
      "source": "functions/repoB",
      "codebase": "repo-b",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ],
      "predeploy": [
        "npm --prefix \"$RESOURCE_DIR\" run build"
      ]
    }
  ]
```
※ めんどうだったので、predeployのlintは一旦消してます！

これで`firebase deploy --only functions`を実行すると同じようにデプロイされていると思います！
そして、最初の試したようなコードの変更を加えると、変更を加えた側にskippedが表示されていると思います！


# 終わりに

Cloud Functionsも、他のFirebaseのサービスと同様、かなりいろんな改善が行われていて、以下のように新しい仕組みがどんどん増えていっています。

- cold standby時間省略
- configを.env方式で行うように
- secret managerを扱いやすく
- gen２の追加
- etc

追っていくのが大変ですが、いままでできなかったことができるようになったり素晴らしい改善が行われているので、うまく扱って、いいサービスをガンガン作っていきましょう！