Google Cloud Functions / Cloud Functions for Firebaseの微妙な使い勝手

AWSのLambdaは高機能だが、GCPのCloud Functionsは機能が少なめでシンプル。ただ微妙なクセがある。

Google Cloud FunctionsとCloud Functions for Firebaseの関係

Cloud Functions for FirebaseはGoogle Cloud Functionsを簡素化してFirebaseで使えるようにしたもの

  • Node.jsのバージョン8のみ
  • Google Cloud Functionsは独自ドメインが使えないが、Cloud Functions for Firebaseでは使える(独自ドメインSSLのWeb APIが可能)。
  • 複数のFunctionsを使う構造になっていない(index.jsのみ)→工夫が必要
  • 管理画面からのデプロイができない
  • Cloud Functions for Firebaseで登録した関数は同じプロジェクトのGCPの管理画面のCloud Functions for Firebaseから確認編集できる。

AWSのLambdaとは違い、Firebase CLIだけでデプロイできるので簡単ではある。

Google Cloud Functionの豆知識とハマるポイント

Firebaseでは関数だけのデプロイが可能

firebase deployだけだとHostingなどのデプロイも同時に行ってしまう。

firebase deploy --only functions

とすると無駄なデプロイがなくなる

Cloud Functions for FirebaseではNode.js以外の言語も扱える

Cloud Functions for FirebaseではNode.jsしか使えないと思っている人が多いかもしれないが、実はPythonやGoも使える。Firebase CliからはNode.jsしかデプロイできないが、同じプロジェクトのGCPの画面で関数を登録し、Firebase Hostingのrewritesの設定でその関数を指示すればOK。

my-project/firebase.json

{
    :
  "hosting": {
      :
    "rewrites": [{
        "source": "/エンドポイント名",
        "function": "pythonの関数名"
    }],
      :
  }
    :
}

これで

https://独自ドメイン/エンドポイント名

へのHTTPアクセスでGoogle Cloud Functionで登録したPythonの関数が実行できる。

Cloud Functions for FirebaseはGoogle Cloud Functionsの(機能を制限した)ラッパーである。上記のように先にGoogle Cloud FunctionsにPythonの関数を登録して後でFirebaseから参照する使い方もできるし、逆にFirebase側で登録した関数をGCPのGoogle Cloud Functionsの管理画面で操作できる。ログもGCPのStackDriverで扱うことができる。

Cloud Functions for Firebaseのロケーション設定

Cloud Functions for Firebaseのロケーション設定ではデフォルトのus-central1を使わないほうがちょっとだけいい。
というのもストレージを使う場合にus-central1を選択できないから。us-east1us-east4なら同じロケーションになって転送時間と料金をちょっとだけ節約できる。

Google Cloud Functionではcookieが使えない

Google Cloud Functions(Cloud Functions for Firebaseも)ではセッション以外のcookieが使えない。ブラウザに保存したデータを使うことが不可能で、ITP対策の実装はGoogle Cloud Functionsではできない(AWS Lambdaとの大きな違い)

自ドメイン割り当て時のreq.hostname

Google Cloud Functions for Firebaseで自ドメインを割り当てていても、req.hostnameは自ドメインではなくロケーション名-プロジェクト名.cloudfunctions.netになる。

Node.jsを使ったときのロギング

標準/エラー出力がStackDriverのログに出ず、ログレベルを設定して簡単に出し分けるlogger.debug()のようなことするにはbunyanかwinstonが必要。一般的なlog4jsは使えない。

bunyan一式のインストール

cd my-project/functions/
npm install bunyan @google-cloud/logging-bunyan express-bunyan-logger --save

しかしbunyanはexpressとの連携が微妙で、結局WebAPI用途でのログを出すのが難しい。ここがLambdaとの大きな違い。

Google Cloud Functions for Firebaseの細かい使い方

ESLintのインストール

firebase initでESLintを使う設定にした場合

cd my-project/functions/
npm install eslint eslint-plugin-promise@latest --save-dev
vi package.json

my-project/functions/package.json

  "scripts": {
      :
    "lint": "./node_modules/.bin/eslint ."
  },

独自ドメインのエンドポイントを設定する

単純に初期状態(firebase.jsonを未編集の状態)でfirebase deployから関数をデプロイするだけでは独自ドメインは使えない。まずこの状態では

https://ロケーション名-プロジェクト名.cloudfunctions.net/関数名

