Cloud Functions にシンプルな Node の関数をデプロイする

# 概要

GCP の Cloud Functions を使おうと思って色々調べていたら、サクッとシンプルなファンクションをデプロイしたいだけなのに意外とシンプルな解に辿り着けませんでした。

なので**ここではシンプルな使い方を書きます。**
同時に微妙なハマりどころの解決方法も記載します。

そして Express をデプロイする方法についても書きます。
これくらいならギリギリシンプルです。多分。

ググると Firebase が云々とかの情報が多く、「シンプルにファンクションをデプロイしたいだけなんだ...Firebase は関係ないんだ...」と思いました。

Firebase 使わずに Cloud Functions を使うケースってそんなにないのかな。

もちろん TypeScript です！
ギリギリシンプル！

# 使うもの

- gcloud コマンド
- @google-cloud/functions-framework

特別なものはこれだけです。
gcloud は手元のマシンで使えるようになっている前提で書きます。

## @google-cloud/functions-framework

https://github.com/GoogleCloudPlatform/functions-framework-nodejs

GCP でサーバーレスな処理を実装する上で色々サポートしてくれる公式ライブラリです。
[公式ドキュメント](https://cloud.google.com/functions/docs/running/function-frameworks?hl=ja)でも言及があります。

なんならシンプルなデプロイに関してはこれの README を読めばいいだけという感じもする。

今回は記述したファンクションをサクッとローカル環境でホスティングしてくれる機能を使います。

# 実装

## package.json

必要最低限のものだけ記載します。

```package.json
{
  "main": "dist/index.js",
  "scripts": {
    "build": "好きなビルド方法",
    "dev": "functions-framework --target=simpleServerlessFunction",
    "deploy": "gcloud functions deploy simpleServerlessFunction --runtime nodejs16 --trigger-http --allow-unauthenticated --memory=必要な値",
  },
  "devDependencies": {
    "@google-cloud/functions-framework": "^3.1.0",
    "@types/express": "^4.17.13",
    "typescript": "^4.6.3"
  }
}

```

### main

main に指定する値は重要です。
**デプロイ時も、これから使うfunctions-frameworkはpackage.jsonのmainに指定してあるファイルをデフォルトで対象にする**ので、ビルド後のファイルを指定してください。

### build

ビルドはtscでもesbuildでも何を使ってもOKです。

### 開発サーバ

```
$ functions-framework --target=simpleServerlessFunction
```

functions-frameworkが開発用のサーバを立ち上げてくれます。
ここで指定する **`--target` は実際にコード上で export する関数名と同じである必要があります。**

### deploy

```
$ gcloud functions deploy simpleServerlessFunction --runtime nodejs16 --trigger-http --allow-unauthenticated --memory=256MB
```

デプロイはこれを叩くだけです。
実際には CloudBuild にレポジトリのコードをアップロードし、ビルドしてデプロイしてくれます。
なので実行時の依存パッケージなどがあってもビルドコマンドは変わりません。

#### --allow-unauthenticated

認証なしでアクセスできるようにします。
URLを知っていれば誰でも叩けるようになるのでお気をつけください。


<details>
<summary>その他のオプションについて（クリックで展開）</summary>
<div>

#### --runtime

2022/04/20現在、Nodeの場合は 16 が推奨です。

最新の値はこちらをご確認ください。
https://cloud.google.com/functions/docs/concepts/nodejs-runtime?hl=ja

#### --trigger-http

これでデプロイ後すぐに発行されるURLにHTTPでアクセスできます。


#### --memory

必須ではないですが、メモリが原因で実行中にエラーになるようなら指定しましょう。
料金はお気をつけを。

このコマンドのもう少し詳しい説明はこちら
https://cloud.google.com/functions/docs/deploying/filesystem?authuser=0&hl=ja


</div>
</details>


### @types/express

なぜ express の型を、という感じなんですが、**ファンクションの引数に渡ってくる req, res の型は express.Request と express.Response なのです。**
（正確には、functions-framework が提供してくれるハンドラの型がそれらを参照しています。）
なので express を使用しなくても型だけは入れておくと便利です。


## src/index.ts

パスもファイル名もなんでも良いのですが、ビルドした実際の js ファイルが package.json の main に指定したパスになるようにしましょう。

```src/index.ts
import { HttpFunction } from "@google-cloud/functions-framework";

const simpleServerlessFunction: HttpFunction = (req, res) => {
  res.send('Hello, World');
};

```


# デプロイ

```
（ビルドは忘れずに。predeploy とかで指定しても良いと思います）
yarn deploy
```

デプロイは数分かかります。
`.gcloudignore` が無い場合、デプロイ初回時に作成されます。

## dist/index.js does not exist が出たら

自動生成される .gcloudignore には以下のような１文が含まれています。

```
#!include:.gitignore
```

これは指定したファイルの中身を .gcloudignore として読み込んでいます。

つまり、この状態だと .gitignore に含まれるファイルはデプロイ時に GCP に送信されないのですね。
便利ではありますが、**ビルド後のファイルを　.gitignore に含めている場合、デプロイ時にファンクションの実際のコードが ignore されて送信されないので上記のエラーが出ます。**

この1行は便利のために存在しているので、それぞれの環境に合わせて消して無視したいファイルを直接書くか、もうこのままでビルドしたファイルを .gitignore から外してしまう（ビルド後のファイルもコミットする）かすると解決します。

## URL

さてこれで完了です。
デプロイが成功していれば

```
httpsTrigger:
  securityLevel: SECURE_OPTIONAL
  url: https://hogehoge.cloudfunctions.net/simpleServerlessFunction
```

のようなログが出ているはずです。このURLが実行可能なファンクションのURLになります。
（Webコンソールからも確認可能です。）

# Express をデプロイしよう

さて、ここまでで最低限のファンクションのデプロイは終わりですが、Expressとか使いたいときもありますよね。

デプロイしたいファンクションにUIが欲しいなら、Expressをデプロイしてその中のルーティングでHTMLを返すことも可能です。
小さい単発のWebサービスならそれだけでできちゃいますね！

想定として、ルートは単純なHTMLを返し、そのHTML上から fetch で別のパスを叩く、という構成で簡単なWebサービスと作るとします。

## public/index.html

これもパスはなんでもOKです。
とにかくHTMLファイルを作ってファンクションの中で Express に渡します。

```public/index.html
<!DOCTYPE html>
<html>

...

<!-- 超単純に fetch を使ったり -->
<script>

const onSomething = async () => {
  const response = await fetch("/api", { ... });
};

</script>

...

</html>
```

## src/index.ts


```src/index.ts
import express from "express";

const simpleServerlessFunction = express();
// HTMLからのリクエストを json で受け取る
simpleServerlessFunction.use(express.json());

const router = express.Router();
router.post("/path/to/api", () => {
  // 実際の処理
});

simpleServerlessFunction.use("/api", router);
// dirty workaround....
simpleServerlessFunction.use("/simpleServerlessFunction/api", router);

// それ以外はHTMLを返す
simpleServerlessFunction.use("/", (req, res) =>
  res.sendFile(path.join(__dirname, "../public/index.html"))
);

export { simpleServerlessFunction };
```

### Express をそのまま export

express のインスタンスをそのまま export すれば処理としてはOKです。

export するファンクション名は前述の通り重要です。
コマンドで指定する文字列と合わせましょう。

今回はそのまま export できるように `express()` の返り値をファンクション名にしています。
（慣習的には `app` を使いますね）


### API のルートを二つ...

`dirty workaround...` としている部分なのですが、これは開発環境と本番環境の差分吸収のためです。

<details>
<summary>本番環境で API へのアクセスが CORS などのエラーで止まるようであればご確認ください。（クリックで展開）</summary>
<div>

functions-framework は開発用サーバのルートに関数をホスティングするのですが、実際本番はオリジンの後に `/simpleServerlessFunction` （ファンクション名）が入ります。

今回は HTML は静的にしたかったので環境変数とか使いたくないので、HTML 上の fetch が指定する URL は固定です。
例えば `/api` とかやっておくと開発環境では通りますが、本番では通りません、本番ではパスが `/simpleServerlessFunction/api` になるからです。

これでルートを一つにした状態で普通に fetch をすると、本番環境ではこのファンクションではない別の Google のURL（ htps://hogehoge.cloudfunctions.net/api ）にアクセスが飛び、リダイレクトが走り、リクエストがクロスオリジンになり、 CORS のエラーが出ます。
同じサイト上のリクエストなのになんで CORS エラーなんだと少しハマりました。

他にも良い方法がある気はしますが、 functions-framework のコマンドにパスを指定する、みたいなオプションもパッと見つからなかったのでルートを二つ書きました😇

誰も困らないので問題なし。
これで誰かが困る場合は他の方法をご検討ください。

</div>

</details>

# おしまい

シンプルな Cloud Functions の使い方のお話でした。