が関数のエンドポイントになる。独自ドメインからアクセスできるようにするためにはFirebase Hostingでトラフィックを関数に流すrewritesの設定が必要。

my-project/firebase.json

{
    :
  "hosting": {
      :
    "rewrites": [{
        "source": "/エンドポイント名",
        "function": "関数名"
    }],
      :
  }
    :
}

これでデプロイすると

https://独自ドメイン/エンドポイント名

がエンドポイントとして使えるようになる。Firebase HostingがLambdaでいうAPI Gatewayのような役割を果たすわけである。
(この設定をした直後はfunctionsのデプロイだけではなく、hostingのデプロイも必要になる)

Expressを使う

my-project/functions/index.jsが関数を記述するファイルになる

const express = require('express');
const app = express();
//app.use(require('cookie-parser')); // 結局cookie自体使えない
//app.use(require('express-bunyan-logger')({logger})); // うまくいかない

// メインロジック
app.get('/my-method-1', (req, res)=>{
    :
});

app.get('/my-method-2', (req, res)=>{
    :
});

const functions = require('firebase-functions');
module.exports = functions.https.onRequest(app);

関数ファイルを複数扱う

デフォルトでは関数は1ファイルmy-project/functions/index.jsしか設定できない。そこで一工夫。my-project/functions/src/というディレクトリを作り、その中に個別の関数のファイルを格納する。

my-project/functions/src/my-func1.js

const functions = require('firebase-functions');
module.exports = functions.https.onRequest((req, res)=>{
  :
});

Expressを使ってもいいし、この要領で複数の関数のファイルを配置していく。

my-project/functions/index.js

const funcs = {
  func1: './src/my-func1.js',
  func2: './src/my-func2.js',
    :
}

loadFunctions = (funcsObj) => {
  for(let name in funcsObj){
    if(! process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === name) {
      exports[name] = require(funcsObj[name])
    }
  }
}

console.log('process.env.FUNCTION_NAME:', process.env.FUNCTION_NAME)
loadFunctions(funcs)
console.log('exports:', exports)

定数funcs{関数名: ファイル名}の形式で個別の関数のスクリプトファイルを指定する。

https://uyamazak.hatenablog.com/entry/2018/10/22/113000

関数のURL(パス)を自由に設定する

上記の「Expressを使う」と「関数ファイルを複数扱う」を組み合わせた設定

my-project/functions/index.js

const funcs = {
  func1: './src/my-func1.js',
  func2: './src/my-func2.js',
    :
}

loadFunctions = (funcsObj) => {
  for(let name in funcsObj){
    if(! process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === name) {
      exports[name] = require(funcsObj[name])
    }
  }
}

console.log('process.env.FUNCTION_NAME:', process.env.FUNCTION_NAME)
loadFunctions(funcs)
console.log('exports:', exports)

my-project/functions/src/my-func1.js

const express = require('express');
const app = express();

// メインロジック
app.get('/my-method-1', (req, res)=>{
    :
});

app.get('/my-method-2', (req, res)=>{
    :
});

const functions = require('firebase-functions');
module.exports = functions.https.onRequest(app);

では

https://ロケーション名-プロジェクト名.cloudfunctions.net/func1/my-method-1

が各メソッドにアクセスするURLとなる。これを独自ドメイン

https://独自ドメイン/apipath/func1/my-method-1

でアクセスできるようにしたい。、まず挿入されたパス/apipathを扱うには

my-project/functions/src/my-func1.js

const express = require('express');
const app = express();

// メインロジック
app.get('/my-method-1', (req, res)=>{
    :
});

app.get('/my-method-2', (req, res)=>{
    :
});

const functions = require('firebase-functions');
// ここから先が違う
const main = express();
main.use('/apipath', app); // パス名を入れる
exports.main = functions.https.onRequest(main);

URL処理のロジックを1段階追加する。これで

https://ロケーション名-プロジェクト名.cloudfunctions.net/apipath/func1/my-method-1

でメソッドにアクセスできるようになる。ここで独自ドメインのrewrites設定を追加する。

my-project/firebase.json

{
    :
  "hosting": {
      :
    "rewrites": [{
        "source": "/apipath/**",
        "function": "func1"
    }],
      :
  }
    :
}

これで

https://独自ドメイン/apipath/func1/my-method-1

から指定のメソッドを呼び出すことができる。

https://stackoverflow.com/questions/44959652/firebase-hosting-with-dynamic-cloud-functions-rewrites

GCP/Firebase の記事一覧